2023年12月31日

配列メソッド

配列には多くのメソッドが用意されています。理解しやすくするために、この章ではそれらをグループに分けて説明します。

アイテムの追加/削除

先頭または末尾からアイテムを追加および削除するメソッドはすでに知っています。

  • arr.push(...items) – 末尾にアイテムを追加します。
  • arr.pop() – 末尾からアイテムを取り出します。
  • arr.shift() – 先頭からアイテムを取り出します。
  • arr.unshift(...items) – 先頭にアイテムを追加します。

他にいくつかのメソッドがあります。

splice

配列から要素を削除するにはどうすればよいでしょうか?

配列はオブジェクトなので、deleteを使用してみることができます。

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

delete arr[1]; // remove "go"

alert( arr[1] ); // undefined

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

要素は削除されましたが、配列にはまだ3つの要素があり、arr.length == 3であることがわかります。

これは当然のことです。なぜなら、delete obj.keykeyによって値を削除するだけだからです。オブジェクトにはそれで問題ありません。しかし、配列の場合、通常は残りの要素がシフトして空いた場所を埋めることを期待します。配列が短くなることを期待します。

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

arr.splice メソッドは、配列の万能ナイフです。要素の挿入、削除、置換など、あらゆることを実行できます。

構文は次のとおりです。

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

インデックスstartから始まるarrを変更します。deleteCount個の要素を削除し、それらの場所にelem1, ..., elemNを挿入します。削除された要素の配列を返します。

このメソッドは、例を見ると簡単に理解できます。

削除から始めましょう。

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

arr.splice(1, 1); // from index 1 remove 1 element

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

簡単でしょう?インデックス1から開始して、1個の要素を削除しました。

次の例では、3つの要素を削除し、他の2つの要素に置き換えます。

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

// remove 3 first elements and replace them with another
arr.splice(0, 3, "Let's", "dance");

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

ここで、spliceが削除された要素の配列を返すことがわかります。

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

// remove 2 first elements
let removed = arr.splice(0, 2);

alert( removed ); // "I", "study" <-- array of removed elements

spliceメソッドは、削除なしで要素を挿入することもできます。そのためには、deleteCount0に設定する必要があります。

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

// from index 2
// delete 0
// then insert "complex" and "language"
arr.splice(2, 0, "complex", "language");

alert( arr ); // "I", "study", "complex", "language", "JavaScript"
負のインデックスが許可されています。

ここや他の配列メソッドでは、負のインデックスが許可されています。ここでは、配列の末尾からの位置を指定します。

let arr = [1, 2, 5];

// from index -1 (one step from the end)
// delete 0 elements,
// then insert 3 and 4
arr.splice(-1, 0, 3, 4);

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

slice

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

構文は次のとおりです。

arr.slice([start], [end])

インデックスstartからendendは含まない)までのすべてのアイテムをコピーした新しい配列を返します。startendの両方を負にすることができます。その場合、配列の末尾からの位置が想定されます。

これは文字列メソッドstr.sliceに似ていますが、部分文字列の代わりに、部分配列を作成します。

例えば

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

alert( arr.slice(1, 3) ); // e,s (copy from 1 to 3)

alert( arr.slice(-2) ); // s,t (copy from -2 till the end)

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

concat

arr.concatメソッドは、他の配列と追加のアイテムからの値を含む新しい配列を作成します。

構文は次のとおりです。

arr.concat(arg1, arg2...)

任意の数の引数(配列または値)を受け入れます。

結果は、arrからのアイテム、次にarg1arg2などを含む新しい配列です。

引数argNが配列の場合、そのすべての要素がコピーされます。それ以外の場合、引数自体がコピーされます。

例えば

let arr = [1, 2];

// create an array from: arr and [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4

// create an array from: arr and [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6

// create an array from: arr and [3,4], then add values 5 and 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) {
  // ... do something with an item
});

たとえば、これは配列の各要素を表示します。

// for each element call alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

そして、このコードは、ターゲット配列でのそれらの位置についてより詳しく説明します。

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

関数(戻り値がある場合)の結果は破棄され、無視されます。

配列の検索

次に、配列を検索するメソッドについて説明します。

indexOf/lastIndexOf と includes

arr.indexOfarr.includesメソッドは、同様の構文を持ち、基本的に文字列の対応物と同じことを行いますが、文字の代わりにアイテムに対して操作します。

  • arr.indexOf(item, from) – インデックスfromから始まるitemを探し、見つかったインデックスを返し、見つからない場合は-1を返します。
  • arr.includes(item, from) – インデックスfromから始まるitemを探し、見つかった場合はtrueを返します。

通常、これらのメソッドは、検索するitemという1つの引数のみで使用されます。デフォルトでは、検索は先頭から行われます。

例えば

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

indexOfは比較に厳密な等価性===を使用することに注意してください。したがって、falseを探している場合、ゼロではなく正確にfalseを見つけます。

配列にitemが存在するかどうかを確認したいだけで、インデックスが必要ない場合は、arr.includesが推奨されます。

arr.lastIndexOfメソッドはindexOfと同じですが、右から左に検索します。

let fruits = ['Apple', 'Orange', 'Apple']

alert( fruits.indexOf('Apple') ); // 0 (first Apple)
alert( fruits.lastIndexOf('Apple') ); // 2 (last Apple)
includesメソッドはNaNを正しく処理します。

includesの小さな、しかし注目すべき機能は、indexOfとは異なり、NaNを正しく処理することです。

const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (wrong, should be 0)
alert( arr.includes(NaN) );// true (correct)

これは、includesがJavaScriptにはるかに後で追加され、内部的に最新の比較アルゴリズムを使用しているためです。

find と findIndex/findLastIndex

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

ここで、arr.find(fn)メソッドが役立ちます。

構文は次のとおりです。

let result = arr.find(function(item, index, array) {
  // if true is returned, item is returned and iteration is stopped
  // for falsy scenario returns 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に提供していることに注意してください。これは一般的であり、この関数の他の引数はめったに使用されません。

arr.findIndexメソッドは、同じ構文を持ちますが、要素自体ではなく、要素が見つかったインデックスを返します。何も見つからない場合は、-1の値が返されます。

arr.findLastIndexメソッドはfindIndexに似ていますが、lastIndexOfと同様に、右から左に検索します。

例を次に示します。

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

// Find the index of the first John
alert(users.findIndex(user => user.name == 'John')); // 0

// Find the index of the last John
alert(users.findLastIndex(user => user.name == 'John')); // 3

filter

findメソッドは、関数がtrueを返す単一(最初)の要素を探します。

多数ある可能性がある場合は、arr.filter(fn)を使用できます。

構文はfindに似ていますが、filterは一致するすべての要素の配列を返します。

let results = arr.filter(function(item, index, array) {
  // if true item is pushed to results and the iteration continues
  // returns empty array if nothing found
});

例えば

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

// returns array of the first two users
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

配列の変換

次に、配列を変換および並べ替えるメソッドに進みましょう。

map

arr.mapメソッドは、最も便利でよく使用されるメソッドの1つです。

配列の各要素に対して関数を呼び出し、結果の配列を返します。

構文は次のとおりです。

let result = arr.map(function(item, index, array) {
  // returns the new value instead of 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 ];

// the method reorders the content of 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 the first value is greater than the second
  if (a == b) return 0; // if values are equal
  if (a < b) return -1; // if the first value is less than the second
}

たとえば、数値としてソートするには、

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 はどんなものでも配列にすることができますね?数値、文字列、オブジェクトなど、何でも入れることができます。つまり、いくつかのアイテムのセットがあるということです。それをソートするには、要素を比較する方法を知っている順序付け関数が必要です。デフォルトは文字列順です。

arr.sort(fn) メソッドは、一般的なソートアルゴリズムを実装しています。内部でどのように動作するか(最適化された クイックソート または ティムソート がほとんどの場合)を気にする必要はありません。配列を走査し、提供された関数を使用して要素を比較し、並べ替えます。私たちに必要なのは、比較を行う fn を提供することだけです。

ところで、どの要素が比較されているかを知りたい場合は、アラートで表示することもできます。

[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 を使用します。

文字列 の比較アルゴリズムを覚えていますか?デフォルトでは、文字をコードで比較します。

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

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

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

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

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

逆順にする

arr.reverse メソッドは、arr 内の要素の順序を反転させます。

例えば

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

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

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

split と join

実際の状況を考えてみましょう。メッセージングアプリを作成中で、ユーザーがコンマ区切りの受信者リストを入力します: John, Pete, Mary。しかし、私たちにとっては、単一の文字列よりも名前の配列の方がはるかに使いやすいでしょう。どうすれば取得できるでしょうか?

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  (and other names)
}

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(glue) の呼び出しは、split の逆を行います。arr アイテムを glue で結合した文字列を作成します。

例えば

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

let str = arr.join(';'); // glue the array into a string using ;

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

reduce/reduceRight

配列を反復処理する必要がある場合は、forEachfor、または for..of を使用できます。

各要素のデータを反復処理して返す必要がある場合は、map を使用できます。

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

構文は次のとおりです。

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

関数は、すべての配列要素に1つずつ順番に適用され、その結果を次の呼び出しに「引き継ぎます」。

引数

  • accumulator – 前の関数呼び出しの結果です。(initial が提供されている場合は)最初は initial と等しくなります。
  • item – 現在の配列要素です。
  • index – その位置です。
  • array – 配列です。

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

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

複雑に聞こえますか?

それを理解する最も簡単な方法は、例によるものです。

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

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

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

alert(result); // 15

reduce に渡される関数は、通常はこれで十分な2つの引数のみを使用します。

何が起こっているかの詳細を見てみましょう。

  1. 最初の実行では、suminitial 値(reduce の最後の引数)であり、0 に等しく、current は最初の配列要素であり、1 に等しいです。したがって、関数の結果は 1 です。
  2. 2回目の実行では、sum = 1 で、2番目の配列要素 (2) をそれに追加して返します。
  3. 3回目の実行では、sum = 3 で、さらに別の要素を追加します。以下同様です…

計算フロー

または、表形式では、各行が次の配列要素に対する関数呼び出しを表します。

合計 現在 結果
最初の呼び出し 0 1 1
2回目の呼び出し 1 2 3
3回目の呼び出し 3 3 6
4回目の呼び出し 6 4 10
5回目の呼び出し 10 5 15

ここでは、前の呼び出しの結果が次の呼び出しの最初の引数になる様子が明確にわかります。

初期値を省略することもできます。

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

// removed initial value from reduce (no 0)
let result = arr.reduce((sum, current) => sum + current);

alert( result ); // 15

結果は同じです。これは、初期値がない場合、reduce が配列の最初の要素を初期値として取得し、2番目の要素から反復を開始するためです。

計算表は上記と同じですが、最初の行がありません。

ただし、このような使用には細心の注意が必要です。配列が空の場合、初期値のない reduce 呼び出しはエラーになります。

例を次に示します。

let arr = [];

// Error: Reduce of empty array with no initial value
// if the initial value existed, reduce would return it for the empty arr.
arr.reduce((sum, current) => sum + current);

そのため、常に初期値を指定することをお勧めします。

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

Array.isArray

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

したがって、typeof はプレーンオブジェクトを配列から区別するのに役立ちません。

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

...しかし、配列は非常によく使用されるため、特別なメソッドがあります: Array.isArray(value)value が配列の場合 true を返し、それ以外の場合は false を返します。

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

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

ほとんどのメソッドは "thisArg" をサポートしています。

sort を除いた、関数を呼び出すほとんどすべての配列メソッド(findfiltermap など)は、オプションの追加パラメータ thisArg を受け入れます。

このパラメータはめったに使用されないため、上記のセクションでは説明されていません。ただし、完全を期すために、説明する必要があります。

これらのメソッドの完全な構文は次のとおりです。

arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg is the optional last argument

thisArg パラメータの値は、functhis になります。

たとえば、ここで 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}
];

// find users, for who army.canJoin returns 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(非包含)までの要素をそこにコピーします。
    • 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(value)value が配列であるかどうかを確認し、そうであれば true を返し、そうでない場合は false を返します。

sortreverse、および splice メソッドは、配列自体を変更することに注意してください。

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

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

    関数 fn は、map と同様に配列の各要素に対して呼び出されます。いずれかの結果またはすべての結果が true の場合は true を返し、それ以外の場合は false を返します。

    これらのメソッドは、|| および && 演算子と似たような動作をします。もし fn が truthy な値を返すと、arr.some() は直ちに true を返し、残りのアイテムに対するイテレーションを停止します。もし fn が falsy な値を返すと、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) は、多次元配列から新しいフラットな配列を作成します。

完全なリストについては、マニュアルを参照してください。

一見すると、非常に多くのメソッドがあり、覚えるのがかなり難しいように思えるかもしれません。しかし実際には、ずっと簡単です。

チートシートに目を通し、それらを把握してください。次に、この章のタスクを解いて練習し、配列メソッドの経験を積んでください。

その後、配列で何かをしたいときに、どうすればいいかわからない場合は、ここに戻ってきて、チートシートを見て、適切なメソッドを見つけてください。例は、正しく書くのに役立ちます。すぐに、特別な努力をしなくても、メソッドを自動的に覚えるでしょう。

タスク

重要度: 5

ダッシュで区切られた "my-short-string" のような単語を、キャメルケースの "myShortString" に変更する関数 camelize(str) を書いてください。

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

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

追伸: ヒント: 文字列を配列に分割するために split を使用し、変換して join で戻してください。

テスト付きのサンドボックスを開く。

function camelize(str) {
  return str
    .split('-') // splits 'my-long-word' into array ['my', 'long', 'word']
    .map(
      // capitalizes first letters of all array items except the first one
      // converts ['my', 'long', 'word'] into ['my', 'Long', 'Word']
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    )
    .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord'
}

テスト付きの解答をサンドボックスで開く。

重要度: 4

配列 arr を受け取り、a 以上かつ b 以下の値を持つ要素を探し、結果を配列として返す関数 filterRange(arr, a, b) を作成してください。

関数は配列を変更すべきではありません。新しい配列を返す必要があります。

例えば

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

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

alert( filtered ); // 3,1 (matching values)

alert( arr ); // 5,3,8,1 (not modified)

テスト付きのサンドボックスを開く。

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

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

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

alert( filtered ); // 3,1 (matching values)

alert( arr ); // 5,3,8,1 (not modified)

テスト付きの解答をサンドボックスで開く。

重要度: 4

配列 arr を受け取り、ab の間の値を除くすべての値を削除する関数 filterRangeInPlace(arr, a, b) を作成してください。テストは: a ≤ arr[i] ≤ b です。

関数は配列のみを変更する必要があります。何も返す必要はありません。

例えば

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

filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 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--;
    }
  }

}

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

filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4

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

テスト付きの解答をサンドボックスで開く。

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

// ... your code to sort it in decreasing order

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" のような "NUMBER 演算子 NUMBER" (スペース区切り) の形式の文字列を受け取り、結果を返すメソッド calculate(str) を実装します。プラス + とマイナス - を理解する必要があります。

    使用例

    let calc = new Calculator;
    
    alert( calc.calculate("3 + 7") ); // 10
  2. 次に、計算機に新しい操作を教えるメソッド addMethod(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 つのスペースで区切られています。
  • 必要であれば、エラー処理を追加してもかまいません。

テスト付きのサンドボックスを開く。

  • メソッドがどのように保存されているかに注意してください。それらは単に this.methods プロパティに追加されます。
  • すべてのテストと数値変換は calculate メソッドで行われます。将来的には、より複雑な式をサポートするように拡張される可能性があります。
function Calculator() {

  this.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 (!this.methods[op] || isNaN(a) || isNaN(b)) {
      return NaN;
    }

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

  this.addMethod = function(name, func) {
    this.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 オブジェクトの配列があり、それぞれが namesurnameid を持っています。

そこから、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

つまり、実際には、オブジェクトの配列を別の配列にマッピングする必要があります。ここで => を試してみてください。少し注意点があります。

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
});

覚えているように、ボディなしの value => expr とボディありの value => {...} の 2 つのアロー関数があります。

ここでは、JavaScript は { をオブジェクトの開始ではなく、関数ボディの開始として扱います。回避策は、それらを「通常の」括弧で囲むことです。

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

これで大丈夫です。

重要度: 5

age プロパティを持つオブジェクトの配列を受け取り、age でソートする関数 sortByAge(users) を作成してください。

例えば

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

let arr = [ pete, john, mary ];

sortByAge(arr);

// now: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
function sortByAge(arr) {
  arr.sort((a, b) => a.age - b.age);
}

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

let arr = [ pete, john, mary ];

sortByAge(arr);

// now sorted is: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
重要度: 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);
}

// 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]}`);
}

結果の例 (JS エンジンによって異なります)

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

123213 は他のものよりもはるかに頻繁に表示されることがわかります。これは明確な偏りです。

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

なぜうまくいかないのですか?一般的に言って、sort は「ブラックボックス」です。配列と比較関数をそれに投げ込み、配列がソートされることを期待します。しかし、比較の完全なランダム性のため、ブラックボックスは狂い、どのように狂うかは、エンジンによって異なる具体的な実装に依存します。

タスクを実行するための他の良い方法があります。たとえば、Fisher-Yates シャッフル と呼ばれる優れたアルゴリズムがあります。考え方は、配列を逆順に歩き、各要素をその前のランダムな要素と交換することです。

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i

    // swap elements array[i] and array[j]
    // we use "destructuring assignment" syntax to achieve that
    // you'll find more details about that syntax in later chapters
    // same can be written as:
    // let t = array[i]; array[i] = array[j]; array[j] = t
    [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 プロパティを持つオブジェクトの配列を受け取り、平均年齢を返す関数 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 と比較して一致するものを探します。

したがって、result100 個の要素があり、str に一致するものが 1 つもない場合、result 全体を調べて正確に 100 回の比較を行います。また、result10000 のように大きい場合、10000 回の比較が行われます。

JavaScript エンジンは非常に高速であるため、10000 個の配列を調べることはマイクロ秒の問題であるため、それ自体は問題ではありません。

しかし、for ループで、arr の各要素に対してそのようなテストを行います。

したがって、arr.length10000 の場合、10000*10000 = 1 億回の比較のようなものが発生します。それはたくさんです。

したがって、この解決策は小さな配列でのみ有効です。

この章の後半のマップとセットでは、それを最適化する方法を見ていきます。

テスト付きの解答をサンドボックスで開く。

重要度: 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);

/*
// after the call we should have:

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 を持つ配列項目は 2 つ存在しない可能性があります。

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

テスト付きのサンドボックスを開く。

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

テスト付きの解答をサンドボックスで開く。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善するための提案がある場合は、コメントするのではなく、GitHub issue を送信するか、プルリクエストをしてください。
  • 記事で何か理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには <code> タグを使用し、複数行の場合は <pre> タグで囲み、10 行を超える場合はサンドボックスを使用してください (plnkrjsbincodepen…)