JavaScriptの勉強中:その7 Standard Library / Iterators and Generators

もう日課になってきた。

bufferings.hatenablog.com

## Ch. 11 The JavaScript Standard Library

  • JavaScript では Object を map や set のように使うこともできるし実際に使われてきた
  • けど、文字列に限定されるし "toString" みたいなプロパティもあってややこしい
  • なので ES6 では Set と Map クラスが導入された。

Set

  • 値の重複を持たないコレクション
  • Set は「順序を持たないコレクション」とよく言われるけど JavaScript の場合はちょっと違う。JavaScript の Set は要素が挿入された順番を覚えていて、iterate するのにその順番を使うから。

へー。日本語版の方には書いてないけど、英語版の方にはちゃんとそう書いてある:

Set - JavaScript | MDN

Set objects are collections of values. You can iterate through the elements of a set in insertion order. A value in the Set may only occur once; it is unique in the Set's collection.

  • 等価性のチェックには SameValueZero が使用される( https://tc39.es/ecma262/#sec-set-objects )。Array の includes() と同じだな。NaN もちゃんと判別されるってことね。
  • Set は iterable なので for/of ループや ... spread operator を使用することができる。
  • forEach() メソッドもある

Map

  • Map のキーの等価性チェックにも SameValueZero が使用される( https://tc39.es/ecma262/#sec-map-objects )
  • Map も iterable なので for/of ループや ... spread operator を使用することができる。
> let m = new Map([['x', 1], ['y', 2]]);
undefined

> m
Map(2) { 'x' => 1, 'y' => 2 }

> [...m]
[ [ 'x', 1 ], [ 'y', 2 ] ]

> [...m.keys()]
[ 'x', 'y' ]

> [...m.values()]
[ 1, 2 ]

> [...m.entries()]
[ [ 'x', 1 ], [ 'y', 2 ] ]

> for(let [key, value] of m) {
... console.log(`${ key }: ${ value }`);
... }
x: 1
y: 2
  • forEach() メソッドもある。ちょっと変だけど引数は (value, key) の順番になる:
matchMedia.forEach((value, key) => {
    // ...
});

配列を一般化したものとして考えてみると、インデックスが2番目の引数に来るのも分かる。

Error

  • JavaScript は何でも throw して catch することができる。プリミティブでも。(まじか・・・
  • でも普通は Error クラスかそのサブクラスを投げる
  • Error クラスは messagename の2つのプロパティと toString() メソッドを持っている
  • ES標準ではないけど、Nodeやブラウザでは stack プロパティがあって、スタックトレースを含む複数行文字列が格納される

URL

  • URL クラスはES標準ではないけど、IE以外のブラウザやNodeで利用できる
> new URL("https://example.com:8000/path/name?q=term#fragment");
URL {
  href: 'https://example.com:8000/path/name?q=term#fragment',
  origin: 'https://example.com:8000',
  protocol: 'https:',
  username: '',
  password: '',
  host: 'example.com:8000',
  hostname: 'example.com',
  port: '8000',
  pathname: '/path/name',
  search: '?q=term',
  searchParams: URLSearchParams { 'q' => 'term' },
  hash: '#fragment'
}

へー。便利そうだな。URL触りたいときには思い出そっと。

  • URL API が定義される前は escape() unescape()エスケープなどに使われていたけど非推奨になった。
  • その代わりに ES が導入したのは encodeURI() decodeURI()encoeURIComponent() decodeURIComponent()
  • これらのレガシー関数の問題は単一のエンコードを全体に適用するというところ。URLの部分によって異なるエンコードを用いている場合でも。
  • 解決策としては単純に URL クラスを使えばいい。

その他

WeakMap, WeakSet, Typed Arrays, Regular Expressions, Date, JSON, Intl, Console API とか色々あるけど、必要になったときに調べるんで良さそう。

## Ch. 12 Iterators and Generators

  • ES6 で Iterator が導入された
  • Iterable なオブジェクトは、 for/of ループで回したり、 spread operator で展開したり、 destructuring assignment で利用したりできる

Iterator がどうやって動くか

  1. Iterable なオブジェクトとは、特別な iterator メソッド( Symbol.iterator )を持っているオブジェクトのことで、そのメソッドは iterator オブジェクトを返す。
  2. Iterator とは next() メソッドを持っているオブジェクトのことで、そのメソッドは iteration result オブジェクトを返す。
  3. Iteration result オブジェクトは valuedone というプロパティを持ったオブジェクトのこと。

ループするときは iteration result オブジェクトの donetrue になるまで処理を続ける。頑張って書くとこういう感じ

let iterable = [1, 2, 3];
let iterator = iterable[Symbol.iterator]();
for (let result = iterator.next(); !result.done; result = iterator.next()) {
    console.log(result.value);
}
  • ビルトインの iterable なオブジェクトの Iterator オブジェクトは、それ自体も Iterable になっていて自分自身を返す。
  • つまり Iterator オブジェクト自身も Symbol.iterator メソッドを持っているということ
  • これは途中まで使った iterator を iterate したい場合に便利
let iterable = [1, 2, 3, 4, 5];
let iterator = iterable[Symbol.iterator]();
iterator.next();
iterator.next();

console.log([...iterator]); // => [3,4,5]

Iterable なオブジェクトを実装する

まずはジェネレーター関数を使わずにやってみる。

class MyIterable {
    [Symbol.iterator]() {
        let next = 0;
        let max = 10;
        return {
            next() {
                return (next <= max) ? { value: next++ } : { done: true };
            }
        }
    }
}


for (let x of new MyIterable()) console.log(x);
// 0から10までが出力された

動いたー。わーい。

便利なので iterator 自身も iterable にしておく:

class MyIterable {
    [Symbol.iterator]() {
        let next = 0;
        let max = 10;
        return {
            next() {
                return (next <= max) ? { value: next++ } : { done: true };
            },

            // 便利なので iterator 自身も iterable にしておく
            [Symbol.iterator]() { return this; }
        }
    }
}

ループの途中の breakreturn や例外などでイテレーションが中断される場合がある。その際に、ファイルやメモリーの開放のような後処理ができるように iterator には return() メソッドを実装できる。return() メソッドが存在する場合は、中断時に引数なしで呼び出される。return() はオブジェクトを返さなければならないが、そのオブジェクトは無視される。

class MyIterable {
    [Symbol.iterator]() {
        let next = 0;
        let max = 10;
        return {
            next() {
                return (next <= max) ? { value: next++ } : { done: true };
            },
            // iterator を iterable にしておくと便利
            [Symbol.iterator]() { return this; },
            // 後処理用(実際はこのクラスでは不要だけど)
            return() {
                console.log("return() が呼び出された");
                return {};
            }
        }
    }
}

let iterable = new MyIterable();

console.log("途中終了しない場合1");
for (let x of iterable){
    //
}

console.log("途中終了しない場合2");
[...iterable];

console.log("途中終了する場合1 break");
for (let x of iterable){
    break;
}

console.log("途中終了する場合2 return");
(function(){
    for (let x of iterable){
        return;
    }
}());

console.log("途中終了する場合3 例外");
try{
    for (let x of iterable){
        throw new Error();
    }
} catch {}

console.log("途中終了する場合3 destructuring assignment");
let [v1, v2] = iterable;

と、こんな感じで Iterable なオブジェクトを実装することができるけど、ちょっと面倒くさい。そこでジェネレーターの登場。

Generators

  1. ジェネレーター関数は function*アスタリスクをつけて定義する。
  2. ジェネレーター関数を呼び出すと、実際にはその関数のボディは実行されずに、ジェネレーターオブジェクトが返される。
  3. このジェネレーターオブジェクトは iterable になっている
  4. next() を呼び出すとジェネレーター関数のボディが呼び出されて yield ステートメントのところまで実行される。
  5. yield は ES6 で導入されたのだけど、この yield ステートメントで指定された値が next() の戻り値になる。

ほえー。

function* myGenerator(){
    yield 1;
    yield 2;
    yield 3;
    yield 4;
}

let generator = myGenerator();

// ジェネレーターオブジェクトは Iterator オブジェクトになってるので next() を呼ぶことができる
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
console.log(generator.next().value); // 4
console.log(generator.next().done); // true

// さらにジェネレーターはそれ自身が Iterable で自分自身を返す
console.log(generator === generator[Symbol.iterator]()); // true

// なのでジェネレーター自体を Iterable なオブジェクトとして扱うことができる
console.log([...myGenerator()]); // [ 1, 2, 3, 4 ]

落ち着いて考えないとこんがらがってくるー。

ジェネレーター関数の定義は関数宣言以外にもこんな感じでかける:

// 関数式
const myGenerator = function*(){
    yield 1;
    yield 2;
    yield 3;
    yield 4;
}

// メソッド
let obj = {
    *myGenerator(){
        yield 1;
        yield 2;
        yield 3;
        yield 4;
    }
}

ということで、さっきジェネレーターを使わずに頑張って書いてたやつは、こういう風に書ける

class MyIterable {
    *[Symbol.iterator]() {
        for (let v = 0; v <= 10; v++) {
            yield v;
        }
    }
}

おー( return() はどうなるんだろう?と思ったら後で出てきた↓)

yield*

ジェネレーターを使って Iterable をこんな風に扱いたい場合に:

function* sequence(...iterables) {
    for(let iterable of iterables) {
        for(let item of iterable) {
            yield item;
        }
    }
}

console.log([...sequence("abc",[1,2,3])]);
// [ 'a', 'b', 'c', 1, 2, 3 ]

yield* を使うとこう書ける:

function* sequence(...iterables) {
    for(let iterable of iterables) {
        yield* iterable;
    }
}
  • yield* は Iterable オブジェクトを指定すると、それぞれの結果を対象に yield できる

Advanced Generator Features

ジェネレーター関数は、イテレーターを作るときに使われるのがほとんどだけど、処理を途中で止めてあとで再開できるというのがそもそもの機能。なので、その辺をもうちょっと詳しく見ていく。

return

  • return を書くと、そこで処理が終わるので、途中で処理を終わらせたい場合に使用する。
  • return に値を指定すると donetrue なのに value にも値が指定されている状態になる。この値は for/of や spread operator では無視される。
function* myGenerator(){
    yield 1;
    yield 2;
    return 3;
}

let g = myGenerator();
console.log(g.next());
// => { value: 1, done: false }
console.log(g.next());
// => { value: 2, done: false }
console.log(g.next());
// => { value: 3, done: true }
console.log(g.next());
// => { value: undefined, done: true }

// return の value は無視される
console.log([...myGenerator()]);
// => [ 1, 2 ]

yield

  • yieldステートメントのように扱ってきたけど、実は式。値を持つことができる。
  • next() の引数に値を指定すると、それが yield 式の値になる。yield から再開するときにその値が取得できる。
function* myGenerator(){
    console.log("myGenerator start");

    let y1 = yield 1;
    console.log(`myGenerator y1=${ y1 }`);

    let y2 = yield 2;
    console.log(`myGenerator y2=${ y2 }`);

    let y3 = yield 3;
    console.log(`myGenerator y3=${ y3 }`);
}

let g = myGenerator();
console.log("main start");

let n1 = g.next("a");
console.log(`main n1.value=${ n1.value }`);

let n2 = g.next("b");
console.log(`main n2.value=${ n2.value }`);

let n3 = g.next("c");
console.log(`main n3.value=${ n3.value }`);

let n4 = g.next("d");
console.log(`main n4.value=${ n4.value }`);

結果はこうなった

main start
myGenerator start
main n1.value=1
myGenerator y1=b
main n2.value=2
myGenerator y2=c
main n3.value=3
myGenerator y3=d
main n4.value=undefined

最初に next() で使った "a" は受け取ることができないね。

return()

あ、さっき気になったやつだ。

  • ジェネレーターには return() メソッドが定義されている
  • ジェネレーターに対して独自の return() を実装することはできない。クリーンアップをしたい場合は try/finally を使う。
  • return() を呼び出すと、ジェネレーターはクリーンアップのコードを実行する

そっか finally を使えばいいのか。

function* myGenerator() {
    try {
        console.log("1の前");
        yield 1;
        console.log("2の前");
        yield 2;
        console.log("3の前");
        yield 3;
        console.log("3のあと");
    } finally {
        console.log("finallyが呼ばれたー");
    }
}

console.log("return() を直接呼び出し =======");
let g = myGenerator();
g.next();
g.next();
g.return();

console.log("destructuring assignment =======");
let [v1] = myGenerator();

console.log("for/ofをbreak =======");
for (let v of myGenerator()){
    break;
}

結果はこう:

return() を直接呼び出し =======
1の前
2の前
finallyが呼ばれたー
destructuring assignment =======
1の前
finallyが呼ばれたー
for/ofをbreak =======
1の前
finallyが呼ばれたー

throw()

  • throw() を呼び出すと、今ポーズしている yield のところで throw される
  • throw() の引数に値を渡すとジェネレーター内の try/catch でキャッチ可能

今日も面白かった。