2024年9月24日

配列のメソッド

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

アイテムの追加/削除

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

  • 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 ) // 今は ["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 arr = ["t", "e", "s", "t"];

alert( arr.slice(1, 3) ); // e,s (1 から 3 をコピー)

alert( arr.slice(-2) ); // s,t (-2 から末尾まで)

引数なし arr.slice() でも呼び出すことができ、これは arr のコピーを生成します。これは、オリジナルの配列に影響を与えない形でさらに変換するためのためのコピーを取得するのによく使用されます。

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]

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

let arr = [1, 2];

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

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

イテレート/反復: 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}`);
});

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

配列での検索

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

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.includes が好ましいです。

また、includes の非常に小さな違いは、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) {
  // true が返却されると、item が返却され、イテレーションは停止します
  // 偽の場合は undefined です
});

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

  • 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 と同じですが、filter はマッチしたすべて要素の配列を返します:

let results = arr.filter(function(item, index, array) {
  // true の場合、item は results にプッシュされ、イテレーションは継続します
  // 何も見つからない場合は、空配列を返します
});

例:

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 は配列を 決まった位置に ソートし、要素の順番を変更します。

これもソートされた配列を返しますが、arr 自身が変更されるので、返却値は通常無視されます。

例:

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 を提供することだけです。

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

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

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

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

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

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

let arr = [ 1, 2, 15 ];

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

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

アロー関数 を覚えていますか? すっきりしたソートを書くために使えます。:

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

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

文字列には localeCompare を使用します

stringsの比較アルゴリズムを思い出してください。デフォルトではコードで文字比較を行います。

多くのアルファベットでは、Ö などの文字のソートを正しく行うためのメソッド str.localeCompare を使用するのがよいです。

例えば、ドイツ語でいくつかの国をソートしてみましょう:

let countries = ['Österreich', 'Andorra', 'Vietnam'];

alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (間違い)

alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (正しい!)

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, for あるいは for..of を使うことができます。

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

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

構文:

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

関数は各要素に順番に適用され、その結果を次の呼び出しに “引き継ぎ” ます:

引数:

  • accumulator – 前の関数呼び出しの結果で、初回は initial と等価です(initial が指定されている場合)
  • item – 現在の配列の項目です。
  • index – 位置です。
  • arr – 配列です。

関数が適用されると、前の関数呼び出しの結果が、次の関数呼び出しの最初の引数として渡されます。

したがって、最初の引数は基本的に、以前のすべての実行の結合結果を格納するアキュムレータです。そして、最後にそれは reduce の結果になります。

複雑に見えますか?

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

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

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

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

alert(result); // 15

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

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

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

計算のフロー:

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

sum current result
最初の呼び出し 0 1 1
2回目の呼び出し 1 2 3
3回目の呼び出し 3 3 6
4回目の呼び出し 6 4 10
5回目の呼び出し 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 メソッドも同じをことを行いますが、右から左に実行します。

Array.isArray

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

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

alert(typeof {}); // object
alert(typeof []); // object(同じ)

…しかし、配列は頻繁に使用されるため、そのための特別なメソッド 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 になります。

例えば、ここでは filter で army オブジェクトのメソッドを使用し、thisArg はそのコンテキストを渡します。:

let army = {
  minAge: 18,
  maxAge: 27,
  canJoin(user) {
    return user.age >= this.minAge && user.age < this.maxAge;
  }
};

let users = [
  {age: 16},
  {age: 20},
  {age: 23},
  {age: 30}
];

// army.canJoin が true となるユーザを見つけます
let soldiers = users.filter(army.canJoin, army);

alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23

もし上の例で users.filter(army.canJoin) としていた場合、army.canJoin はスタンドアローンの関数として呼び出されるため、this=undefined であり、即時エラーになります。

呼び出し users.filter(army.canJoin, army)users.filter(user => army.canJoin(user)) に置き換え可能であり、同じことをします。多くの人にとってより理解しやすいので、後者の方がよく利用されます。

サマリ

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

  • 要素の追加/削除をする場合:

    • push(...items) – アイテムを末尾に追加します,
    • pop() – 末尾からアイテムを抽出します,
    • shift() – 先頭からアイテムを抽出します,
    • unshift(...items) – 先頭にアイテムを追加します.
    • splice(pos, deleteCount, ...items) – インデックス pos から deleteCount 数要素を削除し 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 のようですが、値の代わりにインデックスを返します。
  • 要素を反復処理するには:

    • forEach(func) – すべての要素に対して funcを呼び出し、何も返しません。
  • 配列を変換するには:

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

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

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

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

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

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

    これらのメソッドは ||&& 演算子のように振る舞います。もし fn が真の値を返す場合、arr.some() はすぐに true を返し、残りの項目に対するイテレーションを停止します。fn が偽の値を返す場合は、arr.every() はすぐに false を返し、同様に残りの項目のイテレーションは停止します。

    every は配列を比較するのに使えます:

    function arraysEqual(arr1, arr2) {
      return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
    }
    
    alert( arraysEqual([1, 2], [1, 2])); // true
  • arr.fill(value, start, end) – インデックス start から end まで value で配列を埋めます。

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

  • arr.flat(depth)/arr.flatMap(fn) は多次元配列からフラットな配列を生成します。

完全なリストは 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 結果を返してください。

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

function camelize(str) {
  return str
    .split('-') // my-long-word -> ['my', 'long', 'word']
    .map(
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    ) // ['my', 'long', 'word'] -> ['my', 'Long', 'Word']
    .join(''); // ['my', 'Long', 'Word'] -> myLongWord
}

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

重要性: 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 (修正されていない)

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

function filterRange(arr, a, b) {
  // added brackets around the expression for better readability
  return arr.filter(item => (a <= item && item <= b));
}

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

重要性: 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]

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

function filterRangeInPlace(arr, a, b) {

  for (let i = 0; i < arr.length; i++) {
    let val = arr[i];

    // remove if outside of the interval
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  }

}

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

重要性: 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

“拡張可能な” 計算機オブジェクトを作るコンストラクタ関数 Calculator を作りなさい。

タスクは2つのパートから構成されます。

  1. 最初に フォーマット “数値 演算子 数値” (スペース区切り) で、"1 + 2" のような文字列を取り、結果を返すメソッド calculate(str) メソッドを実装します。それはプラス + と マイナス - を理解できるようにしてください。

    使い方の例:

    let calc = new Calculator;
    
    alert( calc.calculate("3 + 7") ); // 10
  2. 次に、計算機に新しい操作を教えるメソッド addOperator(name, func) を追加します。操作 name と、それを実装する2つの引数を持つ関数 func(a,b) を取ります。

    例えば、乗算 *, 除算 / やべき乗 **:

    let powerCalc = new Calculator;
    powerCalc.addMethod("*", (a, b) => a * b);
    powerCalc.addMethod("/", (a, b) => a / b);
    powerCalc.addMethod("**", (a, b) => a ** b);
    
    let result = powerCalc.calculate("2 ** 3");
    alert( result ); // 8
  • このタスクではかっこや複雑な表現は不要です。
  • 数字と演算子は、正確に1つのスペースで区切られます。
  • 追加したい場合にエラー処理があるかもしれません。

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

  • メソッドの格納方法に注意してください。それらは単に内部オブジェクトに追加されています。
  • すべてのテストと数値変換は calculate メソッドで行われます。将来より複雑な式をサポートするために拡張されるかもしれません。
function Calculator() {

  let methods = {
    "-": (a, b) => a - b,
    "+": (a, b) => a + b
  };

  this.calculate = function(str) {

    let split = str.split(' '),
      a = +split[0],
      op = split[1],
      b = +split[2]

    if (!methods[op] || isNaN(a) || isNaN(b)) {
      return NaN;
    }

    return methods[op](a, b);
  }

  this.addMethod = function(name, func) {
    methods[name] = func;
  };
}

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

重要性: 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) => b.name > a.name ? 1 : -1);
}

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" が見つかりません では、それを最適化する方法を見ていきます。

function unique(arr) {
  let result = [];

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

  return result;
}

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

重要性: 4

{id:..., name:..., age... } といった形式でユーザの配列を受け取ったとしましょう。

これから、id をキーとし、配列項目を値としたオブジェクトを作成する関数 groupById(arr) を作成してください。

例:

let users = [
  {id: 'john', name: "John Smith", age: 20},
  {id: 'ann', name: "Ann Smith", age: 24},
  {id: 'pete', name: "Pete Peterson", age: 31},
];

let usersById = groupById(users);

/*
// 呼び出し後はこのようになります:

usersById = {
  john: {id: 'john', name: "John Smith", age: 20}
  ann: {id: 'ann', name: "Ann Smith", age: 24},
  pete: {id: 'pete', name: "Pete Peterson", age: 31},
}
*/

このような関数はサーバデータを扱う際に非常に便利です。

このタスクでは id はユニークであるとします。同じ id を持つ配列項目はありません。

この解法では配列の .reduce メソッドを使用してください。

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

function groupById(array) {
  return array.reduce((obj, value) => {
    obj[value.id] = value;
    return obj;
  }, {})
}

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

チュートリアルマップ

コメント

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