モダンJSの基礎を改めておさらいする【ES2015】
はじめに
今回は、モダンJSの基礎とも言える、ES2015を改めておさらいします。
ESとはECMAScriptのことで、JavaScriptの仕様を指します。
ES2015は2015年6月に公表され、ES5までのJavaScriptにおけるいくつかの問題点を解消しております。
現代のJSを用いる開発は、babelなどのトランスパイラなどを用いて最新の仕様を先取りすることが一般的となっています。
また、TypeScriptなどのaltJSも、ES2015がベースになってるものが多いです。
特別な理由がない限り、今からJSの学習を始める際はES2015からが良いかと思います。
▼ 目次
開発ツール
- Node.js
- npm
- babel
- ESLint
Node.js
サーバサイドでJavaScriptを実行するために用います。
npm
パッケージ管理ツール。Node.jsをインストールする際に導入されます。
Javaで言うならば、Mavenのようなものです。
bable
トランスパイラ。ES2015などの新しい仕様で記述されたJavaScriptを、ES5などの安定したバージョンに変換してくれます。
ESLint
構文解析ツール。Javaにおけるコンパイル時の構文チェックなどを行ってくれます。
ES2015で追加された構文
変数/定数宣言
let宣言
変数の宣言の際にはletを使うことができます。
let宣言された変数には、以下の特徴があります。
- 変数はブロックスコープとなる
- 同一スコープでの同一識別子での宣言は、エラーとなる
1 2 3 4 5 6 7 |
let currentTempC = 15; currentTempC // => 15 curerntTempC = 22.5; currentTempC // => 22.5; let currentTempC; // => SyntaxError: Identifier 'currentTempC' has already been declared |
const宣言
定数の宣言のためのconstキーワードが追加されました。
const宣言には、以下の特徴があります。
- 値の再代入はエラーとなる
- 同一スコープでの同一識別子での宣言はエラーとなる
定数宣言の際のコーディング規約は、識別子にアッパースネークケースを用いることです。
1 2 3 4 5 6 |
const MAX_TEMP_C = 36; MAX_TEMP_C // => 36 MAX_TEMP_C = 45; // => TypeError: Assignment to constant variable. const MAX_TEMP_C; // => SyntaxError: Identifier 'MAX_TEMP_C' has already been declared |
変数と定数の使い分け
基本的には、const宣言による定数を使うことが推奨されます。
どうしても値の再代入が必要な場合のみに限りlet宣言を使うことを心がけることで、より堅実なプログラミングができます。
ES2015はこのように堅実なプログラミングを助ける構文を多くサポートしていますので、活用しましょう。
識別子の慣習
JSには、他の多くの言語と異なり、変数の識別子に決まった慣習はありません。
しかし変数の識別子には、以下が多く使われます。
- ローワーキャメルケース
- ローワースネークケース
プロジェクトやチームでの開発の際には、これらを統一しましょう。
その他の慣習
- アッパーキャメルケース: クラス名
- アッパースネークケース: 定数名
- $で始める変数名: jQueryオブジェクト
- _で始める変数名: 内部変数(特別な場合を除き使わない)
日本語による識別子について
ES2015では、日本語の識別子が利用できます。
日本語話者による日本のみにおける開発では、使う場面もあるかも知れません。
1 2 3 |
let 夜ご飯 = 'オムライス' 夜ご飯 // => 'オムライス' |
ブロックスコープ
ES2015以前では、変数の宣言の方法はvarをつけるか、つけないかのどちらかでした。
そしてその方法で変数を宣言すると、スコープはvarがついているかどうかと、宣言された場所によって異なるスコープをとりました。
ES2015以降では、宣言の際にletまたはconstが利用でき、これらで変数や定数の宣言を行うと、ブロックスコープにできます。
以下が、ES2015以降のスコープの一覧です。
- グローバルスコープ: varをつけない宣言またはトップレベルでの宣言
- 関数スコープ: 関数内でvarをつけた宣言
- ブロックスコープ: letやconstで宣言
ブロックスコープとは、宣言が行われたブロック内で有効なスコープです。
1 2 3 4 5 6 7 8 9 |
console.log('before block'); { console.log('in block'); const x = 3; console.log(x); } console.log('after block'); console.log(x); // => reference error |
varとlet, constの使い分け
ES2015以降では、メリットがないためにvarによる変数宣言は行わないません。
ES2015を使う際には、letとconstを用いるべきです。
リテラル
テンプレートリテラル
テンプレートリテラルは任意の式の値を文字列に含めることができるリテラルです。
単純な値だけでなく、評価結果として値を返す式を含めることもできます。
1 2 3 4 5 6 7 8 9 |
const name = 'k4h4shi'; //以下は my name is k4h4shi.と出力される console.log(`my name is ${name}.`) const roomTempC = 26.5; let currentTempC = 34.5; //以下は 室温と気温の差 : 8度 と出力される console.log(`室温と気温の差 : ${currentTempC - roomTempC}度`) |
プリミティブ型
ES2015では、プリミティブ型にシンボル型が追加されました。
シンボル
シンボルはユニークであり、他のシンボルと同じになることはありません。
この点では、シンボルはオブジェクトと似ています。
ただし、シンボルはプリミティブであり、リテラル表記を持ちません。
シンボルはSymbol()コンストラクタを使って生成します。
生成の際は、必要に応じて説明のための文字列を渡すことができます。
1 2 3 4 5 6 7 8 9 |
const RED = Symbol(); const BLUE = Symbol(); const ORANGE = Symbol("夕日の色"); RED // => Symbol() ORANGE // => Symbol(夕日の色) RED === BLUE // => false RED === ORANGE // => false |
プロパティのkeyとしてのシンボル
シンボルをプロパティのkeyとして扱った場合には、少し特殊な挙動をします。
- シンボルのプロパティには、.演算子によるアクセスができない
- シンボルのプロパティは、列挙されない
- シンボルのプロパティのキーは文字列とは異なる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
const person = {}; const NAME = Symbol(); person[NAME] = 'k4h4shi'; person.age = 23; person.profession = 'Programmer'; person.NAME // => undefined person[NAME] // => 'k4h4shi' for (let key in person) { console.log(key + ':' + person[key]); } // 以下のように出力される。 // age: 23 // profession: Programmer person.NAME = 'kotaro'; person.NAME // => kotaro person["NAME"] // => kotaro person[NAME] // => k4h4shi for (let key in person) { console.log(key + ':' + person[key]); } // 以下のように出力される。 // age: 23 // profession: Programmer // NAME: kotaro |
コンテナ
オブジェクトや配列はコンテナと呼ばれます。
これは構造的なデータであることを意味し、複数の値を格納しているという意味です。
デストラクチャリング
コンテナは構造を持っているため、それを分解することができます。
分割代入と呼ばれる機能で、オブジェクトや配列といった構造的なデータを複数の変数に分割できます。
オブジェクトの分割代入
オブジェクトの分割代入をする際には、変数名をオブジェクトのプロパティ名と一致させます。
オブジェクトのプロパティに存在しないものには、undefinedが代入されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const obj = { b: 2, c: 3, d: 4 } let {a, b, c } = obj; // 分割代入 a // => undefined b // => 2 c // => 3 const other = { b: 20, c: 30, d: 40 } ({a, b, c} = other); // 宣言済みの変数への分割代入には、()演算子を用いる a // => undefined b // => 2 c // => 3 d // => ReferenceError |
配列の分割代入
配列の分割代入の際には、配列の要素に対応して順番に代入できます。
また展開演算子を使うことで、残りの要素を全て新しい配列に代入することもできます。
1 2 3 4 5 6 7 8 9 10 |
const array = [1, 2, 3, 4]; let [x, y] = array; x // => 1 y // => 2 let [a, b, ...rest] = array; a // => 1 b, // => 2 rest // => [3, 4] |
引数への分割代入
あらかじめ引数をブロック内に列挙しておき、そこへ同様の識別子のプロパティを持つオブジェクトを渡すことで、引数に対して分割代入をしつつ関数が呼出せます。
1 2 3 4 5 6 7 8 9 10 11 12 |
function getSententce({ subject, verb, object }) { return `${subject} ${verb} ${object}`; } const o = { subject: "I", verb: "love", object: "JavaScript" } console.log(getSentence(o)); |
引数の展開演算子
可変長の引数を持つ関数を定義したい場合には、展開演算子を用いることができます。
これによって、複数の引数を一つの識別子に紐付けて配列のような形で渡すことができます。
1 2 3 4 5 6 7 |
function addPrefix(prefix, ...words) { const prefixedWords = []; for(let i = 0; i '5 - 6 - 7' f(5, 6); // => '5 - 6 - 3' f(5); // => '5 - default - 3' f(); // => 'undefind - default - 3' |
メソッドの省略記法
オブジェクトのプロパティとなっている関数を便宜上メソッドと呼びます。
メソッドは、省略記法で記述することができます
1 2 3 4 5 6 7 |
const o = { name: 'Wallace', bark() { return 'うーうー'; } } o // => { name: 'Wallace', bark: [Function: bark] } o.bark() // => 'うーうー' |
アロー関数
アロー関数を使うと、次の三つの点で関数の定義を簡略化できます。
- functionという単語を省略できる
- 引数が一つならば()を省略できる
- 関数本体が一つの式からなる場合、()とreturnを省略できる
アロー関数は無名関数になるため、変数に代入することは可能ですが、名前のついた関数を作成することはできません。
以下に、アロー関数による定義と同様の挙動の定義を列挙します。
1 2 3 4 5 6 7 8 9 10 11 12 |
// functionという単語を省略できる const f1 = function() { return "hello!" } const f1 = () => "hello"; // 引数が一つならば()を省略できる const f2 = function(name) { return `hello ${name}!`;) const f2 = name => `hello ${name}!`; // 関数本体が一つの式からなる場合、()とreturnを省略できる const f3 = function(a, b) { return a + b; } const f3 = (a, b) => a + b; |
#####アロー関数の利点
名前付きの関数が必要な場合、単純に関数を宣言すれば良いため、アロー関数を使う必然性はありません。
アロー関数がよく用いられるのは、無名関数を引数に渡したりする場合です。
また、アロー関数には通常の関数と大きく異なる点があります。
それはthisが他の関数と同様に、語彙的に束縛されるという点です。
以下の例をみてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const o = { name: 'k4h4shi', greetBackwards: function() { const getReverseName = () => { this; // => { name: 'k4h4shi', greetBackwards: [Function: greetBackwards] let nameBackwards = ''; for(let i = this.name.length - 1; i >= 0; i--) { nameBackwards += this.name[i]; } return nameBackwards; } return `${getReverseName()} si eman ym ,olleH`; } } o.greetBackwards(); // => 'ihs4h4k si eman ym ,olleH' |
o.greetBackwards()通常の関数で宣言していた場合、getReserseName の this は getReserseName 自身を指すために、this.nameにてType errorが発生します。
しかし、o.greetBackwardsをアロー関数で宣言するとthisがオブジェクトoを指すために、nameプロパティの値を取得できます。
これによって、以前ならばあらかじめthisをselfなどの変数に格納していた場面で、その必要がなくなります。
アロー関数には、もう2点通常の関数とは異なる点があります。
それは以下の2点です。
- コンストラクタとして使うことはできない
- 変数argumentsが使えない
オブジェクト指向
クラスの定義
ES2015以降で導入された構文を用いることで簡単にクラスを作成できるようになりました。
ES5以前の関数によるクラスの定義と内部的には変わりませんが、その他のオブジェクト指向言語の経験がある方にも馴染みのある糖衣構文が用意されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Car { constructor(maker, model) { this.maker = maker; this.model = model; this.gears = ['P', 'N', 'R', 'D']; this.gear = this.gears[0]; } shift(gear) { if(this.gears.indexOf(gear) Car { maker: 'Tesla', model: 'Model S', gears: [ 'P', 'N', 'R', 'D' ], gear: 'P' } */ |
アクセッサプロパティ
上記の例では、gearプロパティの値がshiftメソッドを介さないで変更されたすることで、不正な値を設定することができてしまいます。
JavaScriptにはアクセス制御のような機構がないのですが、この弱点を補うアクセッサプロパティが用意されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Car { constructor(maker, model) { this.maker = maker; this.model = model; this._gears = ['P', 'N', 'R', 'D']; this._gear = this.gears[0]; } get gear() { return this._gear; } set gear(value) { if(this.gears.indexOf(value) Car { maker: 'Tesla', model: 'Model S', gears: [ 'P', 'N', 'R', 'D' ], gear: 'P' } */ car.gear = 'X'; // Error: ギア指定が正しくありません: X |
これでもまだ、car._gear = 'X'
というように不正な値は設定可能ですが、Lintなどを用いることで十分防げるようになります。
静的メソッド
静的メソッドはクラスメソッドのことです。
インスタンスに付随するインスタンスメソッドに対し、クラスに付随し、クラスに関係する処理を実装する際などに用います。(インスタンスごとに一意なIDをふり分ける等)
以下がサンプルコードになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class Car { static getNextId() { return Car.id++; } constructor(maker, model) { this.maker = maker; this.model = model; this.id = Car.getNextId(); } static areSimilar(c1, c2) { return c1.maker === c2.maker && c1.model === c2.model; } static areSame(c1, c2) { return c1.id === c2.id; } } Car.id = 0; const car1 = new Car("Tesla", "Model S"); const car2 = new Car("Mazda", "3i"); const car3 = new Car("Mazda", "3i"); car1.id; // => 0 car2.id; // => 1 car3.id; // => 2 Car.areSimilar(car1, car1); // => true Car.areSimilar(car1, car2); // => false Car.areSimilar(car2, car3); // => true Car.areSame(car1, car1); // => true Car.areSame(car1, car2); // => false Car.areSame(car2, car3); // => false |
継承
extendsキーワードを用いて、クラスの継承関係を簡潔に表現することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
class Vehicle { constructor() { this.passengers = []; console.log('Vehicle created.') } addPassenger(p) { this.passengers.push(p); } } class Car extends Vehicle { constructor() { super(); console.log('Car created.') } deployAirBags() { console.log('bang!') } } const v = new Vehicle(); // => Vehicle created. v.addPassenger('k4h4shi'); v.addPassenger('kotaro'); console.log(v.passengers); // => [ 'k4h4shi', 'kotaro' ] const c = new Car(); // => Vehicle created.¥nCar created. c.addPassenger('kozaburo'); c.addPassenger('koshiro'); console.log(c.passengers); // => [ 'kozaburo', 'koshiro' ] c.deployAirBags(); // => 'Bang!' class Motorcycle extends Vehicle {} const m = new Motorcycle(); c instanceof Car // => true c instanceof Vehicle // => true m instanceof Car // => false m instanceof Vehicle // => true m instanceof Motorcycle // => true v instanceof Car // => false |
mapとset
ES2015から、MapオブジェクトとSetオブジェクトが追加されました。
Mapはオブジェクトと似ており、keyとvalueのペア(entry)を保持する構造体です。
Setは配列と似ていますが、重複した値は登録できません。
これは意外ですが、JavaScriptのMapとSetは順序を保持しており、それは登録順です。
map
JavaScriptでは、連想配列としてのオブジェクトをMapとして利用していましたが、追加されたMapには連想配列としてのオブジェクトに比べ、以下の利点があります。
- 文字列またはシンボル以外のキーを持てる
- キーと値の組(エントリ)の数を取得できる
- プロトタイプによる意図しないマッピングが生じない
mapのプロパティ
- size: 要素数を表すプロパティ
- set(key, value): keyとvalueの組(entry)を追加する
- get(key):keyに紐づくvalueを取得する。紐づく値が存在しない場合、undefinedを返す
- has(key):keyに対応するentryが存在するかを返す
- delete(key):keyに対応するentryを削除する
- clear():全てのentryを削除する
- keys(): 全てのkeyを格納したMapIteratorを返す
- values():全てのvalueを格納したMapIteratorを返す
- entries():全てのentryを格納したMapIteratorを返す
以下がサンプルコードになります
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
const u1 = { name: '太郎' }; const u2 = { name: '花子' }; const u3 = { name: '一郎' }; const u4 = { name: '二郎' }; const userRoles = new Map(); userRoles .set(u1, 'ユーザー') .set(u2, '管理者') .set(u3, 'ユーザー'); userRoles.size; // => 3 userRoles.get(u1); // => 'ユーザー' userRoles.get(u2); // => '管理者' userRoles.get(u4); // => undefined userRoles.has(u1); // => true userRoles.has(u4); // => false userRoles.set(u1, '管理者'); userRoles.get(u1) // => '管理者' userRoles.delete(u3); userRoles.size; // => 2 userRoles.clear(); userRoles.size; // => 0 |
MapにはMapIteratorを返すメソッドがいくつか用意されています。
MapIteratorは、for...of
ループで利用可能なイテレーション可能なオブジェクトです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
const u1 = { name: '太郎' }; const u2 = { name: '花子' }; const u3 = { name: '一郎' }; const u4 = { name: '二郎' }; const userRoles = new Map(); userRoles .set(u1, 'ユーザー') .set(u2, '管理者') .set(u3, 'ユーザー'); for (let u of userRoles.keys()) { console.log(u.name); // => 太郎¥n花子¥n一郎 } for (let r of userRoles.values()) { console.log(r); // => ユーザー¥n管理者¥nユーザー } for (let [u, r] userRoles.entries()) { console.log(`${u.name}: ${r}`); // => 太郎: ユーザー¥n花子: 管理者¥n一郎: ユーザー } // entriesはMapのデフォルトイテレータのため、省略可能 for (let [u, r] userRoles) { // ... } // 展開演算子を使って、MapIteratorから配列が取得できる const users = [...userRoles.keys()]; users.forEach(u => console.log(u);); // => 太郎¥n花子¥一郎 |
WeakMap
WeakMapはMapと似ていますが、以下の点が異なります
- キーはオブジェクトでなければならない
- キーがガベージコレクションの対象となる
- イテレーションやクリアができない
ウィークマップはオブジェクトのインスタンスに対し、外から操作不可能なプライベートなキーを保管するのに利用できます。
例えば、IIFE(immediately invoked function expression)と併用して、以下のように実装できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const SecretContainer = (function() { const secrets = new WeakMap(); return class { setSecret(secret) { secrets.set(this, secret); } getSecret() { return secrets.get(this); } } })(); const a = new SecretContainer(); a.setSecret('secret'); a.getSecret(); // => secret a.secrets // => undefined |
上記のような場合にマップを用いると、その他のオブジェクトの参照が全て無くなってもガベージコレクションが行われないので注意してください。
Set
セットはデータの集合で、重複は許されません。
この点では数学的な意味での集合と同じですが、JavaScriptの集合は追加された順と言う順序性を持ちます。
Setのプロパティ
- size: 要素数を表す
- add(value): 要素を追加する。すでに要素が存在する場合は何もしない。返り値はセット自身。
- has(value): 要素が存在するかどうかを返す。
- delete(value):要素を削除する。返り値は要素が存在したかどうか。
- clear(): 要素を全て削除する。
- values(): 全ての値を格納したSetIteratorを返す。
- keys(): valuesと同様
- entries(): [値, 値]を格納したSetIteratorを返す。
- forEach(callbackFn, [thisArg]): 配列のforEachと同様。順序は追加順。
Setのプロパティには、keysやentriesも存在しますが、valuesを使うことをお勧めします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const roles = new Set(); roles.add('ユーザー'); // => Set { 'ユーザー' } roles.size; // => 1 roles.has('ユーザー'); // => true roles.add('管理者'); // => Set { 'ユーザー', '管理者' } roles.size; // => 2 roles.add('ユーザー'); // => Set { 'ユーザー', '管理者' } roles.size; // => 2 roles.delete('ユーザー'); // => true roles.size; // => 1 roles.has('ユーザー') // => false roles.delete('ユーザー’); // => false |
WeakSet
WeakSetはオブジェクトだけを含むことができるセットで、オブジェクトがガベージコレクションの対象になる可能性があります。
これはWeakMapと同様の特性で、ウィークセットも同様にイテレーションできません。
このため、ウィークセットを利用できる場面はかなり限られてきます。
ウィークセットが活用できるほぼ唯一の場面と思われるのは、指定のオブジェクトがあるセットにあるかどうかを決定する場合です。
例えば、正直者は褒美をもらえますが、嘘つきはもらえない。と言うことを表現するために、正直者のWeakSetを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const honest = new WeakSet(); const people = [ { name: '嘘つき太郎' }, { name: '正直太郎' } ]; honest.add(people[1]); for (let person of people) { if (honest.has(person)) { console.log(`正直者の${person.name}には褒美をやろう`); } else { console.log(`嘘つきの${person.name}には褒美はやらん`); } } /* 実行結果 * 正直者の正直太郎には褒美をやろう * 嘘つきの嘘つき太郎には褒美はやらん * / |
イテレータとジェネレータ
ES2015から、イテレータとジェネレータと呼ばれる2つの概念が導入されました。
イテレータ
イテレータ(反復子)はその名前の通り、繰り返しのための機構です。
イテレータという言葉に関連して、反復するオブジェクトのことをイテレータオブジェクト(iterator object)、反復可能なオブジェクトのことをiterable objectと呼びます。
この2つは相反するものではなく、「反復可能かつiteratorであるオブジェクト」も存在します。
通常は、イテレータオブジェクトを指して、イテレータと呼びます。
配列とイテレータ
まずは、代表的な反復可能なオブジェクトである配列を見ていきます。
配列にfor…of文を用いることで要素を列挙できますが、これは配列が反復可能なオブジェクトだからです。
つまり、反復可能なオブジェクトにfor…of文を用いることで各要素に対して処理を行うことができます。
その他の反復可能なオブジェクトには、文字列、マップ、セットなどがあります。
配列に対して、for…of文を用いた場合は、以下のようになります。
1 2 3 4 5 |
const array = ['i', 't', 'e', 'r', 'a', 'b', 'l', 'e']; for (let e of array) { console.log(e) // i, t, e, r, a, b, l, eを順に出力する } |
配列からイテレータへの変換
配列は反復可能なオブジェクト(itarable object)ですが、イテレータではありません。
イテレータは、そのメソッドnextを呼び出すことで次々と要素をとり出せるオブジェクトのことを指します。
ES2015では、valuesというメソッドを使って配列を簡単にイテレータに変換することができます。
イテレータのnextメソッドを呼び出した際の返り値は、valueとdoneというプロパティを持つオブジェクトとなります。
valueが処理に利用するデータで、doneは全ての要素をiteratorが供給し終わったかどうかを判定するための真偽値です。
全ての要素を供給し終わった場合には、valueがundefined, doneがtrueとして返ります。
それ以外の場合には、valueには各要素が順に格納され、doneはfalseとなります。
イテレータを使って、for…of文と同様に各要素に対し順に処理を適用できます。
valuesが使えない場合
Firefox, Node.js, Chromeなどの環境では、valuesメソッドが使えない場合があります。
その場合には、Symbol.iteratorをプロパティ名に持つメソッドの呼び出しをすることで、valuesと同様にイテレータへの変換を行うことができます。
1 2 3 4 5 6 7 8 9 10 11 |
const array = ['i', 't', 'e', 'r', 'a', 't', 'o', 'r']; // const it = array.values(); const it = array[Symbol.iterator](); let current = it.next(); while(!current.done) { console.log(current.value); current = it.next(); } |
イテレータのプロトコル
イテレータのプロトコルを実装することで、オブジェクトを反復可能にすることができます。
オブジェクトを反復可能なオブジェクト(iterable object)にしたい場合には、Symbole.iteratorをプロパティ名とするメソッドを実装し、その返り値でイテレータを返すことで実装可能です。
例として、メッセージをタイムスタンプとともに記憶していくロギング用のクラスLogを反復可能なオブジェクトとして実装します。
Logクラスは反復可能なオブジェクトのため、for…of文で要素を順に走査することが可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Log { constructor() { this.messages = []; } add(message) { const now = Date.now(); console.log(`ログ追加: ${message} (${now})`); this.messages.push({message, timestamp: now}); } [Symbol.iterator]() { // return this.messages.value(); return this.messages[Symbol.iterator](); } } const log = new Log(); log.add('1つ目のメッセージ'); log.add('2つ目のメッセージ'); log.add('3つ目のメッセージ'); for (let entry of log) { const date = new Date(entry.timestamp); console.log(`${entry.message} (${date})`); } /* ログ追加: 1つ目のメッセージ (1503320134914) ログ追加: 2つ目のメッセージ (1503320134918) ログ追加: 3つ目のメッセージ (1503320134921) 1つ目のメッセージ (Mon Aug 21 2017 21:55:34 GMT+0900 (JST)) 2つ目のメッセージ (Mon Aug 21 2017 21:55:34 GMT+0900 (JST)) 3つ目のメッセージ (Mon Aug 21 2017 21:55:34 GMT+0900 (JST)) */ |
上記のLogクラスは、内部で配列を保持しているため、配列のvaluesまたは[Symbol.iterator]をメソッド呼び出しした結果を返すことで、簡単に反復可能オブジェクトとして実装可能です。
イテレータのプロトコルを独自に実装する
Logクラスの例では、valuesまたは[Symbol.iterator]を使ってイテレータを得ることでiteratorのプロトコルを実装していますが、独自のイテレータを書くこともできます。
1 2 3 4 5 6 7 8 9 |
[Symbol.iterator]() { let i = 0; const messages = this.messages; return { next: () => i >= messages.length ? {value: undefined, done: true} : {value: messages[i++], done: false} } } |
イテレータと反復可能オブジェクトのまとめ
- イテレータはメソッドnextを実装している必要があります。
- 反復可能オブジェクトは[Symbol.iterator]を実装している必要があります。また[Symbol.iterator]は、イテレータを返す必要があります。
- 反復可能オブジェクトはfor…of文を用いて要素を順に全て走査し、処理を適用することができます。
- 配列、文字列、マップ、セットなどは既に反復可能オブジェクトであるため、これらに対してfor…of文を用いることとができます。
無限の値を供給するイテレータ
イテレータは必ずしも有限個の値を共有する必要はなく、メソッドnextを実装していれば良いため、無限この値を供給するイテレータも実装可能です。
例えばフィボナッチ数に終わりはないため、フィボナッチ数を供給するイテレータは必然的に無限に値を生成します。
以下にフィボナッチ数を生成するイテレータの例を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class FibonacciSequence { [Symbol.iterator]() { let a = 0, b = 1; return { next() { let rval = { value: b, done: false }; b += a; a = rval.value; return rval; } } } } const fib = new FibonacciSequence(); let i = 0; for (let n of fib) { console.log(`${i + 1}: ${n}`); if (++i > 99) break; } |
FibonacciSequenceのインスタンスをfor…ofループで使ってしまうと、無限ループになってしまうため、明示的に終了条件を指定してbreakする必要があります。
ジェネレータ
ジェネレータは関数の一種と考えることができますが、動作が少し異なります。
ジェネレータと関数の違いは次の2点です。
– 関数は制御(と値)を任意の場所から呼び出し側に戻すことができる
– ジェネレータを呼び出す時はすぐには実行されず、まずイテレータが戻される。そのあとで、イテレータのメソッドnextを呼び出すたびに実行が進む
ジェネレータを定義する場合、キーワードfunctionのあとに*がつきます。
それ以外は、関数と同じ構文を使いますが、ジェネレータの場合、呼び出し側に値を返すためにはキーワードyieldが使われます。
以下は、簡単なジェネレータの例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
function* rainbow() { yield '赤'; yield '橙'; yield '黄'; yield '緑'; yield '青'; yield '水色'; yield '紫'; } console.log('while文による走査:'); const it = rainbow(); let current = it.next(); while(!current.done) { console.log(current.value); current = it.next(); } console.log('for文による走査:'); for (let color of rainbow()) { console.log(color); } /* while文による走査: 赤 橙 黄 緑 青 水色 紫 for文による走査: 赤 橙 黄 緑 青 水色 紫 */ |
ジェネレータを呼び出すとイテレータが返りますので、nextを呼び出して戻ってくる値を順に処理することができます。
また、ジェネレータはイテレータを戻すため、for…ofで利用することができます。
yield式と双方向
ジェネレータを用いることで、呼び出し側との間での双方向コミュニケーションが可能になります。
この際には、yield式を使います。
yieldは評価の結果、next呼び出し時の引数の値になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function* interogate() { const name = yield "お名前は?"; const color = yield "お好きな色はなんですか?"; return `${name}さんの好きな色は${color}だそうですよ`; } const it = interogate(); console.log(it.next()); // { value: 'お名前は?', done: false } console.log(it.next('k4h4shi')); // { value: 'お好きな色はなんですか?', done: false } console.log(it.next('赤')); // { value: 'k4h4shiさんの好きな色は赤だそうですよ', done: true } |
なお、ジェネレータをアロー関数で書くことはできません。必ずfunction*を使う必要があります。
まとめ
イテレータを用いることで、セットやオブジェクトが複数の値を供給するものである場合、for…ofのような標準的な手法を用いて処理を行うことができます。
ジェネレータを利用することでより柔軟な関数が実現できます。
ジェネレータは、遅延評価などを可能にし、必要になるまで計算をしないでおく、と言った処理を可能にしてくれます。