0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのArrayについて

Last updated at Posted at 2022-09-23

初めに

今回は配列に関連するメソッドと、配列に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 syntaxArray.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'

上の例では、thisArgfilter()のコールバックconfig.matchthisの参照先を設置しないとエラーになります。
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.matchthisの参照がエラーにならないのです。

// 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はコールバックで全要素を走査する。自体は何も返さないので常にundefinedreturnの値を指定しない場合のデフォルト)です。

// 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)の返り値がスキップされ、sum0のままでした。

もし元の配列が走査中に変更されたら、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 slotundefinedと見なす。empty slotundefinednull''を返す。)

// 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 Syntaxfill()empty slotundefinedに転換するかイテレータする。
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 slotundefinedと見なす。

// 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 slotundefinedを見つける。

// 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 slotundefinedと見なす。

// 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 }
// }
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?