配列は多くのメソッドを提供します。分かりやすくするために、このチャプターではグループに分けて説明します。

アイテムの追加/削除

私たちは既に先頭または末尾にアイテムを追加/削除するメソッドを知っています:

  • push(...items)items を末尾に追加します。
  • pop() は末尾の要素を削除し、それを返します。
  • shift() は先頭の要素を削除し、それを返します。
  • unshift(...items) はアイテムを先頭に追加します。

他にもいくつかあります。

splice

どうやって配列から要素を削除するのでしょうか?

配列はオブジェクトなので、 delete を試すことができます:

let arr = ["I", "go", "home"];

delete arr[1]; // "go" を削除

alert( arr[1] ); // undefined

// now arr = ["I",  , "home"];
alert( arr.length ); // 3

要素は削除されましたが、配列はまだ3つの要素を持っており、arr.length == 3 となります。

これは自然なことです。なぜなら、 delete obj.keykey で値を削除するためです。それがすべてであり、オブジェクトでは問題ありません。しかし、配列では通常残りの要素が移動し、解放された場所を埋めたいです。今より短い配列になることを期待しています。

なので、特別なメソッドを使用する必要があります。

arr.splice(str) メソッドは、配列のためのスイス製アーミーナイフです。それは何でもすることが出来ます: 追加、削除、また要素の挿入も。

構文:

arr.splice(index[, deleteCount, elem1, ..., elemN])

位置 index から始まります: deleteCount の要素を削除した後、その場所に elem1, ..., elemN を挿入します。削除した要素の配列を返します。

このメソッドは例で簡単に把握できます。

削除してみましょう:

let arr = ["I", "study", "JavaScript"];

arr.splice(1, 1); // インデックス 1 から 1 要素を削除

alert( arr ); // ["I", "JavaScript"]

簡単ですよね?インデックス 1 から始まり、1 つ要素を削除します。

次の例では、3つの要素を削除し、他の2つの要素でそれらを置き換えます:

let arr = ["I", "study", "JavaScript", "right", "now"];

// 最初の 3 要素を削除し、別のものに置換
arr.splice(0, 3, "Let's", "dance");

alert( arr ) // now ["Let's", "dance", "right", "now"]

ここで、splice が削除された要素の配列を返していることを見ることができます:

let arr = ["I", "study", "JavaScript", "right", "now"];

// 最初の 2 要素を削除
let removed = arr.splice(0, 2);

alert( removed ); // "I", "study" <-- 削除された要素の配列

splice メソッドは削除せずに挿入することも可能です。そのために、deleteCount0 をセットする必要があります:

let arr = ["I", "study", "JavaScript"];

// インデックス 2 から
// 削除 0
// その後 "complex" と "language" を挿入
arr.splice(2, 0, "complex", "language");

alert( arr ); // "I", "study", "complex", "language", "JavaScript"
負のインデックスも許容されます

上記や他の配列のメソッドでは、負のインデックスが許容されます。それらは配列の末尾からの位置を指定します。:

let arr = [1, 2, 5];

// インデックス -1 (末尾から1つ前) から
// 削除 0 要素,
// その後 3 と 4 を挿入
arr.splice(-1, 0, 3, 4);

alert( arr ); // 1,2,3,4,5

slice

メソッド arr.slice は似たように見える arr.splice よりもはるかにシンプルです。

構文:

arr.slice(start, end)

開始インデックス "start" から "end" ("end" は含みません)の全てのアイテムをコピーした新しい配列を返します。startend はともに負値になることができます。そのときは、配列の末尾からの位置が想定されます。

str.slice のように動作しますが、部分文字列の代わりに部分配列を作ります。

例:

let str = "test";
let arr = ["t", "e", "s", "t"];

alert( str.slice(1, 3) ); // es
alert( arr.slice(1, 3) ); // e,s

alert( str.slice(-2) ); // st
alert( arr.slice(-2) ); // s,t

concat

メソッド arr.concat は配列を他の配列またはアイテムと結合します。

構文:

arr.concat(arg1, arg2...)

任意の数の引数を許容します – 配列または値。

結果は arr, 次に arg1, arg2 などのアイテムを含む新しい配列を返します。

もし、引数が配列、もしくは Symbol.isConcatSpreadable プロパティを持っている場合、その全ての要素がコピーされます。そうでない場合、引数自体がコピーされます。

例:

let arr = [1, 2];

// arr と [3,4] をマージ
alert( arr.concat([3, 4])); // 1,2,3,4

// arr と [3,4] と [5,6] をマージ
alert( arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6

// arr と [3,4] をマージ後, 5 と 6 を追加
alert( arr.concat([3, 4], 5, 6)); // 1,2,3,4,5,6

通常は、配列から要素をコピーするだけです。それ以外のオブジェクトでは、配列のように見えたとしても、全体として追加されます:

let arr = [1, 2];

let arrayLike = {
  0: "something",
  length: 1
};

alert( arr.concat(arrayLike) ); // 1,2,[object Object]
//[1, 2, arrayLike]

…しかし、もし配列のようなオブジェクトが Symbol.isConcatSpreadable プロパティを持つ場合、代わりにその要素が追加されます:

let arr = [1, 2];

let arrayLike = {
  0: "something",
  1: "else",
  [Symbol.isConcatSpreadable]: true,
  length: 2
};

alert( arr.concat(arrayLike) ); // 1,2,something,else

配列での検索

これらは、配列で何かを探すためのメソッドです。

indexOf/lastIndexOf and includes

メソッド arr.indexOf, arr.lastIndexOfarr.includes は文字列の場合と同じ構文を持ち、基本的に同じことを行いますが、文字の代わりにアイテムを操作します:

  • arr.indexOf(item, from) はインデックス from から item を探し、見つかった場所のインデックスを返します。そうでない場合は -1 になります。
  • arr.lastIndexOf(item, from) – 同じですが、右から左に見ていきます。
  • arr.includes(item, from) – はインデックス from から item を探し、見つかった場合、true を返します。

例:

let arr = [1, 0, false];

alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1

alert( arr.includes(1) ); // true

メソッドは === 比較を使うことに留意してください。従って、もしも false を探す場合、ゼロではなく、正確な false を見つけようとします。

もしも含んでいるかをチェックしたいが、正確なインデックスは不要なときは、arr.include が好ましいです。

また、include の非常に小さな違いは、indexOf/lastIndexOf と違い、NaN を正しく処理することができます:

const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (0 になるべきですが, === 等値は NaN では機能しません)
alert( arr.includes(NaN) );// true (正しい)

find と findIndex

オブジェクトの配列を持っていることを想像してください。特定条件を持つオブジェクトをどのようにして見つけますか?

ここで arr.find メソッドが便利です。

構文はこうです:

let result = arr.find(function(item, index, array) {
  // item が探しているものであれば true を返すようにします
});

関数は配列の要素毎に繰り返し呼ばれます:

  • item は要素です。
  • index はインデックスです。
  • array は配列自身です。

もし true を返すと、検索が止まり、item が返却されます。見つからない場合は undefined になります。

例えば、ユーザの配列を持っており、それぞれフィールド idname を持っているとします。id == 1 のものを見つけましょう:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

let user = users.find(item => item.id == 1);

alert(user.name); // John

現実の世界では、オブジェクトの配列は一般的なことです。なので、 find メソッドは非常に役立ちます。

例の中では1つの引数、関数 item => item.id == 1find を行っている点に注意してください。find の他のパラメータは殆ど使われません。

arr.findIndex メソッドは基本的に同じです。が、要素自体ではなく要素が見つかったインデックスを返します。

filter

find メソッドは、関数が true を返すようにする単一の(最初の)要素を探します。 もしそれが多い場合、arr.filter(fn) を使います。

構文は大体 find と同じですが、マッチした要素の配列を返します:

let results = arr.filter(function(item, index, array) {
  // item がフィルタを通過する場合はtrueを返します
});

例:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

// 最初の2人のユーザの配列を返します
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

配列を変換する

このセクションでは、配列を変換または並び替える方法について説明します。

map

arr.map メソッドは最も便利なものの1つで、よく使われます。

構文:

let result = arr.map(function(item, index, array) {
  // item の代わりに新しい値を返します
})

これは、配列の各要素で関数を呼び出し、結果の配列を返します。

例えば、ここでは各要素をその長さに変換します:

let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length)
alert(lengths); // 5,7,6

sort(fn)

メソッド arr.sort は配列を 決まった位置に ソートします。

例:

let arr = [ 1, 2, 15 ];

// このメソッドは arr の内容を並べ替え(てそれを返します)
arr.sort();

alert( arr );  // 1, 15, 2

出力結果が何か不自然であることに気づきましたか?

並び順が 1, 15, 2 となりました。正しくないようです。しかしなぜでしょう?

アイテムは、デフォルトでは文字列としてソートされます。

文字通り、すべての要素は文字列に変換され、比較されます。なので、辞書編集順序が適用され、実際には "2" > "15" となります。

私たち自身のソート順を使うためには、arr.sort() の引数として、2つの引数をもつ関数を提供する必要があります。

関数ははこのように動作する必要があります:

function compare(a, b) {
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

例:

function compareNumeric(a, b) {
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

let arr = [ 1, 2, 15 ];

arr.sort(compareNumeric);

alert(arr);  // 1, 2, 15

これで意図したとおりに動作します。

立ち止まって何が起きているのか考えてみましょう。arr は何でも配列にすることができますよね。それは数値や文字列、html要素やその他何でも含まれる可能性があります。私たちは 何かの セットを持っています。それらをソートするためには、要素を比較する方法を知っている 順序付け関数 が必要です。 デフォルトは文字列です。

arr.sort(fn) メソッドは組み込みでソートアルゴリズムの実装を持っています。私たちはそれが正確にどのように動作するかについて気にする必要はありません (殆どの場合、最適化されたクイックソート です)。配列を歩き、提供された関数を使ってその要素を比較し、並べ替えます。私たちに必要なのは、比較を行う fn を提供することだけです。

ところで、もしどの要素が比較されているかを知りたいとき – アラートをしても問題ありません:

[1, -2, 15, 2, 0, 8].sort(function(a, b) {
  alert( a + " <> " + b );
});

アルゴリズムは処理の中で複数回要素を比較しますが、できるだけ回数を少なくしようとします。

比較関数は任意の数を返すことがあります

実際には、比較関数は正の数を「より大きい」、負の数を「より小さい」として返せば十分です。

より短い関数で書くことができます:

let arr = [ 1, 2, 15 ];

arr.sort(function(a, b) { return a - b; });

alert(arr);  // 1, 2, 15
ベストなアロー関数

記事 "function-expression" が見つかりません を覚えていますか? すっきりしたソートを書くために使えます。:

arr.sort( (a, b) => a - b );

これは他の、上で書いているより長いバージョンと全く同じように動作します。

reverse

メソッド arr.reversearr 内の要素の順序を逆転させます。

例:

let arr = [1, 2, 3, 4, 5];
arr.reverse();

alert( arr ); // 5,4,3,2,1

また、反転後に配列 arr を返します。

split と join

ここでは現実の世界でのシチュエーションを考えます。私たちはメッセージングアプリを書いており、人々はカンマ区切りで受信者のリスト(John, Pete, Mary)を入力します。しかし、我々にとっては、1つの文字列よりも名前の配列の方がはるかに快適です。それを得る方法は?

str.split(delim) メソッドは、正確にそれをします。与えられた区切り文字 delim で文字列を配列に分割します。

下の例では、スペースに続くカンマで分割しています:

let names = 'Bilbo, Gandalf, Nazgul';

let arr = names.split(', ');

for (let name of arr) {
  alert( `A message to ${name}.` ); // A message to Bilbo  (と他の名前)
}

split メソッドは任意の2つ目の数値の引数を持っています – それは配列の長さの制限です。もしこれが指定された場合、余分な要素は無視されます。実際にはほとんど使われませんが。:

let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);

alert(arr); // Bilbo, Gandalf
文字への分割

空の s での split(s) の呼び出しは、文字列を文字の配列に分割します:

let str = "test";

alert( str.split('') ); // t,e,s,t

arr.join(str)split と逆を行います。arr の項目を str で繋いだ文字列を作ります。

例:

let arr = ['Bilbo', 'Gandalf', 'Nazgul'];

let str = arr.join(';');

alert( str ); // Bilbo;Gandalf;Nazgul

reduce/reduceRight

配列に対して繰り返し処理が必要なときは – forEach を使うことができます。

各要素のデータを反復して返す必要があるときには – mapを使うことができます。

メソッド arr.reducearr.reduceRight もまたその種類に属しますが、少し複雑です。それらは、配列に基づいて単一の値を計算するために使用されます。

構文:

let value = arr.reduce(function(previousValue, item, index, arr) {
  // ...
}, initial);

関数は各要素に適用されます。あなたはよく知られている引数に気づくかもしれません。2つ目から始まる引数は次の通りです:

  • item – 現在の配列の項目です。
  • index – その位置です。
  • arr – 配列です。

これまでのところ、forEach/map のようです。しかし、もう1つ引数があります:

  • previousValue – 前の関数の呼び出し結果です。最初の呼び出しは initial です。

これを掴むための最も簡単な方法は、例を見る、です。

ここでは、1行で配列の合計を取得します。

let arr = [1, 2, 3, 4, 5];

let result = arr.reduce((sum, current) => sum + current, 0);

alert(result); // 15

ここでは、2つの引数だけを使用する reduce の最も一般的なバリアントを使用しました。

何が起きているか、詳細を見てみましょう。

  1. 最初の実行で sum は initial 値(reduce の最後の引数)であり、 0 と等価です。そして、 current は最初の配列要素で 1 になります。従って、結果は 1 です。
  2. 2回目の実行では、sum = 1 で、2つ目の配列要素(2)をそれに足して返します。
  3. 3回目の実行では、sum = 3 で、それに1つ要素を足します。それが続きます。

計算のフロー:

また、次のテーブルでは、各行は次の配列要素の関数呼び出しを表しています。

sum current result
the first call 0 1 1
the second call 1 2 3
the third call 3 3 6
the fourth call 6 4 10
the fifth call 10 5 15

これらから分かるように、前の呼び出しの結果は次の実行のときの最初の引数になっています。

また、 initial 値を省略することもできます。:

let arr = [1, 2, 3, 4, 5];

// reduce からの初期値を削除する(0なし)
let result = arr.reduce((sum, current) => sum + current);

alert( result ); // 15

結果は同じです。なぜなら、初期値が指定されていない場合、reduce は配列の最初の要素を初期値とみなし、2つ目の要素から繰り返し処理を始めるためです。

計算テーブルは、上と同じで、最初の行を引いたものです。

しかし、このような利用は極度の注意を要求します。もし配列が空の場合、初期値なしでの reduce 呼び出しがエラーを返します。

例:

let arr = [];

// Error: 初期値なしの空配列の Reduce はエラーです
// 初期値が存在する場合、reduce は空の arr に対しそれを返します。
arr.reduce((sum, current) => sum + current);

従って、常に初期値を指定することをおすすめします。

arr.reduceRight メソッドも同じをことを行いますが、右から左に実行します。

反復: forEach

arr.forEach メソッドは配列の全要素に対して関数を実行することができます。

構文:

arr.forEach(function(item, index, array) {
  // ... item に対して何か処理をする
});

例えば、これは配列の各要素を表示します:

// 各要素は alert を呼び出す
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

そしてこのコードは、ターゲット配列内の位置についてより細かいです。

["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
  alert(`${item} is at index ${index} in ${array}`);
});

関数の結果(もし何かを返す場合)は捨てられ、無視されます。

Array.isArray

配列は別の言語タイプを形成しません。 それらはオブジェクトに基づいています。

なので typeof では、通常のオブジェクトと配列を区別するのには助けになりません:

alert(typeof {}); // object
alert(typeof []); // same

…しかし、配列は頻繁に使用されるため、そのための特別なメソッド Array.isArray(value) があります。これは、value が配列のときに true を、そうでない場合には false を返します。

alert(Array.isArray({})); // false

alert(Array.isArray([])); // true

ほとんどのメソッドは “thisArg” をサポートします

findfiltermap のような関数を呼び出すほとんどの配列メソッドは、sort の例外を除いて、オプションの追加パラメータ thisArg を受け取ります。

これは殆ど使われないため、このパラメータは上のセクションでは説明されていません。しかし、完全性のためにはそれをカバーする必要があります。

それらのメソッドの完全な構文です:

arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg はオプションの最後の引数です

thisArg パラメータの値は func での this になります。

例えば、フィルタとしてオブジェクトメソッドを使い、thisArg は便利です:

let user = {
  age: 18,
  younger(otherUser) {
    return otherUser.age < this.age;
  }
};

let users = [
  {age: 12},
  {age: 16},
  {age: 32}
];

// user より若いすべてのユーザを見つけます
let youngerUsers = users.filter(user.younger, user);

alert(youngerUsers.length); // 2

上の呼び出しでは、フィルタとして user.younger を使い、そのコンテキストとして user を提供しています。もしもコンテキストを提供しなかった場合、users.filter(user.younger) はスタンドアロン関数として this=undefineduser.younger を呼び出します。それは即時エラーを意味します。

サマリ

配列メソッドの チートシート です:

  • 要素の追加/削除をするため:

    • push(...items) – アイテムを末尾に追加します,
    • pop() – 末尾からアイテムを抽出します,
    • shift() – 先頭からアイテムを抽出します,
    • unshift(...items) – 先頭にアイテムを追加します.
    • splice(pos, deleteCount, ...items) – インデックス posdeleteCount 要素を削除し items を挿入します。
    • slice(start, end) – 新しい配列を作り、start から end まで(endは含まない) の要素をコピーします。
    • concat(...items) – 新しい配列を返します: 現在のものすべてをコピーし、items を追加します。items のいずれかが配列の場合、その要素が取得されます。
  • 要素を検索するため:

    • indexOf/lastIndexOf(item, pos) – 位置 pos から始めて item を探します。 インデックス、または見つからなかった場合は -1 を返します。
    • includes(value) – 配列が value を持っている場合 true を返します。そうでなければ false です。
    • find/filter(func) – 関数を介して要素をフィルタリングし、true を返す最初の/すべての値を返します。
    • findIndexfind のようですが、値の代わりにインデックスを返します。
  • 配列を変換するには:

    • map(func) – すべての要素に対して func を呼び出した結果から新しい配列を作成します。
    • sort(func) – 配列を適切な位置でソートし、それを返します。
    • reverse() – 配列を反転してそれを返します。
    • split/join – 文字列を配列に変換したり、戻します。
    • reduce(func, initial) – 各要素に対して funcを呼び出し、呼び出しの間に中間結果を渡すことで配列全体の単一の値を計算します。
  • 要素を反復処理するには:

    • forEach(func) – すべての要素に対して funcを呼び出し、何も返しません。
  • さらに:

    • Array.isArray(arr)arr が配列かどうかをチェックします。

sort, reversesplice メソッドは、配列自身を変更することに注意してください。

これらのメソッドは最も使われるもので、ユースケースの99%をカバーしますが、他にもいくつかあります:

  • arr.some(fn)/arr.every(fn) は配列をチェックします。

    関数 fnmap と同じように配列の各要素で呼ばれます。もし どれか/すべて の結果が true であれば true, それ以外は false になります。

  • arr.fill(value, start, end) – インデックス start から end まで value で配列を埋めます。

  • arr.copyWithin(target, start, end) – 位置 start から end までの要素を、自身target の位置にコピーします (既存のものを上書きします)。

完全なリストは manual を見てください。

初めてみたとき、多くのメソッドがあり覚えるのがとても難しいように見えるかもしれません。しかし、実際にはそう見えるよりもはるかに簡単です。

それらを意識して チートシート を見てください。次にこのチャプターを通してあなたは配列のメソッドを経験しました。

今後、配列で何かをする必要があるとき、どうやればいいか分からないときはいつでも – ここに来て チートシート を見て正しいメソッドを見つけてください。例はあなたが正しくそのメソッドを使うのを助けるでしょう。使っていると自然とそれらのメソッドを覚えていくでしょう。

タスク

重要性: 5

“my-short-string” のようなダッシュ区切りの言葉をキャメルケースの “myShortString” に変更する関数 camelize(str) を書いてください。

つまり、すべてのダッシュを削除し、ダッシュの後の各言葉を大文字にします。

例:

camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';

P.S. ヒント: 文字列を配列に分割するために split を使い、それを変換し、join 結果を返してください。

テストと一緒にサンドボックスを開く

重要性: 4

配列 arr を取得し、ab の間で要素を探し、それらの配列を返す関数 filterRange(arr, a, b) を書いてください。

この関数は配列を変更するべきではありません。新しい配列を返すべきです。

例:

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1 (マッチした値)

alert( arr ); // 5,3,8,1 (修正されていない)

テストと一緒にサンドボックスを開く

重要性: 4

配列 arr を取得し、ab の間を除くすべての値をそこから削除する関数 filterRangeInPlace(arr, a, b) を書いてください。テストは a ≤ arr[i] ≤ b です。

この関数は配列のみを修正するべきです。なにも返却するべきではありません。

例:

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // 1 から 4 までの値以外を削除

alert( arr ); // [3, 1]

テストと一緒にサンドボックスを開く

重要性: 4
let arr = [5, 2, 1, -10, 8];

// ... 逆順でソートをするあなたのコード

alert( arr ); // 8, 5, 2, 1, -10
let arr = [5, 2, 1, -10, 8];

arr.sort((a, b) => b - a);

alert( arr );
重要性: 5

文字列の配列 arr を持っています。私たちはソートされたそのコピーを持ちたいですが、arr を修正はせずにキープしたいです。

このようなコピーを返す関数 copySorted(arr) を作成してください。

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted ); // CSS, HTML, JavaScript
alert( arr ); // HTML, JavaScript, CSS (no changes)

コピーを作りそれをソートするのに、 slice() を使うことができます。:

function copySorted(arr) {
  return arr.slice().sort();
}

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted );
alert( arr );
重要性: 5

user オブジェクトの配列を持っているとします。それは user.name を持っています。それは名前の配列に変換するコードを書いてください。

例:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = /* ... your code */

alert( names ); // John, Pete, Mary
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = users.map(item => item.name);

alert( names ); // John, Pete, Mary
重要性: 5

あなたは user オブジェクトの配列をもっており、それは name, surnameid を持っています。

そこから、idfullName (fullNamenamesurname から生成されます)をもつオブジェクトの別の配列を作成するコードを書いてください。

例:

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = /* ... your code ... */

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ) // 1
alert( usersMapped[0].fullName ) // John Smith

したがって、実際には、オブジェクトの1つの配列を別の配列にマップする必要があります。 ここで =>を使ってみてください。

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ); // 1
alert( usersMapped[0].fullName ); // John Smith

アロー関数の場合、追加の括弧が必要であることに注意してください。

このように書くことはできません:

let usersMapped = users.map(user => {
  fullName: `${user.name} ${user.surname}`,
  id: user.id
});

ご存知のように、2つのアロー関数があります: 本体なし value => expr と本体あり value => {...}

ここでは、JavaScript は { をオブジェクトの開始ではなく、関数本体の開始として扱います。ワークアラウンドは “通常の” 括弧でそれらを囲むことです。:

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

これで大丈夫です。

重要性: 5

プロパティ name を持つオブジェクトの配列を取得し、それをソートする関数 sortByName(users) を書いてください。

例:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ john, pete, mary ];

sortByName(arr);

// now: [john, mary, pete]
alert(arr[1].name); // Mary
function sortByName(arr) {
  arr.sort((a, b) => a.name > b.name);
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ john, pete, mary ];

sortByName(arr);

// now sorted is: [john, mary, pete]
alert(arr[1].name); // Mary
重要性: 3

配列の要素をシャッフル(ランダムに再配置)する関数 shuffle(array) を書いてください。

shuffle の複数回実行すると、異なる要素順になります。例えば:

let arr = [1, 2, 3];

shuffle(arr);
// arr = [3, 2, 1]

shuffle(arr);
// arr = [2, 1, 3]

shuffle(arr);
// arr = [3, 1, 2]
// ...

すべての要素順は等しい発生確率である必要があります。例えば、[1,2,3][1,2,3] または [1,3,2] または [3,1,2] などに並び替えられる可能性があり、各ケースの発生確率は同じです。

シンプルな解法は次のようになります:

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

let arr = [1, 2, 3];
shuffle(arr);
alert(arr);

それはいくらか動作します。なぜなら Math.random() - 0.5 は正または負のランダム値なので、ソート関数はランダムに要素を並び替えます。

しかし、ソート関数はこのように使われることを意図していないので、すべての順列が同じ確率を持つわけではありません。

例えば、下のコードを考えてみてください。shuffle を 1000000回実行し、可能性のあるすべての順序の出現数をカウントします。:

function shuffle(array) {
  array.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 array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// 結果を表示します
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

結果例は次の通りです(V8, 2017/7):

123: 250706
132: 124425
213: 249618
231: 124880
312: 125148
321: 125223

明らかにバイアスがあることが分かります: 123213 は他のものよりはるかに多く出現しています。

このコードの結果は JavaScriptエンジンによって異なる可能性がありますが、このアプローチが信頼できないことが分かります。

なぜ上手く動作しないのでしょうか?一般に、sort は “ブラックボックス” です: 配列と比較関数をそこに投げ、配列がソートされることを期待します。しかし、比較の完全なランダム性によりブラックボックスが狂ってしまい、どの程度狂ってしまうかはエンジンによって異なる具体的な実装に依存します。

このタスクをするための他の良い方法があります。例えば、Fisher-Yates shuffle と呼ばれる素晴らしいアルゴリズムがあります。この考えは、逆順に配列を見ていき、各要素をその前のランダムな要素と入れ替えます。

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // 0 から i のランダムなインデックス
    [array[i], array[j]] = [array[j], array[i]]; // 要素を入れ替えます
  }
}

同じ方法でテストしてみましょう。:

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

// counts of appearances for all possible permutations
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// show counts of all possible permutations
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

出力例です:

123: 166693
132: 166647
213: 166628
231: 167517
312: 166199
321: 166316

良い感じに見えます: すべての順列は同じ確率で表示されています。

また、Fisher-Yates アルゴリズムはパフォーマンスの面で遥かに優れており、“ソート” のオーバヘッドがありません。

重要性: 4

プロパティ age をもつオブジェクtの配列を取得し、その平均を取得する関数 getAverageAge(users) を書いてください。

平均の公式は (age1 + age2 + ... + ageN) / N です。

例:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28
function getAverageAge(users) {
  return users.reduce((prev, user) => prev + user.age, 0) / users.length;
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // 28
重要性: 4

arr を配列とします。

arr のユニークなアイテムを持つ配列を返す関数 unique(arr) を作成してください。

例:

function unique(arr) {
  /* your code */
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

テストと一緒にサンドボックスを開く

配列要素を見ていきましょう:

  • 各アイテムに対して、返却する配列がすでにそれを持っているかをチェックします。
  • もしそうであれば無視し、持っていなければ結果に追加します。
function unique(arr) {
  let result = [];

  for (let str of arr) {
    if (!result.includes(str)) {
      result.push(str);
    }
  }

  return result;
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

このコードは機能しますが、そこには潜在的な性能問題があります。

メソッド result.includes(str) は内部で配列 result を歩き、各要素を str と比較して一致するものを探します。

従って、もし result の中に 100 要素あり、誰も str にマッチしない場合、result 全体を歩き、正確に 100 回の比較を行うことになります。また、 10000 のように result が大きいと 10000 回の比較になります。

JavaScriptエンジンは非常に高速なので、それ自体は問題ではありません。なので、 10000 配列を見るのはマイクロ秒のレベルです。

しかし、for ループの中で arr の各要素にこのようなテストをします。

すると、arr.length10000 の場合、10000*10000 = 1億回の比較になります。これは多いです。

従って、この解答は小さい配列の場合にのみ良いです。

さらにチャプター Map, Set, WeakMap と WeakSet では、それを最適化する方法を見ていきます。

サンドボックスでテストと一緒に解答を開く

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。