もう日課になってきた。
## Ch. 11 The JavaScript Standard Library
- JavaScript では Object を map や set のように使うこともできるし実際に使われてきた
- けど、文字列に限定されるし "toString" みたいなプロパティもあってややこしい
- なので ES6 では Set と Map クラスが導入された。
Set
- 値の重複を持たないコレクション
- Set は「順序を持たないコレクション」とよく言われるけど JavaScript の場合はちょっと違う。JavaScript の Set は要素が挿入された順番を覚えていて、iterate するのにその順番を使うから。
へー。日本語版の方には書いてないけど、英語版の方にはちゃんとそう書いてある:
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 クラスは
message
とname
の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 がどうやって動くか
- Iterable なオブジェクトとは、特別な iterator メソッド(
Symbol.iterator
)を持っているオブジェクトのことで、そのメソッドは iterator オブジェクトを返す。 - Iterator とは
next()
メソッドを持っているオブジェクトのことで、そのメソッドは iteration result オブジェクトを返す。 - Iteration result オブジェクトは
value
とdone
というプロパティを持ったオブジェクトのこと。
ループするときは iteration result オブジェクトの done
が true
になるまで処理を続ける。頑張って書くとこういう感じ
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; } } } }
ループの途中の break
や return
や例外などでイテレーションが中断される場合がある。その際に、ファイルやメモリーの開放のような後処理ができるように 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
- ジェネレーター関数は
function*
とアスタリスクをつけて定義する。 - ジェネレーター関数を呼び出すと、実際にはその関数のボディは実行されずに、ジェネレーターオブジェクトが返される。
- このジェネレーターオブジェクトは iterable になっている
next()
を呼び出すとジェネレーター関数のボディが呼び出されてyield
ステートメントのところまで実行される。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
に値を指定するとdone
がtrue
なのに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
でキャッチ可能
今日も面白かった。