※スマホ対応はしてません。

カテゴリー: JavaScript

ジェネレータと自作イテレータで各種オブジェクトもぐーるぐる。(現代的JavaScriptおれおれアドベントカレンダー2017 – 23日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 23日目

概要

function* で、イテレータを返すジェネレータ関数を作成できます。中で yield が使えます。

function* createIdGenerator() {
  let currentId = 100;
  while (true) {
    yield currentId++;
  }
}

const iterator = createIdGenerator();
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);

これはオブジェクトを反復可能(イテラブル)にするのに便利です。

const obj = {
  *[Symbol.iterator]() {
    const max = 3;
    for (let i = 0; i < max; i++) {
      yield i;
    }
  }
};

for (let item of obj) {
  console.log(item);
}

イテレータ

著名なデザインパターンのひとつです。配列でないオブジェクトでも、共通のインターフェイスで反復処理を行えるようにします。

JavaScript (ES2015+) では(たぶん一般的な定義とは異なり)イテレータは next() メソッドを持つオブジェクトです。具体的なインターフェイスは後述します。

JavaScriptでのイテレータ

イテラブルなオブジェクトで [Symbol.iterator]() というすごい名前のメソッドを実行すると、イテレータを得られます。

const iterable = new Set([100, 200, 300]);

// イテレータ生成
const it = iterable[Symbol.iterator]();

while (true) {
  // 反復処理を進め、現段階の状態を得る
  const result = it.next();

  // 繰り返し条件を確認
  if (result.done) {
    break;
  }
  
  // 項目取得
  const item = result.value;

  console.log(item);
}

Set オブジェクトはイテラブルなオブジェクトなので、 [Symbol.iterator]() を実行するとイテレータ it を生成して返してくれます。

そのイテレータ it は next() メソッドを持ちます。このメソッド呼び出しを繰り返して反復 (iterate) していきます。

next() メソッドは実行するたびに、なんだろ、結果オブジェクト? result を生成して返します。結果オブジェクトって呼び方で良いかな。 IteratorResult という名前のインターフェイス(後述)を満たすオブジェクトです。

この結果オブジェクトはまた、二つのプロパティを持ちます。 value が反復途中の現段階の値で、 done がもう反復し終えたかが格納される真偽値です。

これらを使って配列でも何でもぐるぐるできます。

for-of で使う

オブジェクトがイテラブルである、つまり前述の各要素を満たす場合、 for-of を使って簡単に反復処理を実現できます。

const iterable = new Set([100, 200, 300]);

for (let item of iterable) {
  console.log(item);
}

わあ短い。

ジェネレータで生成する

[Symbol.iterator]() をちゃんと自作すればどんなオブジェクトでもイテラブルにできます。

next() メソッドを自前で実装しても良いんだけど、面倒なので、ジェネレータ関数というのを使って作ると簡単です。

ジェネレータ

function* を使ってジェネレータ関数を宣言できます。ジェネレータ関数を実行するとジェネレータオブジェクトを得られます。

function* createGenerator() {
  const values = [100, 200, 300];

  for (let i = 0; i < values.length; i++) {
    const item = values[i];
    yield item;
  }
}

const generator = createGenerator();

ジェネレータオブジェクトは加えてイテラブルでもあるで for-of で使えます。わーい。

for (let item of generator) {
  console.log(item);
}

generator[Symbol.iterator]() の結果は元のオブジェクト generator になります。

また同時にイテレータでもあるので next() を持ちます。コードは書かなくても良いかね。

yield

さらっと使ったけど、ジェネレータ関数の中では yield という式を使えます。この式は右辺を value 、また done: false という設定で、 next() で返す IteratorResult オブジェクトを生成します。

必ずしも for 文で使う必要はないです。

function* oneTwoThree() {
  yield 1;
  yield 2;
  yield 3;
}

const it = oneTwoThree();

let result;
result = it.next();
console.log(result.value);  // => 1
result = it.next();
console.log(result.value);  // => 2
result = it.next();
console.log(result.value);  // => 3

[Symbol.iterator]() を実装する

いよいよ [Symbol.iterator]() です。 * を付けて、ジェネレータ関数というかジェネレータメソッドになります。

const obj = {
  *[Symbol.iterator]() {
    const max = 3;
    for (let i = 0; i < max; i++) {
      yield i;
    }
  }
};

for (let item of obj) {
  console.log(item);
}

他のイテレータ生成メソッドを作る

obj じゃなくて obj.iterator() のようにメソッドを呼んでやる必要があるけれど、他の名前でも大丈夫。

目的別に何通りか用意しても良いかもね。

const obj = {
  *iterator() {
    const max = 3;
    for (let i = 0; i < max; i++) {
      yield i;
    }
  }
};

for (let item of obj.iterator()) {
  console.log(item);
}

イテレータのインターフェイス

いっぱい出てきたのでまとめ。

  • 反復可能(イテラブル)なオブジェクト … [Symbol.iterator]() をもつオブジェクト( Iterable )
  • [Symbol.iterator]() … イテレータを返すメソッド
  • イテレータ … next() をもつオブジェクト( Iterator )
  • next()value 、 done を持つオブジェクト( IteratorResult )を返し、内部状態を進めるメソッド
  • value … 反復処理各段階における値
  • done … 反復処理が終了しているかどうか

[Symbol.iterator]()

Symbol.iterator というシンボルが、名前というか何だろ、識別子?になってるメソッドです。 [] は動的にプロパティ名を決めるやつです。別稿参照。あとシンボル Symbol についても。

なんならどこかで Symbol.iterator = 'iterate' とか定義されてると考えてください。

Iterable インターフェイス

オブジェクトがこのインターフェイスを満たしていると、 for-of とか ... とかが使えます。

  • [Symbol.iterator]() … イテレータオブジェクトを返す

Iterator インターフェイス

このインターフェイスを満たすオブジェクトをイテレータオブジェクトと呼びます。

  • next() … イテレータオブジェクトを返す
  • return() (optional) … イテレータオブジェクトを返す
  • throw() (optional) … イテレータオブジェクトを返す

return() を呼び出すと「もう終了するぞ」と、 throw() を呼び出すと「なんかおかしいぞ」を、呼び出し先となるオブジェクトに伝えることが、できる、そう、です。単純に配列みたいな情報を扱う上ではいらなさそうだけど、何か外部から情報を引っ張ってくるようなやつで使うときに便利なんだろか。わかんない。

IteratorResult インターフェイス

next() 他のメソッドはこのインターフェイスを満たすオブジェクトを返す必要があります。

  • done
  • value

分割代入やスプレッド演算子 ...

これら実はイテラブルオブジェクトが対象です。配列じゃなくても自作のオブジェクトでも、 [Symbol.iterator]() を備えていれば使えます。

function* oneTwoThree() {
  yield 1;
  yield 2;
  yield 3;
}

// 分割代入
const [one, two, three] = oneTwoThree();
console.log(one);
console.log(two);
console.log(three);

// スプレッド演算子
function say(one, two, three) {
  console.log(one);
  console.log(two);
  console.log(three);
}
say(...oneTwoThree());

ES2018では普通のオブジェクトも分割代入したりスプレッド演算子で展開したりできるようになるっぽいんだけど、じゃあ普通のオブジェクトも全部イテラブルになるのかな? 仕様策定の様子は追ってないので思っただけだけど。

分割代入と ... の使い方は別稿参照。

その他

[Symbol.iterator]() て何だよ

なんで普通の名前じゃないんだ Symbol なんだ、 toString() みたいにしなかったんだ。

だいたいイテレータはイテラブル

「イテレータ」と「イテラブル」は別インターフェイスなので同時に満たす必要はないんだけど、内部で %IteratorPrototype% というイテレータ共通のプロトタイプがありまして、こいつがイテラブルだったりします。

配列とかのイテレータはこのプロトタイプを継承しているので、JavaScriptネイティブなイテレータはだいたいイテラブルになるっぽい。 TypedArray は専用のものを持たないが、普通の配列 Array の処理を利用している。

ちなみに %IteratorPrototype% の [Symbol.iterator]() は this を返します。

イテラブルではないイテレータ

の例。

class MyIterator {
  constructor(values) {
    this.values = values;
    this.index = 0;
  }

  // 自前で実装するぞ
  next() {
    const value = this.values[this.index];
    this.index += 1;
    return {
      value: value,
      done: this.index >= this.values.length,
    };
  }
};

const values = [100, 200, 300];

// イテレータとして利用
const it = new MyIterator(values);
let result;
result = it.next();
console.log(result.value);

// イテラブルとして利用(できない)
// Exception: TypeError: (new MyIterator(...)) is not iterable
for (let item of new MyIterator(values)) {
  console.log(item);
}

普通のイテレータ

Java方面の話で聞いたときは next() で次に移動しつつ値を戻り値でそのまま得て、続きがあるかどうかは hasNext() という別のメソッドを使うみたいな流れだったと思うんだけど、なんで違うのかしらん。

時代が変わってイテレータパターン自体が変わった?

インターフェイス vs プロトコル

ちなみにMDNだとイテレータとかで「プロトコル (protocol)」という表現が用いられているけれど、仕様書だと「インターフェイス (interface)」です。まあ同じものでしょ。

たしかSwiftはprotocolという表現してたよね。

function と * の間で改行できる

適当に空白文字を置ける様子。

function
* goo() {
}

async はだめだったのに~。

参考

更新履歴

  • 2017/12/23 TypedArray がイテラブルじゃないみたいな勘違いしてたのを修正

イテレータとfor-of文で配列以外もぐーるぐる。(現代的JavaScriptおれおれアドベントカレンダー2017 – 22日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 22日目

概要

配列とか Map とかは for-of 文を使って、添え字アクセス? [] を使わずにぐるぐるできます。

const arr = ['Hello', 'World', '!'];

for (let item of arr) {
  console.log(item);
}

普通のオブジェクトでは使えないけど、インターフェイスを追加すれば使えるようにもなります。

使い方

普通の for 文を使って配列で arr[i] する代わりに for-of でいきなり値を取ってこれます。

const arr = ['Hello', 'World', '!'];

for (let item of arr) {
  console.log(item);
}

配列以外でも反復可能なオブジェクト(後述)で使えます。

const map = new Map([[1, 11], [2, 22], [3, 33]]);

for (let [key, value] of map) {
  console.log(`${key}: ${value}`);
}

文字列と絵文字

文字列でも使えます。絵文字とかのサロゲートペアを上手に扱ってくれます。やったね。

const str = '#sushi🍣';

console.log('--- for ---');
for (let i = 0; i < str.length; i++) {
  console.log(str[i]);
}

console.log('--- for-of ---');
for (let item of str) {
  console.log(item);
}

--- for ---
#
s
u
s
h
i
�
�
--- for-of ---
#
s
u
s
h
i
🍣

反復可能なオブジェクト

for-of 文は反復可能なオブジェクトに対して利用することができます。

対応してないオブジェクトだとエラーに。

const obj = {};

for (let item of obj) {  // TypeError: obj[Symbol.iterator] is not a function
    console.log(item);
}

反復可能なオブジェクト

この「反復可能(イテラブル)なオブジェクト (iterable object)」とは、「イテレータ」を作成するインターフェイスを整えたオブジェクトのことです。配列 Array や Map 等が該当します。

インターフェイスが整っていれば何でも良いので、自作することもできます。

// 反復可能なオブジェクト
const iterable = {
  // for-ofに必要なインターフェイス
  [Symbol.iterator]() {
    const max = 10;
    let n = 0;

    const iterator = {
      next() {
        return { value: n++, done: n > max };
      },
    };

    return iterator;
  },
};

// よっしゃー使うぞー
for (let item of iterable) {
  console.log(item);
}

詳細は別稿参照。

その他

for-in と for-of

もちろん別物なんだけど、同じ項 (13.7.5) でまとめて定義されてるので、兄弟みたいなものらしい。

for-in はカンマ , 区切りの値を置ける

なんでOKなんだろ。

const arr1 = [100];
const arr2 = [200];

arr1.foo = 123;
arr2.bar = 123;
for (let index in arr1, arr2) {
  console.log(index);
}

もちろん末尾のもの arr2 が有効になります。

0
bar

for-of でやると構文エラーです。

const arr1 = [100];
const arr2 = [200];

for (let item of arr1, arr2) {  // SyntaxError: Unexpected token ,
  console.log(item);
}

for-in で初期値を書ける場面が

"use strict" の厳格モードでない場面で、 var をここで使用する場合のみ、初期値を置ける。 let を使ったり、別の場所で var 宣言してからの代入だとだめ。

const arr = [1, 2, 3];

for (var index = 'wow' in arr) {
  console.log(index);
}

といってもその初期値が適用される場面てあるんだろか。

謎仕様だなー。後方互換のためなのかな。

参考

  • ECMAScript® 2017 Language Specification
    • 13.7.5 The for-in and for-of Statements
    • 13.7.5.12 Runtime Semantics: ForIn/OfHeadEvaluation ( TDZnames, expr, iterationKind ) … イテレータの作成。初期処理的な
    • 13.7.5.13 Runtime Semantics: ForIn/OfBodyEvaluation ( lhs, stmt, iterator, iterationKind, lhsKind, labelSet ) … イテレータを使って反復処理する
    • 13.7.5.15 EnumerateObjectProperties ( O ) … for-in 用のイテレータ
    • 6.1.5.1 Well-Known Symbols … Symbole.iterator
    • 7.4.1 GetIterator ( obj [ , method ] )
    • 7.4.5 IteratorStep ( iterator ) … イテレータで反復
    • 25.1 Iteration
    • B.3.6 Initializers in ForIn Statement Heads … for-in で初期値使える
  • for…of – JavaScript | MDN
  • for…in – JavaScript | MDN

WeakMap、WeakSetで「弱い参照」を使えるようになったぞ。(現代的JavaScriptおれおれアドベントカレンダー2017 – 21日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 21日目

概要

基本的な使い方は Map や Set と一緒です。キーに使われるオブジェクトの参照カウントを増やさないとかそういうアレで、「弱い参照」で保持します。メモリリーク対策とかに。

Map 、 Set との違い

基本は Map 、 Set と同様なんだけど、キーが弱い参照(後述)になります。

できること:

  • get() (WeakMap)
  • set() (WeakMap)
  • add() (WeakSet)
  • has()
  • delete()

できないこと:

  • size
  • forEach()
  • clear()
  • keys()
  • 他

キーはオブジェクトのみ

Map は何でもキーに使えたんだけど、 WeakMap はオブジェクトだけです。

まあ意味ないしね。

弱い参照?

JavaScript界隈では新しい概念のはず。

プログラム内で使ってる情報ってメモリに置かれるじゃないですか。あれって使われてる間は保持されてて、使われなくなったら消してくれるんですよ、誰かが。上位のプログラムが。

で、問題はどうやって「使われているか」を判断するかなんだけど、変数から参照されてる数を覚えておく、というのがよくある基本的な仕組みです。そんでその数を「参照カウンタ」とか呼んだりします。参照カウンタが0になったらもう誰も見てないから消しちゃおう、ていうね。

参照を数える様子。

変数はスコープが終わると消えて、参照カウンタが減って、他から参照されてなければ参照先の情報も消える、と。

弱い参照だと、その参照カウンタを増やさずにおくことができます。なので、弱い参照が残っていてもメモリ上の情報は消えてしまうこともあります。

WeakMapのキーは弱い参照

WeakMapのキーとしてのみ使われている場合、そのリソースは削除されます。

WeakMap objects are collections of key/value pairs where the keys are objects and values may be arbitrary ECMAScript language values. A WeakMap may be queried to see if it contains a key/value pair with a specific key, but no mechanism is provided for enumerating the objects it holds as keys. If an object that is being used as the key of a WeakMap key/value pair is only reachable by following a chain of references that start within that WeakMap, then that key/value pair is inaccessible and is automatically removed from the WeakMap. WeakMap implementations must detect and remove such key/value pairs and any associated resources.

WeakMapオブジェクトはkey/valueペアのコレクションです。keyはオブジェクト、valueはECMAScript言語の任意の値を取ります。WeakMapはあるkey/valueペアを格納しているかを、特定のkeyを用いて確認できるよう要求されるかもしれませんが、キーとして保持しているオブジェクトの列挙についての手順は提供されていません。もしWeakMapのkey/valueペアのキーとして使われているオブジェクトが、そのWeakMap内から開始する参照チェインを辿ってのみ到達できる場合、そのkey/valueペアはアクセスできず、また自動的にそのWeakMapから削除されます。WeakMapの実装は、このようなkey/valueペア及び関連リソースを検出し、削除する必要があります。

(強調は引用者。)

弱参照とか言ったけど、仕様書ではそういう単語は使ってないし、実装方法が任意なので参照カウンタが用いられているとも限らないです。たぶん使ってると思うんだけど。

値の方は普通の参照です。

継承して

仕様書によると「サブクラスとして使えるように設計されている」とのことです。

内部の初期処理が必要なので、コンストラクタで super 呼び出ししないと怒られます。

class ElementData extends WeakMap {
  constructor() {
    // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from deriv
  }
}

HTML要素をキーに

DOMノードだったら同じ内容で別インスタンスみたいなものってあんまり作られない感じがするので、キーにするのに良さそう。jQueryの data() みたいなやつ。

const elementData = new WeakMap();

const el = document.querySelector('#foo');
elementData(el, { text: '追加情報 ' });

console.log(elementData.get(el));

Reactとかでがんがん要素再生成しますーみたいな設計の場合は、たぶん識別子とか持ってるんじゃないかな。そんなら普通の Map で十分そう。いや自動削除ないか。

実際の利用ケース

Stack Overflowの解答から。

Some use cases that would otherwise cause a memory leak and are enabled by WeakMaps include:

  • Keeping private data about a specific object and only giving access to it to people with a reference to the Map. A more ad-hoc approach is coming with the private-symbols proposal but that’s a long time from now.
  • Keeping data about library objects without changing them or incurring overhead.
  • Keeping data about a small set of objects where many objects of the type exists to not incur problems with hidden classes JS engines use for objects of the same type.
  • Keeping data about host objects like DOM nodes in the browser.
  • Adding a capability to an object from the outside (like the event emitter example in the other answer).

WeakMapを使わないとメモリリークを引き起こす利用ケース:

  • あるオブジェクトに関するプライベートな情報を保持し、Mapへの参照を経由してのアクセスのみを公開する。より直接的な手段としてプライベートシンボルが提案されているが、まだ時間かかりそう
  • ライブラリオブジェクトに関する情報を、ライブラリを変更したりオーバーヘッドを招かずに保持する
  • 小さなオブジェクトの組み合わせに関する情報を保持する。この種の多数のオブジェクトは、JSエンジンが同種のオブジェクトのために使う隠しクラスに関する問題を引き起こさないために存在する(訳注: ごめんイミフ)
  • ブラウザのDOMノードのような、ホストオブジェクトに関する情報を保持する
  • あるオブジェクトの外側から能力 (capability) を追加する(他の解答にあるイベントエミッタの例みたいに)

載せといてなんだけど正直何言ってんのかよくわかんない……。

その他

キー一覧機能の提供は禁止

速度問題に発展しがちなので、そういうのは実装しちゃだめ (must not) と仕様書に書かれてます。

(あれ、そういう理解で合ってるよね?)

参考

値だけ覚えておくならSetという手が。(現代的JavaScriptおれおれアドベントカレンダー2017 – 20日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 20日目

概要

Map のキーだけを保持する版です。

const set = new Set();
set.add(1);
set.add(2);

console.log(set.size);  // 2
console.log(set.has(2));  // => true

使い方

なんとなく Map と同じ感じ。ただし値を追加するのは set() じゃなくて add() になってます。

// 作成
const set = new Set();

// 追加
set.add(1);
set.add(2);
set.add(3);
set.add(1);  // 同じ値は無視される

// 有無
console.log(set.has(2));  // => true

// 削除
set.delete(2);
console.log(set.has(2));  // => false

// サイズ
console.log('Size:', set.size);  // 2

// 反復
set.forEach((value) => {
    console.log('forEach', value);
})

for (let value of set) {
    console.log('for-of', value);
}

// 値だけまとめて取得
const values = set.values();

// 全削除
set.clear();

keys() もある

ただし keys() と values() は完全に同じ関数です。

console.log(set.keys === set.values);  // => true

あと entries() もあって、なんか想像通りの変な感じの結果を返します。インターフェイスを統一する的なやつなんだろか。

その他

キー vs 値

Set の仕様内では “value” という表現を使ってるっぽい。でも重複しないという特性はキーっぽいよね。

参考

データ保持ならObjectよりMapの方が良いの?(現代的JavaScriptおれおれアドベントカレンダー2017 – 19日目)

カテゴリー: JavaScript

現代的JavaScriptおれおれアドベントカレンダー2017 – 19日目

良いか???

概要

Array#map() じゃなくて Map というビルトインコンストラクタです。

辞書 (dictionary) みたいなノリで使えます。

const map = new Map();

map.set(100, 'Number 100');
console.log(map.get(100));  // => "Number 100"

for (let [key, value] of map) {
    console.log(`[${key}] = [${value}]`);
}

使い方

new で作って名前と値の組み合わせの情報を格納します。

情報の追加と取得

set() で追加(と上書き)、 get() で取得。

const map = new Map();
const key = 100;

// set
map.set(key, { message: 'Hello World!' });

// get
const value = map.get(key);

concole.log(key, value);

set() は map オブジェクト自体を返すので、メソッドチェインみたいに書けます。

map
  .set(100, 'Hello')
  .set(101, 'World')
  .set(102, '!')

あんまり書かない方が良いと思うけど。

有無の確認

const map = new Map();

console.log(map.has(1));  // => false

map.set(1, 'one');
console.log(map.has(1));  // => true

削除

delete() でキーを指定して消します。削除できた場合は true が、もともと値を持っていなかった場合は false が帰ります。 delete 演算子と違うね。

あと clear() で全部消します。こちらは常に undefined を返す。

const map = new Map();
let deleted;

map.set(100, 'Hello World!');
map.set(101, 'Goodmorning Universe!');
map.set(102, 'How is it going man?');

console.log('Size:', map.size);  // => Size: 3
console.log('#101', map.get(101));  // => #101 Goodmorning Universe!

// ひとつ削除
deleted = map.delete(101);
console.log('Size:', map.size);  // => Size: 2
console.log('#101', map.get(101));  // => #101 undefined
console.log('Delete successfully?', deleted);  // => true

// 削除済みのものを削除
deleted = map.delete(101);
console.log('Delete successfully?', deleted);  // => false

// 全部削除
map.clear();
console.log('Size:', map.size);  // => Size: 0

キーは何でも

オブジェクトの場合は基本的に toString() で文字列に変換されるんだけど、 Map の場合は型も含めてそのままキーになります。 1 と "1" は別物扱いです。

const map = new Map();

// 数値の1と文字列の"1"
map.set(1, 'Number 1');
map.set('1', 'String 1');

console.log(map.get(1));  // => "Number 1"
console.log(map.get('1'));  // => "String 1"

各種オブジェクトをそのまま突っ込むこともできます。

繰り返す

forEach() あるいは for-of ループでぐるぐるできます。

あと size という、配列の length みたいなプロパティがあります。

const map = new Map();
let deleted;

map.set(100, 'Hello World!');
map.set(101, 'Goodmorning Universe!');
map.set(102, 'How is it going man?');

console.log('Size:', map.size);  // => Size: 3

map.forEach((value, key) => {
    console.log('forEach', key, value);
})

for (let [key, value] of map) {
    console.log('for-of', key, value);
}

forEach() 以外の、 Array.prototype 系のメソッドはないです。

キー、値をまとめて取得

keys() でキーだけ、 values() で値だけ、さらに entries() でキーと値の組のイテレータを得られます。

const map = new Map();

map.set(100, 'Hello World!');
map.set(101, 'Goodmorning Universe!');
map.set(102, 'How is it going man?');

for (let key of map.keys()) {
    console.log('key', key);
}

for (let value of map.values()) {
    console.log('value', value);
}

for (let [key, value] of map.entries()) {
    console.log('key-value', key, value);
}

[] アクセスはだめ

マップじゃなくて普通のインスタンスプロパティへのアクセスになります。

const map = new Map();

map.set(100, 'Hello World!');
map[101] = 'Goodmorning Universe!';

console.log(map.size);  // => 1
console.log(map.get(100));  // => "Hello World!"
console.log(map.get(101));  // => undefined
console.log(map['101']);  // => "Goodmorning Universe!"

オブジェクトに追加情報を持たせる

他に独自の情報を持ちたいけど元のオブジェクトに触りたくないなーというとき、そのオブジェクト自体をキーにして、追加情報を別途置いておくことができます。

const map = new Map();

const el = document.querySelector('#foo');
const someDataForTheElement = {};
map.set(el, someDataForTheElement);

でも別インスタンスになると別物っていう扱いになっちゃうので、永続的に使える識別子があればそっちの方が良さそう。

使い道

と、ここまで書いてきたけど正直これっていう使い道が思いつかない……。

オブジェクト Object (ないし {} )でも同じようなことはできます。今までずっとこっちでやってきましたね。

const map = {};
map[100] = 'A hundred';
map[101] = 'A hundred and one';

console.log(100, map[100]);  // => "A hundred"

Object.keys(map)
    .forEach((key) => {
        const value = map[key];
        console.log(key, value);
    });

反復処理とかちょっとアレだけどまあどうにかなるし、まだあんまりよくわかんない。

Object と Map の違い

  • [] の代わりに set() 、 get() を使う
  • キーに文字列以外も使える( 1 と "1" が別物扱い)
  • 反復処理 for-of できる
  • size で数を取れる

MDNには

こう載ってる。

『オブジェクトとマップの比較』より。

これは Map をいつも使えばいいということではありません。オブジェクトはまだ多くの場面で使えます。Map インスタンスはコレクションとして使う場合のみに役に立ちます。以前オブジェクトをこのように使っていたコードに Map を使うことを考えてみるべきです。オブジェクトはメンバ変数とメソッドを備えたレコードとして使われます。もしまだどちらを使えばいいかわからないなら、以下の質問に答えてみてください。

  • キーがいつも実行時までわからない、またはキーを直接調べる必要がありますか?
  • すべての値が同じ型で、交換して使用できますか?
  • 文字列でないキーが必要ですか?
  • キーと値のペアを時々、追加または削除しますか?
  • 簡単に量が変わるキーと値のペアがありますか?
  • そのコレクションをイテレートしますか?

これらはあなたが Map をコレクションとして使いたいときのサインです。もし対照的に、固定された量のキーがあり、それら個々に操作し、Mapの使い方と区別する場合、オブジェクトを使いましょう。

その他

NaN が合致する

JavaScriptの細かい話で良く出てくるやつなんですが、 NaN “Not a Number” は特殊な値で、自身と等値になりません。

const n = NaN;
console.log(n === n);  // false (wow)

が、マップのキーとして使う場合、(内部で)同じ値とみなしてくれます。

const map = new Map();

map.set(NaN, 'Not a Number');
console.log(map.get(NaN));  // => "Not a Number"
map.set(NaN, '!');
console.log(map.get(NaN));  // => "!"

もちろん NaN と文字列 "NaN" は合致しません。やったね。

オブジェクト Object のキー

上の方で「 obj[key] の key は基本的に文字列に」みたいに言ったけど、 Symbol てのも使えるようになりました。

参考

  • ECMAScript® 2017 Language Specification
    • 23.1 Map Objects
    • 23.1.3 Properties of the Map Prototype Object … 各種メソッドとプロパティ
    • 7.2.10 SameValueZero ( x, y ) … get() でキーの合致を確認する内部処理
    • 12.3.2.1 Runtime Semantics: Evaluation … obj[key] の解釈
    • 7.1.14 ToPropertyKey ( argument ) … obj[key] の key の解釈
  • Map – JavaScript | MDN
  • Symbol – JavaScript | MDN