読者です 読者をやめる 読者になる 読者になる

interprism's blog

インタープリズム株式会社の開発者ブログです。

JavaScriptの配列関数について今更まとめてみた(後編)

JavaScript EcmaScript

この投稿は インタープリズムの面々が、普段の業務に役立つ記事を丹精込めて書き上げる! Advent Calendar 2016 - Qiitaの5日目 の記事です。

こんにちは、imamotoです。

JavaScriptの配列関数について、2回に渡って整理するシリーズの後編になります。

前編はこちらをご覧ください。

interprism.hatenablog.com

前編では以下の配列関数について説明しました。

1. static関数
ES7項番 関数 戻り値 バージョン
22.1.2.2 Array.isArray ( arg ) Boolean ES5
22.1.2.1 Array.from ( items [ , mapfn [ , thisArg ] ] ) Array ES6
22.1.2.3 Array.of ( ...items ) Array ES6
2. 配列をコピーせずに要素を追加・削除する関数
ES7項番 関数 戻り値 バージョン
22.1.3.18 Array.prototype.push ( ...items ) 新しいlength -
22.1.3.29 Array.prototype.unshift ( ...items ) 新しいlength -
22.1.3.17 Array.prototype.pop ( ) 最後の要素 -
22.1.3.22 Array.prototype.shift ( ) 先頭の要素 -
3. 配列をコピーして要素を追加・削除する関数
ES7項番 関数 戻り値 バージョン
22.1.3.1 Array.prototype.concat ( ...arguments ) Array -
22.1.3.23 Array.prototype.slice (start, end) Array -
4. ある配列から特定のIteratorを取得する関数
ES7項番 関数 戻り値 バージョン
22.1.3.14 Array.prototype.keys ( ) Iterator ES6
22.1.3.30 Array.prototype.values ( ) Iterator ES6
22.1.3.4 Array.prototype.entries ( ) Iterator ES6

後編では、前編で紹介しきれなかった残りの配列関数についてまとめていきたいと思います。

少し長く情報も多いですが、最後まで読んでいただけると幸いです。

それじゃ引き続き、イカした関数を紹介していくぜ!!

5. すべての要素にコールバックを実行する関数

ES7項番 関数 戻り値 バージョン
22.1.3.10 Array.prototype.forEach ( callbackfn [ , thisArg ] ) void ES5
22.1.3.7 Array.prototype.filter ( callbackfn [ , thisArg ] ) Array ES5
22.1.3.16 Array.prototype.map ( callbackfn [ , thisArg ] ) Array ES5
22.1.3.19 Array.prototype.reduce ( callbackfn [ , initialValue ] ) Any ES5
22.1.3.20 Array.prototype.reduceRight ( callbackfn [ , initialValue ] ) Any ES5

引数のコールバックを配列のすべての要素に対して実行する関数たち

  • このグループでは「引数で受け取ったコールバック関数を、配列のすべての要素について実行する関数」5つをまとめました。
  • すべてES5で追加された、非常に便利な関数です。
  • 戻り値の種類に応じて更に以下の3タイプに分かれます。
    • コールバックを実行するだけの関数
    • コールバックを実行して新たな配列を取得する関数
    • コールバックのすべての実行結果から新たな値を取得する関数

コールバックを実行するだけの "forEach"

  • Array.prototype.forEach ( callbackfn [ , thisArg ] )
    • 配列の要素について、第一引数のコールバック関数callbackfnを先頭から順に実行します。
      • このコールバック関数には配列の値, 配列のインデックス, 配列そのものの3つの引数を与えられます。
    • 第二引数のthisArgでコールバック関数内のthisを指すオブジェクトをバインドできます。
[1, 2, 3].forEach(function(value, index, array) {
    console.log('length: ' + array.length + ', index: ' + index + ', value: ' + value);
});
// length: 3, index: 0, value: 1
// length: 3, index: 1, value: 2
// length: 3, index: 2, value: 3

コールバックを実行して新たな配列を取得する "filter", "map"

  • "filter"と"map"は、前述した"forEach"と同じ引数を受け取ります(コールバック関数の引数も同じです)
  • Array.prototype.filter ( callbackfn [ , thisArg ] )

    • 配列の要素について、第一引数のコールバック関数callbackfnを先頭から順に実行し、戻り値がtrueである要素だけを持つ新たな配列を取得します。
      • このコールバック関数には配列の値, 配列のインデックス, 配列そのものの3つの引数を与えられます。
    • 第二引数のthisArgでコールバック関数内のthisを指すオブジェクトをバインドできます。
  • Array.prototype.map ( callbackfn [ , thisArg ] )

    • 配列の要素について、第一引数のコールバック関数callbackfnを先頭から順に実行し、その実行結果で構成された新たな配列を取得します。
      • このコールバック関数には配列の値, 配列のインデックス, 配列そのものの3つの引数を与えられます。
    • 第二引数のthisArgでコールバック関数内のthisを指すオブジェクトをバインドできます。
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// filterで偶数だけ取得
var evenArray = array.filter(function(value) {return value % 2 === 0;});

// mapですべての要素を2乗
var evenSquareArray = evenArray.map(function(value) {return value * value;});

// forEachで出力
evenSquareArray.forEach(function(value) {console.log(value);});
// 4
// 16
// 36
// 64
// 100

コールバックのすべての実行結果から新たな値を取得する "reduce", "reduceRight"

  • Array.prototype.reduce ( callbackfn [ , initialValue ] )
    • 配列の要素について、第一引数のコールバック関数callbackfnを先頭から順に実行して、1つの値を返却します。
      • このコールバック関数には1つ前の実行結果, 現在の配列の値, 配列のインデックス, 配列そのものの4つの引数を与えられます。
    • 第二引数がある場合は、コールバック関数の引数「1つ前の実行結果」の初期値となります。
    • 第二引数がない場合は、コールバック関数の引数「1つ前の実行結果」の初期値は配列の先頭の要素の値になります。
      • この場合「現在の配列の値」「配列のインデックス」は配列の2つ目の要素になります。
  • Array.prototype.reduceRight ( callbackfn [ , initialValue ] )
    • 配列の要素について、第一引数のコールバック関数callbackfnを末尾から順に実行して、1つの値を返却します。
    • それ以外は "reduce" と同じです。
// 第二引数の初期値がある場合
[1, 2, 3].reduce(function(prev, current, index, array) {
    console.log('prev: ' + prev + ', current: ' + current + ', index: ' + index);
    return prev + current;
}, 10);
// prev: 10, current: 1, index: 0
// prev: 11, current: 2, index: 1
// prev: 13, current: 3, index: 2
// 16

// 第二引数の初期値がない場合
[1, 2, 3].reduce(function(prev, current, index, array) {
    console.log('prev: ' + prev + ', current: ' + current + ', index: ' + index);
    return prev + current;
});
// prev: 1, current: 2, index: 1
// prev: 3, current: 3, index: 2
// 6

// reduceとreduceRightの比較
var combineString = function(prev, current) { return prev + ' ' + current; };
["Hello","Array","World"].reduce(combineString); // "Hello Array World"
["Hello","Array","World"].reduceRight(combineString); // "World Array Hello"

[余談] "map", "filter", "forEach" とアロー関数の相性が良い理由

  • ES6から新たに追加されたアロー関数は関数が短く書けてとても便利です。
  • mapfilterは配列が戻り値なので、このアロー関数とは特に相性が良いように思います。
// 先程の例もワンライナーで書ける
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter(v => v % 2 === 0).map(v => v * v).forEach(v => console.log(v));
// 4
// 16
// 36
// 64
// 100
  • もう1つ、map, filter, forEachはアロー関数と相性が良い理由があります。
  • それは以下の例のように、第二引数で指定するコールバック関数のthisへのバインドを省略できることにあります。
function Example() {
    // コンストラクタ関数のローカル変数
    this.number = 10;

    // thisを保存
    var self = this;

    // 1. selfを使ってnumberの値を参照
    this.func1 = function() {
        [1, 2, 3].forEach(function(value) {
            console.log(self.number + value);
        });
    };

    // 2. 第二引数にthisをバインドする
    this.func2 = function() {
        [1, 2, 3].forEach(function(value) {
            console.log(this.number + value);
        }, this);
    };

    // 3. コールバック関数をアロー関数にすると関数が定義された場所のthisを指す
    this.func3 = function() {
        [1, 2, 3].forEach(v => console.log(this.number + v));
    };

    // 誤った例:thisをバインドせずに直接コールバック関数内で呼び出す
    this.wrongFunc = function() {
        [1, 2, 3].forEach(function(value) {
            console.log(this.number + value);
        });
    };
}

var example = new Example();

// すべて同じ出力結果になる(11, 12, 13)
example.func1();
example.func2();
example.func3();

// this.numberを参照できない(NaN, NaN, NaN)
example.wrongFunc();

アロー関数についてはこちらもとても参考になりました。

developer.mozilla.org

js-next.hatenablog.com

6. 配列内の要素を検索する関数

ES7項番 関数 戻り値 バージョン
22.1.3.5 Array.prototype.every ( callbackfn [ , thisArg ] ) Boolean ES5
22.1.3.24 Array.prototype.some ( callbackfn [ , thisArg ] ) Boolean ES5
22.1.3.8 Array.prototype.find ( predicate [ , thisArg ] ) value ES6
22.1.3.9 Array.prototype.findIndex ( predicate [ , thisArg ] ) index ES6
22.1.3.12 Array.prototype.indexOf ( searchElement [ , fromIndex ] ) index ES5
22.1.3.15 Array.prototype.lastIndexOf ( searchElement [ , fromIndex ] ) index ES5
22.1.3.11 Array.prototype.includes ( searchElement [ , fromIndex ] ) Boolean ES7

色々な方法で配列内の要素を検索してくれる関数たち

  • このグループでは「配列内の要素を探す関数」7つをまとめました。
  • ES5から充実し始めており、唯一ES7で追加となった配列関数includesもこのグループに含まれます。
  • 「何で探すか」と「何を返すか」で分類できます。

分類表

コールバック関数がtrueを返す要素を探す 指定した値に一致する要素を探す
当てはまったか否かをBooleanで返す every(全部true), some(1つでもtrue) includes(1つでも一致)
当てはまった値を返す find(最初の値) なし
当てはまったインデックスを返す findIndex(最初のインデックス) indexOf(最初のインデックス), lastIndexOf(最後のインデックス)
  • コールバック関数がtrueを返す要素を探す関数
    • 第一引数のコールバック関数は配列の値, 配列のインデックス, 走査中の配列の3つの引数を受け取ることができます。
    • 第二引数ではコールバック関数のthisをバインドできます。
  • 指定した値に一致する要素を探す関数
    • 第二引数に検索を開始するインデックスを指定できます。

[余談] 検索を開始するインデックスと配列走査の方向について

  • 「指定した値に一致する要素を探す関数」3つについて、以下にそれぞれ特徴がある
    • 検索を開始するインデックス(fromIndex)のデフォルトについて
    • 配列走査の方向について
  • indexOf, includes
    • fromIndexのデフォルト値は0(配列全体を検索)
    • fromIndexが負の値の場合は配列の終わりからのオフセット
    • fromIndexに負の値が設定されても、配列の走査は前から後ろ
  • lastIndexOf
    • fromIndexのデフォルト値はarray.length
    • fromIndexが負の値の場合は配列の終わりからのオフセット
    • fromIndexに負の値が設定されても、配列の走査は後ろから前

7. 配列の要素を並び替える関数

ES7項番 関数 戻り値 バージョン
22.1.3.21 Array.prototype.reverse ( ) void -
22.1.3.25 Array.prototype.sort (comparefn) void -

配列の要素を並び替えてくれる関数たち

  • このグループでは「要素を並び替える関数」2つをまとめました。
  • 2つとも破壊的な関数で、ES5よりも前から存在しています。

  • Array.prototype.reverse ( ):配列を逆順にソートします。

  • Array.prototype.sort (comparefn):引数のコールバック関数の戻り値に応じてソートします。
    • コールバック関数が指定されない場合は要素を文字列比較して辞書順にソートされます。
    • コールバック関数が指定されている場合、関数は比較対象の2つの配列の要素を引数として受け取ります。
      • comparefn(a, b)が負の値を返す場合、aはbよりも前(小さいインデックス)になります。
      • comparefn(a, b)が正の値を返す場合、aはbよりも後(大きいインデックス)になります。
      • comparefn(a, b)が0を返す場合、aとbの関係の変動はありません。

8. 配列の文字列表現

ES7項番 関数 戻り値 バージョン
22.1.3.13 Array.prototype.join (separator) String -
22.1.3.27 Array.prototype.toLocaleString ( [ reserved1 [ , reserved2 ] ] ) String -
22.1.3.28 Array.prototype.toString ( ) String -

配列を文字列で表現する関数たち

  • このグループでは「配列を文字列表現にする関数」3つをまとめました。
  • どの関数も特定の区切り文字で先頭から各要素を繋げる

  • Array.prototype.join (separator):指定した区切り文字で各要素を繋げた文字列を返却します。デフォルトは","(カンマ)。

  • Array.prototype.toLocaleString ( [ reserved1 [ , reserved2 ] ] ):各要素をロケールに応じた文字列に変換し、カンマやピリオドで区切った文字列を返却します。
  • Array.prototype.toString ( ):カンマ区切りの文字列を返却します。

9. 配列内の要素を更新・削除する関数

ES7項番 関数 戻り値 バージョン
22.1.3.3 Array.prototype.copyWithin (target, start [ , end ] ) void ES6
22.1.3.6 Array.prototype.fill (value [ , start [ , end ] ] ) void ES6
22.1.3.26 Array.prototype.splice (start, deleteCount, ...items ) void -

配列の中身を変えちゃう関数たち

  • このグループでは「配列内の要素を更新・削除する関数」3つをまとめました。
  • copyWithinfillはES6で追加された新しい関数です。
  • 文字だけでの説明が難しいため、コードを見ながら1つずつ説明します。

自身の一部を自身の別の場所に貼り付ける "copyWithin"

  • Array.prototype.copyWithin (target, start [ , end ] )
    • 配列の連続した要素を、その配列の別の場所から貼り付けて要素を上書きします。
    • 既存の要素を上書きするので、配列の長さは変わりません。
    • target:貼り付け先
    • start:コピー元開始位置(デフォルトは0)
    • end:コピー元終了位置(デフォルトは配列のlength)
// startとendはデフォルト値
// コピー元の長さがはみ出た場合も配列の長さは変わらない
[1, 2, 3, 4, 5].copyWithin(3); // [1, 2, 3, 1, 2]

// 負の値も設定できる
[1, 2, 3, 4, 5].copyWithin(-2); // [1, 2, 3, 1, 2]

// startとendにも負の値が設定できる
[1, 2, 3, 4, 5].copyWithin(0, -2 ,-1); // [4, 2, 3, 4, 5]

同じ値で配列を埋め尽くす "fill"

  • Array.prototype.fill (value [ , start [ , end ] ] )
    • 配列の開始位置と終了位置を指定して、すべて同じ値で要素を上書きします。
    • 既存の要素を上書きするので、配列の長さは変わりません。
    • value:配列に設定する値
    • start:開始位置(デフォルトは0)
    • end:終了位置(デフォルトは配列のlength)
// すべての値を上書きする
[1, 2, 3].fill(4); // [4, 4, 4]

// startとendに負の値が設定できる
[1, 2, 3].fill(4, -3, -2);  // [4, 2, 3]

// 配列の長さと埋める値のみを指定する書き方
Array(3).fill(4); // [4, 4, 4]

要素の挿入も削除も自由自在な "splice"

  • Array.prototype.splice (start, deleteCount, ...items )
    • 配列の特定の位置から複数の要素を削除と挿入を同時に行える関数です。
    • 破壊的関数のため、元の配列が更新されます。
    • 戻り値は、削除された値で構成された配列です。
    • start:削除・挿入の開始位置
    • deleteCountstartの位置から削除する要素の個数(デフォルトは配列のlength)
    • ...itemsstartの位置から挿入する要素(可変長)
// 開始位置のみ指定
var array1 = [1, 2, 3];
var delArray1 = array1.splice(2);
array1; // [1, 2] 削除後の配列
delArray1; // [3] 削除された値で構成された配列

// 開始位置、削除個数、追加する要素を指定
var array2 = [1, 2, 3];
var delArray2 = array2.splice(1, 2, 3, 4, 5);
array2; // [1, 3, 4, 5]
delArray2; // [2, 3]

// 配列の一部を別の配列で置き換える
var array3 = [1, 2, 3, 4, 5,  6, 7, 8, 9, 10];
var replaceArray3 = [16, 17, 18, 19, 20];
var delArray3 = array3.splice(5, replaceArray3.length, ...replaceArray3);
array3; // [1, 2, 3, 4, 5, 16, 17, 18, 19, 20]
delArray3; // [6, 7, 8, 9, 10]

10. Well-known Symbolで参照される関数

ES7項番 関数 戻り値 バージョン
22.1.3.31 Array.prototype [ @@iterator ] ( ) Iterator ES6

最後はWell-known Symbolで参照される関数

  • 最後のグループは「Well-known Symbolで参照される関数」です。1つしかありません。
  • 関数自体の紹介の前に、Symbolについて先にご紹介したいと思います。

そもそもSymbolとは何か

  • SymbolとはES6から新たに追加されたプリミティブなデータ型で、値を生成する度に必ずユニークになるという特徴を持っています。
  • typeofの戻り値はsymbolとなる
// 値を生成
var sym1 = Symbol();
var sym2 = Symbol();

sym1 === sym2; // false

// Symbolの説明を引数に渡すこともできます
var sym3 = Symbol('これは3番目のSymbol');

typeof sym3; // symbol

Well-known Symbolとは何か

  • EcmaScriptの仕様の中で定義されており、用途が定められているSymbol
  • Well-known SymbolはSymbol.symbolNameという形で参照が可能
  • EcmaScript上では@@symbolNameと表記されるが、言語の文法上使える表現という訳ではない
  • 今回出て来るArray.prototype [ @@iterator ] ( )は、コード上ではArray.prototype[Symbol.iterator]()と表現される

Symbol.iteratorについて

  • Well-known Symbolの1つで、IterableなオブジェクトはSymbol.iteratorを必ずプロパティとして保持している
    • Symbol.iteratorプロパティはIteratorオブジェクトを返却する関数である
    • このIteratorオブジェクトはES6から導入されたfor 〜 of構文でのループで使用される
  • Array.prototype[@@iterator]()Array.prototype.value()と同じ関数を初期値として持つ
// iterator取得
var iterator = [1, 2][Symbol.iterator]();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

// Array.prototype[Symbol.iterator]を上書きすると for 〜 of 構文が動かなくなる
Array.prototype[Symbol.iterator] = function hoge(){};
for (let a of [1, 2, 3]) {
    console.log(a);
}

[余談] Well-known Symbolが必要な理由は"互換性"

  • Array.prototype [ @@iterator ] ( )Array.prototype.value()と同じ関数を指すが、for 〜 of構文で呼び出されます。
  • もし仮にArray.prototype.values()for 〜 of構文で呼び出す仕様にすると、Array.prototype.valuesというプロパティで配列を拡張しているコードがあった際に、for 〜 of構文そのものが動かなくなってしまいます。
  • 必ずユニークになるSymbol.iteratorというプロパティの関数を使うと、明示的に破壊する意図が無い限りはfor 〜 of構文は使い続けられます。
  • Symbolを理解するのにこちらの記事がとても参考になったので、是非読んでください。

qiita.com

[余談] Array.prototype.values()を実装するブラウザは非常に少ない

  • 前編 の「[余談] "values" の実装率が低い理由」の項でも言及した通り、Array.prototype.values()は実装率がとても低いです。
  • 一部のソフトウェアでの互換性が最も大きな理由ですが、Array.prototype [ @@iterator ] ( )があれば同じ関数を呼び出せる仕様のため、実装する必要も無いというのがもう一つ大きな理由なのでは無いかと思います。

参考ページ www.fxsitecompat.com

おわりに

2回に分けてJavaScriptの配列関数についてまとめてみました。

数が多すぎたり、情報を詰め込みすぎたりで読みにくい部分もあるかと思いますが、部分的にでも役に立つところがあれば幸いです。

最後まで読んでいただきありがとうございました。

インタープリズムの面々が、普段の業務に役立つ記事を丹精込めて書き上げる! Advent Calendar 2016 - Qiita6日目の記事

PAGE TOP