反復可能オブジェクトは、配列の一般化です。これは、任意のオブジェクトをfor..of
ループで使用できるようにする概念です。
もちろん、配列は反復可能です。しかし、同様に反復可能な他の多くの組み込みオブジェクトがあります。例えば、文字列も反復可能です。
オブジェクトが技術的には配列ではないが、何かのコレクション(リスト、セット)を表す場合、for..of
はそれをループ処理するための優れた構文です。そのため、どのように動作させるかを見てみましょう。
Symbol.iterator
独自の反復可能オブジェクトを作成することで、反復可能オブジェクトの概念を簡単に理解できます。
たとえば、配列ではないが、for..of
に適したオブジェクトがあるとします。
数値の範囲を表すrange
オブジェクトなどです。
let range = {
from: 1,
to: 5
};
// We want the for..of to work:
// for(let num of range) ... num=1,2,3,4,5
range
オブジェクトを反復可能にする(そしてfor..of
を動作させる)には、オブジェクトにSymbol.iterator
という名前のメソッドを追加する必要があります(そのためだけに用意された特別な組み込みシンボルです)。
for..of
が開始すると、このメソッドを一度呼び出します(見つからない場合はエラーが発生します)。このメソッドは、イテレータを返す必要があります。イテレータとは、next
メソッドを持つオブジェクトのことです。- 以降、
for..of
は返されたオブジェクトのみを処理します。 for..of
が次の値を要求すると、そのオブジェクトでnext()
を呼び出します。next()
の結果は{done: Boolean, value: any}
という形式でなければなりません。ここで、done=true
はループが終了したことを意味し、そうでない場合はvalue
が次の値になります。
コメント付きのrange
の完全な実装を以下に示します。
let range = {
from: 1,
to: 5
};
// 1. call to for..of initially calls this
range[Symbol.iterator] = function() {
// ...it returns the iterator object:
// 2. Onward, for..of works only with the iterator object below, asking it for next values
return {
current: this.from,
last: this.to,
// 3. next() is called on each iteration by the for..of loop
next() {
// 4. it should return the value as an object {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// now it works!
for (let num of range) {
alert(num); // 1, then 2, 3, 4, 5
}
反復可能オブジェクトの中核となる機能、つまり懸念事項の分離に注意してください。
range
自体にはnext()
メソッドがありません。- 代わりに、
range[Symbol.iterator]()
の呼び出しによっていわゆる「イテレータ」オブジェクトが作成され、そのnext()
が反復のための値を生成します。
したがって、イテレータオブジェクトは、それが反復処理するオブジェクトとは別個です。
技術的には、これらをマージして、コードを簡素化するためにrange
自体をイテレータとして使用できます。
このように
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, then 2, 3, 4, 5
}
これでrange[Symbol.iterator]()
はrange
オブジェクト自体を返します。これは必要なnext()
メソッドを持ち、this.current
に現在の反復の進行状況を記憶しています。より短いですか?はい。そして、場合によってはそれも問題ありません。
欠点は、オブジェクトに対して2つのfor..of
ループを同時に実行できなくなることです。イテレータはオブジェクト自体のみであるため、それらは反復状態を共有します。しかし、非同期シナリオであっても、2つの並列for-ofはまれなことです。
無限イテレータも可能です。たとえば、range.to = Infinity
の場合、range
は無限になります。または、擬似乱数の無限シーケンスを生成する反復可能オブジェクトを作成することもできます。これも有用です。
next
に制限はありません。より多くの値を返すことができます。それは普通のことです。
もちろん、そのような反復可能オブジェクトに対するfor..of
ループは無限ループになります。しかし、break
を使用していつでも停止できます。
文字列は反復可能
配列と文字列は、最も広く使用されている組み込みの反復可能オブジェクトです。
文字列の場合、for..of
は文字をループ処理します。
for (let char of "test") {
// triggers 4 times: once for each character
alert( char ); // t, then e, then s, then t
}
そして、サロゲートペアでも正しく動作します!
let str = '𝒳😂';
for (let char of str) {
alert( char ); // 𝒳, and then 😂
}
イテレータの明示的な呼び出し
より深い理解のために、イテレータを明示的に使用する方法を見てみましょう。
for..of
とまったく同じ方法で文字列を反復処理しますが、直接呼び出しを行います。このコードは文字列イテレータを作成し、そこから「手動で」値を取得します。
let str = "Hello";
// does the same as
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // outputs characters one by one
}
これはめったに必要ありませんが、for..of
よりもプロセスをより詳細に制御できます。たとえば、反復プロセスを分割できます。少し反復処理してから停止し、別の処理を行い、後で再開します。
反復可能オブジェクトと配列様オブジェクト
2つの公式用語は似ていますが、大きく異なります。混乱を避けるために、それらをよく理解してください。
- 反復可能オブジェクトは、上記のように
Symbol.iterator
メソッドを実装するオブジェクトです。 - 配列様オブジェクトは、インデックスと
length
を持つオブジェクトであり、配列のように見えます。
ブラウザやその他の環境でJavaScriptを実用的なタスクに使用する場合、反復可能オブジェクトまたは配列様オブジェクト、あるいはその両方であるオブジェクトに出会うことがあります。
たとえば、文字列は反復可能(for..of
で動作する)であり、配列様(数値インデックスとlength
を持つ)でもあります。
しかし、反復可能オブジェクトは配列様オブジェクトではない場合があります。そしてその逆もまた然りです。
たとえば、上記の例にあるrange
は反復可能ですが、インデックス付きのプロパティとlength
がないため、配列様ではありません。
そして、配列様オブジェクトだが反復可能ではないオブジェクトを以下に示します。
let arrayLike = { // has indexes and length => array-like
0: "Hello",
1: "World",
length: 2
};
// Error (no Symbol.iterator)
for (let item of arrayLike) {}
反復可能オブジェクトと配列様オブジェクトは通常配列ではありません。push
、pop
などは持っていません。このようなオブジェクトがあり、配列のように操作したい場合、これは非常に不便です。たとえば、配列メソッドを使用してrange
を操作したいとします。どのようにすれば達成できますか?
Array.from
反復可能オブジェクトまたは配列様オブジェクトを受け取り、「実際の」Array
を作成する汎用メソッドArray.fromがあります。その後、その配列メソッドを呼び出すことができます。
たとえば
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World (method works)
(*)
行のArray.from
はオブジェクトを受け取り、それが反復可能オブジェクトまたは配列様オブジェクトかどうかを調べ、新しい配列を作成してすべてのアイテムをコピーします。
反復可能オブジェクトでも同じことが起こります。
// assuming that range is taken from the example above
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (array toString conversion works)
Array.from
の完全な構文では、オプションの「マッピング」関数も提供できます。
Array.from(obj[, mapFn, thisArg])
オプションの第2引数mapFn
は、配列に追加する前に各要素に適用される関数にすることができ、thisArg
を使用すると、そのthis
を設定できます。
たとえば
// assuming that range is taken from the example above
// square each number
let arr = Array.from(range, num => num * num);
alert(arr); // 1,4,9,16,25
ここでは、Array.from
を使用して文字列を文字の配列に変換しています。
let str = '𝒳😂';
// splits str into array of characters
let chars = Array.from(str);
alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2
str.split
とは異なり、文字列の反復可能な性質に依存しているため、for..of
と同様に、サロゲートペアでも正しく動作します。
技術的には、ここでは以下と同じことを行っています。
let str = '𝒳😂';
let chars = []; // Array.from internally does the same loop
for (let char of str) {
chars.push(char);
}
alert(chars);
…しかし、より短いです。
サロゲートペアに対応したslice
を構築することもできます。
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
let str = '𝒳😂𩷶';
alert( slice(str, 1, 3) ); // 😂𩷶
// the native method does not support surrogate pairs
alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs)
まとめ
for..of
で使用できるオブジェクトは、反復可能オブジェクトと呼ばれます。
- 技術的には、反復可能オブジェクトは
Symbol.iterator
という名前のメソッドを実装する必要があります。obj[Symbol.iterator]()
の結果は、イテレータと呼ばれます。これは、さらなる反復プロセスを処理します。- イテレータは
next()
という名前のメソッドを持っている必要があり、{done: Boolean, value: any}
というオブジェクトを返します。ここでdone:true
は反復プロセスの終了を示し、そうでない場合はvalue
が次の値です。
Symbol.iterator
メソッドはfor..of
によって自動的に呼び出されますが、直接行うこともできます。- 文字列や配列などの組み込みの反復可能オブジェクトも
Symbol.iterator
を実装しています。 - 文字列イテレータはサロゲートペアを認識しています。
インデックス付きのプロパティとlength
を持つオブジェクトは、配列様オブジェクトと呼ばれます。このようなオブジェクトは、他のプロパティやメソッドを持つ場合もありますが、配列の組み込みメソッドは欠けています。
仕様の中を見てみると、ほとんどの組み込みメソッドは、「実際の」配列ではなく、反復可能オブジェクトまたは配列様オブジェクトで動作することを前提としています。なぜなら、それはより抽象的だからです。
Array.from(obj[, mapFn, thisArg])
は、反復可能オブジェクトまたは配列様オブジェクトobj
から実際のArray
を作成し、その後、その配列メソッドを使用できます。オプションの引数mapFn
とthisArg
を使用すると、各アイテムに関数を適用できます。
コメント
<code>
タグを使用し、いくつかの行の場合は<pre>
タグで囲み、10行以上の場合にはサンドボックス(plnkr、jsbin、codepen…)を使用してください。