初めに
今回は配列に関連するメソッドと、配列にempty slotを見つけるメソッドをまとめてみました。
Methods
Add/Remove
Remove: Array.prototype.pop() & Array.prototype.shift()
pop()
末尾から取り出す。
shift()
先頭から取り出す。
(いずれも元の配列を変更する。)
let fruits = ['Apple', 'Orange', 'Lemon', 'Banana', 'Watermelon'];
fruits.pop()
console.log(fruits); // [ 'Apple', 'Orange', 'Lemon', 'Banana' ]
console.log(fruits.pop()); // Banana
console.log(fruits); // [ 'Apple', 'Orange', 'Lemon' ]
Add: Array.prototype.push() & Array.prototype.unshift()
push()
:末尾に入れる。
unshift()
:先頭に入れる。
(いずれも元の配列を変更する。)
let fruits = ['Apple', 'Orange', 'Lemon', 'Banana', 'Watermelon'];
fruits.shift()
console.log(fruits); // [ 'Orange', 'Lemon', 'Banana', 'Watermelon' ]
console.log(fruits.shift()) // Orange
console.log(fruits); // [ 'Lemon', 'Banana', 'Watermelon' ]
Delete/Replace/Copy
Array.prototype.splice()
splice()
:要素を削除や置き換え、または挿入する。
(元の配列を変更する。)
// arr.splice(start[, deleteCount, insertItem1, ..., insetItemN])
let fruits = ['Apple', 'Orange', 'Lemon', 'Banana', 'Watermelon'];
// delete
console.log(fruits.splice(0, 1)); // [ 'Apple' ]
console.log(fruits); // [ 'Orange', 'Lemon', 'Banana', 'Watermelon' ]
// replace
console.log(fruits.splice(0, 2, 'Pear')); // [ 'Orange', 'Lemon' ]
console.log(fruits); // [ 'Pear', 'Banana', 'Watermelon' ]
// insert
// fruits.splice(0, 0, 'Apple', 'Orange');
// console.log(fruits) // [ 'Apple', 'Orange', 'Pear', 'Banana', 'Watermelon' ]
let arr = ['Apple', 'Orange'];
fruits.splice(0, 0, ...arr);
console.log(fruits); // [ 'Apple', 'Orange', 'Pear', 'Banana', 'Watermelon' ]
負のインデックスは末尾から逆行する意味で、
-1
:最後の要素から逆行。(=arr.length -1
)
arr.length
:最後の要素の後から。
// negative index
console.log(fruits.splice(-1, 1)) // [ 'Watermelon' ]
console.log(fruits) // [ 'Apple', 'Orange', 'Lemon', 'Banana' ]
// fruits.splice(-1, 0, 'Watermelon')
// console.log(fruits) // [ 'Apple', 'Orange', 'Lemon', 'Watermelon', 'Banana' ]
fruits.splice(fruits.length, 0, 'Watermelon');
console.log(fruits) // [ 'Apple', 'Orange', 'Lemon', 'Banana', 'Watermelon' ]
Array.prototype.slice()
slice()
:要素を浅いコピーして新しい配列を返す。
// arr.slice(start[[, end]])
let newArr = [
{ list: ['one Egg', 'two cups Water'] },
['We need flour to bread!'],
'Apple'
];
// shallow copy
let spreadArr = [...newArr];
let sliceArr = newArr.slice();
spreadArr[0].list = ['two Egg', 'two cups Milk'];
sliceArr[1][0] = ['Maybe rice flour is better.'];
spreadArr[2] = ['Apple', 'Orange'];
spreadArr[2][1] = ['No Orange!'];
console.log(newArr);
// [
// { list: [ 'two Egg', 'two cups Milk' ] },
// [ [ 'Maybe rice flour is better.' ] ],
// 'Apple'
// ]
console.log(spreadArr);
// [
// { list: ['two Egg', 'two cups Milk'] },
// [['Maybe rice flour is better.']],
// ['Apple', ['No Orange!']]
// ]
console.log(sliceArr);
// [
// { list: [ 'two Egg', 'two cups Milk' ] },
// [ [ 'Maybe rice flour is better.' ] ],
// 'Apple'
// ]
newArr[0]
:object type
newArr[1]
:object type
newArr[2]
:primitive type
JavaScriptのオブジェクトはpass by sharing
(参照先の共有)なので、元の配列の要素がオブジェクトタイプであれば、浅いコピーした配列たちではproperty value
へのリアサイン(spreadArr[0].list
)や、ネストされた配列へのリアサイン(sliceArr[1][0]
)などの操作が元の配列を変える。
それに対し、浅いコピーした配列spreadArr[2]
でいくら値をリアサインしてもプリミティブタイプのnewArr[2]
は変えられません。そしてnewArr[2]
と参照先を共有している配列sliceArr
も変えられません。
この場合は、値が変えられたのは配列spreadArr
だけです。そしてもしここで配列spreadArr
と参照を共有する配列copySpreadArr
で、オブジェクトタイプのcopySpreadArr[2][1]
にリアサインしたら、spreadArr[2][1]
も変えられる。
let copySpreadArr = spreadArr.slice();
copySpreadArr[2][1] = 'cinnamon';
console.log(spreadArr)
// [
// { list: [ 'two Egg', 'two cups Milk' ] },
// [ [ 'Maybe rice flour is better.' ] ],
// [ 'Apple', 'cinnamon' ]
// ]
配列のコピー操作では、Spread syntax
、Array.prototype.slice()
、Array.prototype.concat()
、Array.from()
は浅いコピーです。
プリミティブタイプの値はimmutable
(不可変)、状態を変えることができないため、浅いコピーとは深いコピーとは関係ないです。プリミティブタイプのコピーはいつも新しいメモリー位置に記録されるのでreal copy
と呼ばれる。
slice()
の第1引数は指定したい場合は0
です。
第2引数は常に目標配列のlength
です。
// copy all or apart of array
let newArr = fruits.slice(); // no specific index means copy all
console.log(newArr); // [ 'Apple', 'Orange', 'Lemon', 'Banana', 'Watermelon' ]
newArr = fruits.slice(2); // start from index 2
console.log(newArr); // [ 'Lemon', 'Banana', 'Watermelon' ]
newArr = fruits.slice(2, 4); // start from index 2, end at index 4 (not included)
console.log(newArr); // [ 'Lemon', 'Banana' ]
// negative index
newArr = fruits.slice(-1)
console.log(newArr); // [ 'Watermelon' ]
newArr = fruits.slice(-1, fruits.length)
console.log(newArr); // [ 'Watermelon' ]
newArr = fruits.slice(-2)
console.log(newArr); // [ 'Banana', 'Watermelon' ]
newArr = fruits.slice(2, -2)
console.log(newArr); // [ 'Lemon' ]
Merge
Array.prototype.concat()
concat()
:引数を浅いコピーして結合する。
(新しい配列を返す。)
配列はどんなデータ型でも格納できる。concat()
の引数が配列ではなくても浅いコピーして結合する。
// oldArr.concat(arg1, arg2...)
let fruits = ['Apple', 'Orange', 'Banana'];
let vegetables = ['Cabbage'];
let obj = {
a: '0'
}
let newArr = fruits.concat(vegetables, obj, 'abc');
console.log(newArr);
// [ 'Apple', 'Orange', 'Banana', 'Cabbage', { a: '0' }, 'abc' ]
concat()
は要素を浅いコピーするが配列の平坦化はできません。
// return one-dimensional array
let fruits = ['Apple', ['Orange', ['Banana']]];
let vegetables = ['Carrot', 'Cabbage'];
let newArr = fruits.concat(vegetables);
console.log(newArr);
// ['Apple', ['Orange', ['Banana']], 'Carrot', 'Cabbage']
浅いコピーなのでオブジェクトタイプの値に操作すると元の配列も変えられる。
let fruits = ['Apple', ['Orange', ['Banana']]];
let vegetables = [{ root: 'Carrot' }, { leaf: 'Cabbage' }];
let newArr = fruits.concat(vegetables);
console.log(newArr);
// [
// 'Apple',
// ['Orange', ['Banana']],
// { root: 'Carrot' },
// { leaf: 'Cabbage' }
// ]
newArr[1][1] = ['Lemon'];
console.log(fruits);
// [ 'Apple', [ 'Orange', [ 'Lemon' ] ] ]
newArr[2].root = 'Radish';
console.log(vegetables);
// [ { root: 'Radish' }, { leaf: 'Cabbage' } ]
[Symbol.isConcatSpreadable]
// Symbol.isConcatSpreadable
let arr = [1, 2];
let arrayLike = {
0: 'Apple',
1: 'Orange',
[Symbol.isConcatSpreadable]: true,
length: 2
};
console.log(arr.concat(arrayLike));
// [ 1, 2, 'Apple', 'Orange' ]
//
arrayLike = {
0: 'Apple',
1: 'Orange',
[Symbol.isConcatSpreadable]: false,
length: 2
};
console.log(arr.concat(arrayLike));
// [
// 1,
// 2,
// {
// '0': 'Apple',
// '1': 'Orange',
// length: 2,
// [Symbol(Symbol.isConcatSpreadable)]: false
// }
// ]
Iterate
thisArg
配列のイテレーターメソッドは、ほとんど最後の引数にthisArg
が設置されています。(例えばsort()
にはthisArg
ありません。)
let config = {
minAge: 18,
maxAge: 30,
match(user) {
return (user.age >= this.minAge) && (user.age < this.maxAge);
}
};
let veterans = {
minAge: 30,
maxAge: 40
}
let novice = {
minAge: 10,
maxAge: 18
}
let participant = [
{ name: 'Ada', age: 32 },
{ name: 'Leon', age: 25 },
{ name: 'Sherry', age: 16 },
{ name: 'Claire', age: 24 },
]
let candidates = participant.filter(config.match, config);
console.log(candidates);
// [ { name: 'Leon', age: 25 }, { name: 'Claire', age: 24 } ]
candidates = participant.filter(config.match, veterans);
console.log(candidates);
// [ { name: 'Ada', age: 32 } ]
candidates = participant.filter(config.match, novice);
console.log(candidates);
// [ { name: 'Sherry', age: 16 } ]
// note: if we use 'this' in 'config.match', we should set reference to 'thisArg'
上の例では、thisArg
でfilter()
のコールバックconfig.match
にthis
の参照先を設置しないとエラーになります。
thisArg
は一部の人(私も)にとって直観的に読みくいのであまり使われていませんが、実際thisArg
の設置を通してほかのオブジェクトに参照して分岐処理ができます。
下は整理したあとのコードです。
let config = {
candidates: { minAge: 18, maxAge: 30 },
veterans: { minAge: 30, maxAge: 40 },
novice: { minAge: 10, maxAge: 18 },
match(user) {
return (user.age >= this.minAge) && (user.age < this.maxAge);
}
};
let participant = [
{ name: 'Ada', age: 32 },
{ name: 'Leon', age: 25 },
{ name: 'Sherry', age: 16 },
{ name: 'Claire', age: 24 },
];
console.log(participant.filter(config.match, config.candidates))
// [ { name: 'Leon', age: 25 }, { name: 'Claire', age: 24 } ]
console.log(participant.filter(config.match, config.veterans))
// [ { name: 'Ada', age: 32 } ]
console.log(participant.filter(config.match, config.novice))
// [ { name: 'Sherry', age: 16 } ]
下のようにコールバックではなく関数内部での呼び出しではconfig.match
のthis
の参照がエラーにならないのです。
// or call it simply
let candidates = participant.filter((user) => config.match(user));
console.log(candidates);
// [ { name: 'Leon', age: 25 }, { name: 'Claire', age: 24 } ]
Array.prototype.forEach()
forEach
:全要素を走査する。
(元の配列を変更しない、いつもundefined
を返す。)
(empty slotをスキップする。)
// arr.forEach(function(item, index, array), thisArg)
let fruits = ['Apple', 'Orange', 'Lemon'];
fruits.forEach((item, index, array) => {
console.log(`${item} is at index ${index} in ${array}`);
});
// Apple is at index 0 in Apple,Orange,Lemon
// Orange is at index 1 in Apple,Orange,Lemon
// Lemon is at index 2 in Apple,Orange,Lemon
forEach
はコールバックで全要素を走査する。自体は何も返さないので常にundefined
(return
の値を指定しない場合のデフォルト)です。
// forEach always return undefined
console.log(fruits.forEach((item, index) => {
return item === 'Orange' ? index : 'undefined';
}))
// undefined
// note: if it can't return values of callback function, we shouldn't use it in Promise chain
// note: forEach always iterate all the elements, that is why it always return undefined
let arr = [];
console.log(fruits.forEach((item, index) => {
item === 'Orange' ? arr.push(index) : 'undefined';
}))
console.log(arr); // [ 1 ]
これはメリットでもデメリットでもあります。
メリットは全要素を一度イテレータする。
デメリットはreturn
は指定しないので早期終了ができないし、コールバックからの返り値も受け取らない。この同期関数の特徴がPromise
や非同期関数の使用に不向きです。プロミスチェインでは値をreturn
しなければ意味をもたらさない、await
非同期関数からの結果も待ちません。
let arr = [1, 2, 3];
let sum = 0;
// Promise
const sumFn = async (a, b) => a + b;
arr.forEach(async (item) => {
sum = await sumFn(sum, item);
});
console.log(sum); // 0
プロミスsumFn
ではarr
の要素と今のsum
の和を返すの待つ。しかしforEach
は要素の走査に集中するためawait sumFn(sum, item)
の返り値がスキップされ、sum
は0
のままでした。
もし元の配列が走査中に変更されたら、forEach()
のindex
も変えられてしまう。
let fruits = ['Apple', 'Orange', 'Lemon'];
fruits.forEach((fruit) => {
console.log(fruit);
if (fruit === 'Apple') {
fruits.shift();
}
});
// Apple
// Lemon
console.log(fruits)
// [ 'Orange', 'Lemon' ]
'Apple'
が削除され、残りの'Orange'
と'Lemon'
のindex
も変えられてしまったので次の要素はindex 1
つまり'Lemon'
からです。
Return Boolean: Array.prototype.filter()
filter()
:設置した条件にtrue
を答えた要素だけ取り出す。
(新しい配列を返す。)
(empty slotをスキップする。)
// arr.filter(function(item, index, array), thisArg)
let users = [
{ id: 1, name: 'Jon', score: 90 },
{ id: 2, name: 'Robin', score: 75 },
{ id: 3, name: 'Sansa', score: 80 },
{ id: 4, name: 'Arya', score: 100 },
{ id: 5, name: 'Bran', score: 120 },
{ id: 6, name: '', score: 1000 }
];
let over100 = users.filter((user) => user.score >= 100);
console.log(over100)
// [
// { id: 4, name: 'Arya', score: 100 },
// { id: 5, name: 'Bran', score: 120 },
// { id: 6, name: '', score: 1000 }
// ]
// filter() returns elements that condition is true
let nameOfOver100 = users.filter((user) => {
if (user.score >= 100) return user.name
});
console.log(nameOfOver100)
// [
// { id: 4, name: 'Arya', score: 100 },
// { id: 5, name: 'Bran', score: 120 }
// ]
filter()
は濾過器です。多重の条件をコールバックに設置してtrue
を返す要素だけが新しい配列に現れる。
検索にもよく使われています。
function findNameHasA(arr, query) {
return arr.filter((item) => {
return item.name.toLowerCase().indexOf(query.toLowerCase()) !== -1
})
}
console.log(findNameHasA(users, 'a'));
// [
// { id: 3, name: 'Sansa', score: 80 },
// { id: 4, name: 'Arya', score: 100 },
// { id: 5, name: 'Bran', score: 120 }
// ]
(気になるところのメモ)
配列が変更される(要素の変更/追加/削除)時の動きについてどう解釈するかは分からない。イテレータ中の配列の変更は元の配列にも影響するが、追加された要素がイテレータされない。
Transform Array: Array.prototype.map()
map()
:コールバックで要素を処理し、新しい配列に返す。
(新しい配列を返す。)
(empty slotをスキップして処理しないが、empty slotを保留する。)
forEach()
は要素を走査するが何も返しません。
map()
は処理された要素を配列として返してくれます。
// arr.map(function(item, index, array), thisArg)
let users = [
{ id: 1, name: 'Jon', score: 90 },
{ id: 2, name: 'Robin', score: 75 },
{ id: 3, name: 'Sansa', score: 80 },
{ id: 4, name: 'Arya', score: 100 },
{ id: 5, name: 'Bran', score: 120 },
{ id: 6, name: '', score: 1000 }
];
let nameLengths = users.map((user) => user.name.length);
console.log(nameLengths) // [ 3, 5, 5, 4, 4, 0 ]
// compare with forEach
let arr = [];
users.forEach((user) => {
arr.push(user.name.length);
});
console.log(arr); // [ 3, 5, 5, 4, 4, 0 ]
call()
で反復可能な文字列を指定することができます。もちろんforEach
も同じことできますが少し手間がかかります。
let arr = ['1', '2', '3'];
console.log(arr.map(Number)); // [ 1, 2, 3 ]
// string is iterable
let map = Array.prototype.map
let getCodePoint = map.call('I am string', (item) => item.codePointAt(0))
console.log(getCodePoint);
// [
// 73, 32, 97, 109,
// 32, 115, 116, 114,
// 105, 110, 103
// ]
// compare with forEach
let arr = []
let foreach = Array.prototype.forEach
foreach.call('I am string', (item) => arr.push(item.codePointAt(0)));
console.log(arr)
// [
// 73, 32, 97, 109,
// 32, 115, 116, 114,
// 105, 110, 103
// ]
map()
とforEach()
はcall()
を使って呼び出すのは文字列と配列だけです。(配列ならcall
を使う理由はないけど。)反復可能なオブジェクトならfor...of
で処理します。
map()
はよく使われる汎用性の高いメソッドですが、配列を返さないまたは返した配列を使用しないならほかのメソッドを使ったほうがよさそうです。
// compare with filter
let over100 = users.filter((user) => user.score >= 100);
console.log(over100)
// let mappedOver100 = users.map((user) => {
// if (user.score >= 100) return user
// return
// });
// console.log(mappedOver100);
// // [
// // undefined,
// // undefined,
// // undefined,
// // { id: 4, name: 'Arya', score: 100 },
// // { id: 5, name: 'Bran', score: 120 },
// // { id: 6, name: '', score: 1000 }
// // ]
// in this case, using filter() or forEach() is better
let mappedOver100 = []
users.map((user) => {
if (user.score >= 100) {
mappedOver100.push(user)
}
});
console.log(mappedOver100);
// [
// { id: 4, name: 'Arya', score: 100 },
// { id: 5, name: 'Bran', score: 120 },
// { id: 6, name: '', score: 1000 }
// ]
Calculate and Combine: Array.prototype.reduce()
reduce()
:全要素を結合させるメソッド。
(結合した値を返す。)
(empty slotをスキップする。)
// arr.reduce(function (previousValue, currentValue, index, array), initialValue);
let arr = [1, 2, 3, 4, 5];
// if initialValue is not specified, previousValue will be arr[0]
let result = arr.reduce((previousValue, current) => {
return (previousValue + current);
})
console.log(result); // 15
result = arr.reduce((a, b) => (a + b), 100);
console.log(result); // 115
arr = []
// result = arr.reduce((a, b) => (a + b));
// console.log(result);
// // TypeError: Reduce of empty array with no initial value
result = arr.reduce((a, b) => (a + b), 0);
console.log(result); // 0
初期値がある場合はpreviousValue
は初期値。
ない場合はpreviousValue
は配列の最初の要素。
文字列の要素も結合することできます。
let arr = ['a', 'b', 'c'];
let result = arr.reduce((a, b) => (a + b));
console.log(result); // abc
arr = [0, 'a', 1, 'b', 2, 3];
result = arr.reduce((a, b) => (a + b));
console.log(result); // 0a1b23
arr = [0, 1, 2, 'a', 3, 'b'];
result = arr.reduce((a, b) => (a + b));
console.log(result); // 3a3b
しかし数値と文字列が混在しているなら勝手に型変換されたり、予期せぬ結果が返してくるかもしれません。
filter()
でいったんろ過して必要な要素だけ結合させましょう。
result = arr.filter((item) => typeof item === 'number').reduce((a, b) => (a + b));
console.log(result); // 6
Array.prototype.reduceRight()
reduceRight()
:全要素を結合させるメソッド。右(最後の要素)から左に実行する。
(結合した値を返す。)
let arr = ['a', 'b', 'c'];
let result = arr.reduceRight((a, b) => (a + b));
console.log(result); // cba
Sort: Array.prototype.sort()
sort()
:要素をソートするメソッド。
(元の配列を変更する。ソートされた元の配列を返す。)
// arr.sort(function compare(a, b))
let arr = [1, 2, 15];
console.log(arr.sort()); // [ 1, 15, 2 ]
// default function in sort() will change elements into string
console.log(arr.sort(function compare(a, b) {
if (a > b) return 1;
if (a < b) return -1;
if (a === b) return 0;
})); // [ 1, 2, 15 ]
色んなタイプの要素が混在しているならfilter()
からsort()
。
let arr = ['1', 2, 'a', -1, 5, undefined];
let sortedArr = arr.filter((item) => {
return item && typeof item === 'number'
}).sort((a, b) => a - b);
console.log(sortedArr); // [ -1, 2, 5 ]
Number: function compare(a, b)
配列オブジェクトでもプロパティから値を取得してソートする。
let users = [
{ id: 1, name: 'Jon', score: 90 },
{ id: 2, name: 'Robin', score: 75 },
{ id: 3, name: 'Sansa', score: 80 },
{ id: 4, name: 'Arya', score: 100 },
{ id: 5, name: 'Bran', score: 120 },
{ id: 6, name: '', score: 1000 }
];
// let sortByScore = users.sort((a, b) => {
// if (a.score > b.score) return 1
// if (a.score < b.score) return -1
// return 0
// });
// console.log(sortByScore);
let sortByScore = users.sort((a, b) => a.score - b.score);
// note: a.score - b.score > 0 => ascending order
// note: b.score - a.score > 0 => descending order
console.log(sortByScore);
// [
// { id: 2, name: 'Robin', score: 75 },
// { id: 3, name: 'Sansa', score: 80 },
// { id: 1, name: 'Jon', score: 90 },
// { id: 4, name: 'Arya', score: 100 },
// { id: 5, name: 'Bran', score: 120 },
// { id: 6, name: '', score: 1000 }
// ]
String: String.prototype.localeCompare()
文字列のソートならstr.localeCompare()
を使いましょう。
// use str.localeCompare()
let alphabet = ['Banana', 'Lemon', 'apple'];
console.log(alphabet.sort((a, b) => {
console.log(`${a}: ${a.codePointAt(0)}, ${b}: ${b.codePointAt(0)}`);
// Lemon: 76, Banana: 66
// apple: 97, Lemon: 76
// apple: 97, Lemon: 76
// apple: 97, Banana: 66
// note: quickSort
return a.localeCompare(b)
}));
// [ 'apple', 'Banana', 'Lemon' ]
let japanese = ['あ', 'た', 'さ', 'ア', 'タ', 'サ'];
console.log(japanese.sort((a, b) => a.localeCompare(b)));
// [ 'あ', 'ア', 'さ', 'サ', 'た', 'タ' ]
reverse: Array.prototype.reverse()
reverse()
:要素の順序を逆転させる。反転後の配列を返す。
(元の配列を変更する。)
// arr.reverse()
let arr = ['a', 'b', 'c'];
console.log(arr.reverse()); // [ 'c', 'b', 'a' ]
Array ⇔ String
Array.prototype.join()
join()
:要素を結合して文字列として返す。
(元の配列を変更しない。)
(join()
はempty slotをundefined
と見なす。empty slot、undefined
、null
は''
を返す。)
// arr.join(separator)
let arr = ['a', 'b', 'c'];
console.log(arr.join()); // a,b,c
console.log(arr.join('')); // abc
console.log(arr.join('.')); // a.b.c
arr = ['', null, undefined, , 'a'];
console.log(arr.join('.')); // ....a
String.prototype.split()
split()
:指定の区切り文字列で分割し、文字列の配列を返す。
// str.split([separator[, limit]])
let fruits = 'Apple, Banana, Lemon';
console.log(fruits.split(',')); // [ 'Apple', ' Banana', ' Lemon' ]
console.log(fruits.split(', ')); // [ 'Apple', 'Banana', 'Lemon' ]
// note: Apple/, /Banana/, /Lemon
console.log(fruits.split(', ', 1)); // [ 'Apple' ]
サロゲートペアは分割されてしまうので、スプレット構文やArray.from
などサロゲートペアを識別できるメソッドを使いましょう。
// split() can break surrogate pairs
let str = '𝟘𝟙𝟚𝟛';
let arr = str.split('');
console.log(arr);
// [
// '\ud835', '\udfd8',
// '\ud835', '\udfd9',
// '\ud835', '\udfda',
// '\ud835', '\udfdb'
// ]
// use spread syntax or Array.from()
arr = [...str];
console.log(arr);
// ['𝟘', '𝟙', '𝟚', '𝟛']
arr = Array.from(str);
console.log(arr);
// ['𝟘', '𝟙', '𝟚', '𝟛']
String(CodePoint), Iterable object, Array-like, Array ⇒ Array
Array.from()
Array.from()
:要素を浅いコピーして新しい配列インスタンスを生成する。
(新しい配列を返す。)
Array.from()
は文字列、反復可能なオブジェクト、配列風オブジェクトを配列にして返してくれます。第2引数はmap()
メソッドなのでそのまま配列操作や変形していい、非常に強力で便利なメソッドです。(おすすめ!)
// Array.from(arg[, map[, thisArg]])
// String
let str = 'abc';
let result = Array.from(str, (item) => item.codePointAt().toString(16));
console.log(result); // [ '61', '62', '63' ]
str = '𝟘𝟙𝟚𝟛';
result = Array.from(str, (item) => item.codePointAt().toString(16));
console.log(result); // [ '1d7d8', '1d7d9', '1d7da', '1d7db' ]
// Iterable object
// Map([[key1, value1], [key2, value2]])
const map = new Map([[1, 2], [2, 4]])
console.log(map);
// Map(2) { 1 => 2, 2 => 4 }
map.set('a', 10); // .set(key, value)
console.log(map);
// Map(3) { 1 => 2, 2 => 4, 'a' => 10 }
console.log(Array.from(map));
// [ [ 1, 2 ], [ 2, 4 ], [ 'a', 10 ] ]
// Array-like
function addString() {
// console.log(arguments);
// // [Arguments] { '0': 1, '1': 2, '2': 3 }
// console.log(Array.isArray(Array.from(arguments)));
// // true
return Array.from(arguments).join('')
}
console.log(addString('a', 'b', 'c')); // abc
console.log(addString(1, 2, 3)); // 123 // string
長さに合わせる配列の宣言ではnew Array(length)
で配列を作るとき、要素が指定できないempty slotになり要素を補填してから配列を操作するのが非常に手間がかかります。Array.from()
なら配列風のようにlength
を指定し、あとはmap()
で要素を詰めればいいのです。
// create array-like object => array
console.log(Array.from({ length: 5 }, (item, index) => index));
// [ 0, 1, 2, 3, 4 ]
const createRange = (start, end, step) => {
return Array.from({ length: (end - start) / step + 1 }, (item, index) => {
return start + (index * step);
})
};
console.log(createRange(0, 4, 1))
// [ 0, 1, 2, 3, 4 ]
console.log(createRange(1, 10, 2))
// [ 1, 3, 5, 7, 9 ]
Index & Includes
Array.prototype.indexOf() & Array.prototype.lastIndexOf()
indexOf()
:指定の要素を探す。見つかったらインデックスを返す。
lastIndexOf()
:最後のインデックスから逆行して探す。
arr.indexOf(searchElement[, fromIndex])
arr.lastIndexOf(searchElement[, fromIndex])
// true => return index, false => return -1
スタートのインデックスを指定しない場合は一番に見つかった要素のインデックスを返します。
// index
let arr = [1, 'a', 2, 'b', 3, 'a'];
console.log(arr.length); // 6
console.log(arr.indexOf('a')); // 1
console.log(arr.lastIndexOf('a')); // 5
console.log(arr.indexOf('a', Math.floor(arr.length / 2))); // 5
console.log(arr.lastIndexOf('a', Math.floor(arr.length / 2))); // 1
Array.prototype.find() & Array.prototype.findIndex()
find()
:条件に当てはまる要素を探す。見つかったら最初の要素を返す。
findIndex()
:条件に当てはまる要素を探す。見つかったら最初のインデックスを返す。
arr.find(function(item, index, array), thisArg)
// true => return item, false return undefined
arr.findIndex(function(item, index, array), thisArg)
// true => return index, false return undefined
find()
とfindIndex()
はコールバック関数で設置した条件に合わせて要素を探すが、一つの結果しか返しません。filter()
なら見つかったすべての要素を配列にして返すが。
// arr.find(function(item, index, array), thisArg)
let arr = [1, 'a', 2, 'b', 3, 'a'];
console.log(arr.find((item, index, array) => {
if (index > Math.floor(array.length / 2) && item === 'a') {
// return index; // it won't work
return item; // find() always return item
}
}));
// a
console.log(arr.find((item, index, array) => {
return index > Math.floor(array.length / 2) && item === 'a'
}));
// a
// arr.findIndex(function(item, index, array), thisArg)
let arr = [1, 'a', 2, 'b', 3, 'a'];
console.log(arr.findIndex((item, index, array) => {
return index > Math.floor(array.length / 2) && item === 'a'
}));
// 5
Array.prototype.some() & Array.prototype.every() & Array.prototype.includes()
some
:配列に少なくとも一つの要素が条件に当てはまるとtrue
、でなければfalse
。
every
:配列にすべての要素が条件に当てはまるとtrue
、でなければfalse
。
includes()
:指定の要素の存在を確認する。あればtrue
、なければfalse
。
arr.some(function(item, index, array), thisArg)
arr.every(function(item, index, array), thisArg)
// return true/false
arr.includes(searchElement[, fromIndex])
// return true/false
// arr.some(function(item, index, array), thisArg)
let arr = [1, 'a', 2, 'b', 3, 'a'];
console.log(arr.some((item, index) => item === 'a' && index === 5)) // true
// arr.every(function(item, index, array), thisArg)
let arr = [1, 'a', 2, 'b', 3, 'a'];
console.log(arr.every((item) => typeof item === 'string')); // false
arr = ['a', 'b', 'c'];
console.log(arr.every((item) => typeof item === 'string')); // true
// includes()
let arr = ['a', 'a', 1, 2, 3];
console.log(arr.includes('a', Math.floor(arr.length / 2))); // false
Compare array elements
浅いコピーでなければ配列は別々のメモリー位置に保存されていますので配列の要素の比較は難しいです。every()
は全要素の比べに使えます。
// every()
function arrayElementsEqual(arr1, arr2) {
return arr1.length === arr2.length
&& arr1.every((item, index) => item === arr2[index])
}
console.log(arrayElementsEqual([1, 'a'], [1, 'a'])); // true
Access Element
Array.prototype.at()
ES2022の新しいメソッドです。
at()
:指定のインデックスの要素をアクセスする。
// arr.at(index)
let arr = ['a', 'a', 1, 2, 3];
console.log(arr.at()); // a
console.log(arr.at(0)); // a
console.log(arr.at(-1)); // 3
console.log(arr[arr.length - 1]); // 3
Flat multidimensional array
Array.prototype.flat()
flat()
:多次元配列を指定の深さまで平坦化する。
// arr.flat([depth])
let arr = [0, [1, 2], [[3, 4]], [[[5], 6]]];
console.log(arr.flat());
// [ 0, 1, 2, [ 3, 4 ], [ [ 5 ], 6 ] ]
console.log(arr.flat(1));
// [ 0, 1, 2, [ 3, 4 ], [ [ 5 ], 6 ] ]
console.log(arr.flat(2));
// [
// 0, 1, 2,
// 3, 4, [5],
// 6
// ]
console.log(arr.flat(3));
// [
// 0, 1, 2, 3,
// 4, 5, 6
// ]
でも深さはいちいち手動で指定するのが面倒なので、関数を書いてみました。
let arr = [0, [1, 2], [[3, 4]], [[[5], 6]]];
function flatArr(arr) {
let depth = 0
let type = Object.prototype.toString
arr.forEach((item) => type.call(item).includes('Array') ? depth++ : null)
return arr.flat(depth);
}
console.log(flatArr(arr));
// [
// 0, 1, 2, 3,
// 4, 5, 6
// ]
Array.prototype.flatMap()
flatMap()
:map()
の後flat(1)
するのと同じ効果です。
// arr.flatMap(function(currentValue, index, array), thisArg)
let arr = [1, 2, 3, 4];
let arrSqrt = arr.map((item) => [item ** 2]);
console.log(arrSqrt);
// [ [ 1 ], [ 4 ], [ 9 ], [ 16 ] ]
console.log(arrSqrt.flat(1));
// [ 1, 4, 9, 16 ]
console.log(arr.flatMap((item) => [item ** 2]));
// [ 1, 4, 9, 16 ]
console.log(arr.flatMap((item) => [[item * 10]]))
// [ [ 10 ], [ 20 ], [ 30 ], [ 40 ] ]
Undefined vs. Empty slots
Identify: Object.hasOwn()
hasOwn()
:指定されたオブジェクトに、指定のプロパティが自身のプロパティである場合はtrue
、ではない場合はfalse
。
// Object.hasOwn(arr, index)
let arr = [1, undefined, 'a'];
console.log(Object.hasOwn(arr, 1)); // true
arr = [1, , 'a'];
console.log(Object.hasOwn(arr, 1)); // false
Skip empty slot
forEach()
、filter()
、reduce()
、some()
、every()
。
// skip
let arrUndefined = [undefined, 'a'];
let arrEmptySlot = [, 'a'];
// forEach()
arrUndefined.forEach((item) => console.log(item));
// undefined
// a
arrEmptySlot.forEach((item) => console.log(item));
// a
// filter()
console.log(arrUndefined.filter((item) => item));
// [ 'a' ]
console.log(arrUndefined.filter((item) => item === undefined));
// [ undefined ]
console.log(arrEmptySlot.filter((item) => item));
// [ 'a' ]
// reduce()
console.log(arrUndefined.reduce((a, b) => a + b));
// undefineda
console.log(arrEmptySlot.reduce((a, b) => a + b));
// a
// some()
console.log(arrUndefined.some((item) => item !== 'a'));
// true
console.log(arrEmptySlot.some((item) => item !== 'a'));
// false
// every()
console.log(arrUndefined.every((item) => item === 'a'));
// false
console.log(arrEmptySlot.every((item) => item === 'a'));
// true
Keep/Iterate/Copy empty slot
map()
:map()
はempty slotを保留するけどイテレータしない。
Array.from()
、Spread Syntax
、fill()
:empty slotをundefined
に転換するかイテレータする。
copyWithin()
:empty slotでもコピーする。
let arrUndefined = [undefined, 'a'];
let arrEmptySlot = [, 'a'];
// Keep
// map()
console.log(arrUndefined.map((item) => item));
// [ undefined, 'a' ]
console.log(arrEmptySlot.map((item) => item));
// [ <1 empty item>, 'a' ]
console.log(arrUndefined.map((item) => 'abc'));
// [ 'abc', 'abc' ]
console.log(arrEmptySlot.map((item) => 'abc'));
// [ <1 empty item>, 'abc' ]
// Iterate
// Array.from()
console.log(Array.from(arrUndefined));
// [ undefined, 'a' ]
console.log(Array.from(arrEmptySlot));
// [ undefined, 'a' ]
// Spread Syntax
console.log([...arrUndefined]);
// [ undefined, 'a' ]
console.log([...arrEmptySlot]);
// [ undefined, 'a' ]
// fill()
console.log(arrUndefined.fill('abc'));
// [ 'abc', 'abc' ]
console.log(arrEmptySlot.fill('abc'));
// [ 'abc', 'abc' ]
// Copy
// arr.copyWithin(targetIndex[, startIndex[, endIndex]]) // not including end
// console.log([0, 1, 2, 3].copyWithin(0, 3)); // [ 3, 1, 2, 3 ]
// console.log([0, 1, 2, 3].copyWithin(0, 1, 3)); // 1, 2, 2, 3 ]
console.log(arrUndefined.copyWithin(1, 0, 1));
// [ undefined, undefined ]
console.log(arrEmptySlot.copyWithin(1, 0, 1));
// [ <2 empty items> ]
They are empty in String
join()
、toString()
。
// They are empty in String
let arr = ['', undefined, null, , 'a'];
console.log(arr.join('.')); // ....a
console.log(arr.toString()); // ....a
Find empty slot index
indexOf()
、lastIndexOf()
:empty slotをスキップする。引数に何も入れてない場合はundefined
を見つける。
// indexOf()/lastIndexOf()
let arr = ['', undefined, null, , 'a'];
console.log(arr.indexOf(null)); // 2
console.log(arr.lastIndexOf(undefined)); // 1
console.log(arr.indexOf('')); // 0
console.log(arr.lastIndexOf('')); // 0
console.log(arr.indexOf()); // 1
console.log(arr.lastIndexOf()); // 1
// note: indexOf() and lastIndexOf() can not find out empty slot
// note: they treat undefined as empty
find()
、findIndexOf()
:empty slotをundefined
と見なす。
// find()
let arr = ['', undefined, null, , 'a'];
console.log(arr.find((item) => item)); // a
console.log(arr.find((item) => item === null)); // null
console.log(arr.find((item) => item === undefined)); // undefined
console.log(arr.find((item) => item === '')); // ''
arr = [1, , 'a']
console.log(arr.find((item) => true)); // 1
arr = [, 'a']
console.log(arr.find((item) => true)); // undefined
arr = [, 'a']
console.log(arr.find((item) => item === undefined)); // undefined
// note: find() treats empty slot as undefined, a real value
// findIndexOf()
let arr = ['', undefined, null, , 'a'];
console.log(arr.findIndex((item) => item)); // 4
console.log(arr.findIndex((item) => item === null)); // 2
console.log(arr.findIndex((item) => item === undefined)); // 1
console.log(arr.findIndex((item) => item === '')); // 0
arr = [, 'a']
console.log(arr.findIndex((item) => item === undefined)); // 0
arr = ['a', undefined]
console.log(arr.findIndex((item) => item === undefined)); // 1
// note: findIndexOf() also treats empty slot as undefined
includes()
:引数に何も入れてない場合はempty slotとundefined
を見つける。
// includes()
let arr = ['', undefined, null, , 'a'];
console.log(arr.includes(null)); // true
console.log(arr.includes(undefined)); // true
console.log(arr.includes('')); // true
arr = [, 'a'];
console.log(arr.includes()); // true
arr = [undefined, 'a']
console.log(arr.includes()); // true
arr = [null]
console.log(arr.includes()); // false
// note: includes() treats empty slot and undefined as empty, but null is null
at()
:アクセスして取得した値empty slotはundefined
と見なす。
// at()
let arr = ['', undefined, null, , 'a'];
console.log(arr.at(0)); // ''
console.log(arr.at(1)); // undefined
console.log(arr.at(2)); // null
console.log(arr.at(3)); // undefined
console.log(arr.at(3) === undefined); // true
NaN
NaN
ならincludes()
だけ識別できます。
// only includes() can find NaN
let arr = [NaN];
console.log(arr.indexOf(NaN)); // -1
console.log(arr.lastIndexOf(NaN)); // -1
console.log(arr.find((item) => item === NaN)); // undefined
console.log(arr.findIndex((item) => item === NaN)); // -1
console.log(arr.includes(NaN)); // true
Practices
// practices
// shuffle an arr
// function shuffle(arr) {
// arr.sort(() => Math.random() - 0.5);
// }
let count = {
'123': 0,
'132': 0,
'213': 0,
'231': 0,
'321': 0,
'312': 0
};
for (let i = 0; i < 1000000; i++) {
let arr = [1, 2, 3];
shuffle(arr)
count[arr.join('')]++;
}
for (let key in count) {
console.log(`${key}: ${count[key]}`);
}
// 123: 374675
// 132: 62596
// 213: 125113
// 231: 62539
// 312: 62586
// 321: 312491
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
// 123: 166666
// 132: 166429
// 213: 167335
// 231: 166500
// 312: 166718
// 321: 166352
// filter unique array members
function unique(arr) {
let result = [];
for (let str of arr) {
// if result have no same name, push it
if (!result.includes(str)) {
result.push(str);
}
}
return result;
}
let strings = ['Hare', 'Krishna', 'Hare', 'Krishna',
'Krishna', 'Krishna', 'Hare', 'Hare', ':-O'
];
console.log(unique(strings));
// [ 'Hare', 'Krishna', ':-O' ]
// Be careful of performance problem
let result = [];
strings.forEach((item) => {
if (!result.includes(item)) {
result.push(item);
}
});
console.log(result);
// [ 'Hare', 'Krishna', ':-O' ]
// create keyed object from array
let users = [
{ id: 'Jon', name: 'Jon Snow', score: 90 },
{ id: 'Robin', name: 'Robin Stark', score: 75 },
{ id: 'Sansa', name: 'Sansa Stark', score: 80 },
{ id: 'Arya', name: 'Arya Stark', score: 100 },
{ id: 'Bran', name: 'Bran Stark', score: 120 },
{ id: '', name: '', score: 1000 }
];
let usersById = groupByID(users);
function groupByID(arr) {
return arr.reduce((obj, currentItem) => {
obj[currentItem.id] = currentItem;
return obj;
}, {})
}
console.log(usersById);
// {
// Jon: { id: 'Jon', name: 'Jon Snow', score: 90 },
// Robin: { id: 'Robin', name: 'Robin Stark', score: 75 },
// Sansa: { id: 'Sansa', name: 'Sansa Stark', score: 80 },
// Arya: { id: 'Arya', name: 'Arya Stark', score: 100 },
// Bran: { id: 'Bran', name: 'Bran Stark', score: 120 },
// '': { id: '', name: '', score: 1000 }
// }