今や、私たちは次のような複雑なデータ構造を知っています:

  • キー付けされたコレクションを格納するオブジェクト
  • 順序付けされたコレクションを格納する配列

しかし、実際にはこれだけでは不十分です。そのために、MapSet が存在します。

Map

MapObject と同じように、キー付されたデータ項目の集まりです。主な違いは Map は任意の型のキーを許可することです。

主なメソッドは次の通りです:

  • new Map() – 新しい map を作ります.
  • map.set(key, value) – キーで、値を格納します.
  • map.get(key) – 指定されたキーの値を返却します。map に存在しない key の場合には undefined になります.
  • map.has(key) – キーが存在する場合に true を返します。そうでなければ false になります.
  • map.delete(key) – キーで値を削除します.
  • map.clear() – map をクリアします.
  • map.size – 現在の要素の数です.

例:

let map = new Map();

map.set('1', 'str1');   // 文字列キー
map.set(1, 'num1');     // 数値キー
map.set(true, 'bool1'); // 真偽値キー

// 通常のオブジェクトを覚えていますか?キーを文字列に変換していました。
// Map は型を維持します。なので、これらは別ものです:
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3

上の通り、オブジェクトとは違い、キーは文字列には変換されません。任意の型のキーが利用可能です。

Map はキーとしてオブジェクトを使うこともできます。

例:

let john = { name: "John" };

// 各ユーザに対し、訪問回数を保持しましょう
let visitsCountMap = new Map();

// john は map のキーです
visitsCountMap.set(john, 123);

alert( visitsCountMap.get(john) ); // 123

オブジェクトをキーとして使うことは、最も重要で特筆すべき Map の機能の1つです。文字列キーを利用する場合は、Object で問題ありませんが、上記の例では、Map を通常の Object に置き換えるのは難しいでしょう。

Map が存在する前は、ユニークな識別子をオブジェクトに追加していました:

// id フィールドを追加します
let john = { name: "John", id: 1 };

let visitsCounts = {};

// id をキーに値を保持します
visitsCounts[john.id] = 123;

alert( visitsCounts[john.id] ); // 123

…しかし Map は遥かにエレガントです。

Map はどのようにキーを比較するか

等価のテストをするために、MapSameValueZero アルゴリズムを使います。大雑把には厳密等価 === と同じですが、違いは NaNNaN と等しいとみなされる点です。なので、NaN も同様にキーとして使うことができます。

このアルゴリズムは変更したりカスタマイズすることはできません。

チェーン

map.set 呼び出しは map 自身を返すので、呼び出しを “チェーン” することができます:

map.set('1', 'str1')
  .set(1, 'num1')
  .set(true, 'bool1');

オブジェクトから Map を生成

Map を生成する時、キー/値のペアをもつ配列(または別の反復可能(iterable)) を渡すことができます:

// [key, value] ペアの配列
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);

オブジェクトのキー/値のペアの配列を、その形式で返す組み込みのメソッド Object.entries(obj) があります。

なので、次のようにオブジェクトから map の初期化をすることができます:

let map = new Map(Object.entries({
  name: "John",
  age: 30
}));

ここで、Object.entries はキー/値のペアの配列を返します: [ ["name","John"], ["age", 30] ]。これは Map が必要とするものです。

Map での繰り返し/ループ

map でループするためには3つのメソッドがあります:

  • map.keys() – キーに対する iterable を返します。
  • map.values() – 値に対する iterable を返します。
  • map.entries() – エントリ [key, value] の iterable を返します。これは for..of でデフォルトで使われます。

例:

let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// キー(野菜)の反復
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomateos, onion
}

// 値(量)の反復
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// [key, value] エントリーの反復
for (let entry of recipeMap) { // recipeMap.entries() と同じ
  alert(entry); // cucumber,500 (など)
}
挿入順が使われます

繰り返しは値が挿入された順で行われます。通常の Object とは違い、Map はこの順番を保持しています。

それに加えて、MapArray と同じように、組み込みの forEach メソッドを持っています。

recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

Set

Set – は値の集まりで、それぞれの値は一度しか現れません。

主なメソッドは次の通りです:

  • new Set(iterable) – set を作ります。オプションで値の配列(任意の iterable が指定可能)からも可能です。
  • set.add(value) – 値を追加し、 set 自身を返します。
  • set.delete(value) – 値を削除し、value が呼び出し時に存在すれば true, そうでなければ false を返します。
  • set.has(value) – set の中に値が存在すれば true を返し、それ以外は false です。
  • set.clear() – set から全てを削除します。
  • set.size – set の要素数です。

例えば、訪問者全員を覚えておきたいです。が、繰り返し訪問しても重複しないようにしたいです。訪問者は一度だけ “カウント” される必要があります。

Set はそれに相応しいものです:

let set = new Set();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

// 訪問、何度も来るユーザもいます
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// set はユニークな値のみをキープします
alert( set.size ); // 3

for (let user of set) {
  alert(user.name); // John (そして Pete と Mary)
}

Set の代替は、ユーザの配列と arr.find を使って挿入毎に重複をチェックするコードです。しかし、このメソッドはすべての要素をチェックするため配列全体を見ます。そのためパフォーマンスははるかに悪いです。Set は一意性チェックを高速に行うよう、内部的に最適化されています。

Set での繰り返し

for..of または forEach を使うことで set をループすることができます:

let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) alert(value);

// forEach と同じ:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

面白い点に注意してください。Set の中の forEach 関数は3つの引数を持っています: 値(value), 次に 再び値(valueAgain), 次にターゲットのオブジェクトです。実際、引数には同じ値が2回出現します。

これは forEach が3つの引数をもつ Map との互換性のために作られています。

  • set.keys() – 値に対する iterable なオブジェクトを返します。
  • set.values()set.keys と同じで、Map との互換性のためです。
  • set.entries()[value, value] のエントリのための iterable なオブジェクトを返します。Map の互換性のために存在します。

WeakMap と WeakSet

WeakSet はJavaScriptがメモリからそのアイテムを削除するのを妨げない、Set の特別な種類です。WeakMapMap に対する同様のものです。

チャプター ガベージコレクション で学んだ通り、JavaScriptエンジンは、それが到達可能な(そして潜在的に利用される可能性がある)間、メモリ上に値を保持しています。

例:

let john = { name: "John" };

// オブジェクトへはアクセス可能です。john がその参照を持っています

// 参照を上書きします
john = null;

// オブジェクトはメモリから削除されるでしょう

通常、オブジェクトまたは配列の要素、もしくは別のデータ構造のプロパティは到達可能と考えられ、そのデータ構造がメモリにいる間は保持され続けます。

例えば、あるオブジェクトを配列に入れた場合、その配列が生きている間は、他の参照がなくてもそのオブジェクトは生きていることになります。

例:

let john = { name: "John" };

let array = [ john ];

john = null; // 参照を上書きします

// john は配列内に格納されているので、ガベージコレクションされません。
// array[0] としてそれを取得することが可能です

また、通常の Map のキーとしてオブジェクトを使うと、Map が存在している間はそのオブジェクトも存在します。これはメモリを占め、ガベージコレクションされないかもしれません。

例:

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 参照を上書きします

// john は map の中に保持されています
// map.keys() で取得することができます

WeakMap/WeakSet はこの点で根本的に異なります。これらはキーオブジェクトのガベージコレクションを妨げることはありません。

WeakMap を使って説明しましょう。

Map との最初の違いは、WeakMap のキーはプリミティブな値ではなくオブジェクトでなければならないことです:

let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // 正常に動作します (オブジェクトのキー)

weakMap.set("test", "Whoops"); // エラー, "test" はプリミティブだからです

いま、オブジェクトをキーとして使用し、そのオブジェクトへの参照が他にない場合、自動的にメモリ(と map)から削除されます。

let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 参照を上書きします

// john はメモリから削除されます!

上の例を通常の Map の場合と比べて見てください。WeakMap のキーとしてのみ john が存在する場合、自動的に削除されます。

WeakMap は繰り返しと、メソッド keys(), values(), entries() をサポートしません。そのため、すべてのキーや値を取得する方法はありません。

WeakMap は次のメソッドのみを持っています:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key, value)
  • weakMap.has(key)

なぜこのような制限があるのでしょうか?これは技術的な理由です。もしオブジェクトがすべての他の参照を失った場合(上のコードの john のように)、自動的に削除されます。しかし、技術的には いつクリーンアップが発生するか は正確には指定されていません。

それはJavaScriptエンジンが決定します。エンジンはすぐにメモリのクリーンアップを実行するか、待ってより多くの削除が発生した後にクリーンアップするかを選択できます。従って、技術的にはWeakMap の現在の要素数はわかりません。エンジンがクリーンアップしている/していない、または部分的にそれをしているかもしれません。このような理由から、WeakMap 全体にアクセスするメソッドはサポートされていません。

さて、どこでこのようなものが必要なのでしょう?

WeakMap のアイデアは、オブジェクトが存在している間だけ存在するオブジェクトに対して何かを格納することができる、と言うことです。しかし、何かを格納しているという事実だけで、オブジェクトを強制的に生存させることはしません。

weakMap.put(john, "secret documents");
// もし john がなくなった場合、秘密のドキュメントは破壊されるでしょう

どこかにオブジェクトを格納するメインの場所を持ち、オブジェクトが存続している間だけ関連する追加情報を保持する必要がある、といった場合に便利です。

例を見てみましょう。

例えば、ユーザ毎の訪問回数を持つコードを持っているとします。情報は map に格納されており、ユーザがキーで、訪問回数が値です。ユーザが去ったとき、もう訪問回数の格納は不要です。

一つ目の方法は、各ユーザを追跡し、ユーザが去った際に手動で map から削除する方法です:

let john = { name: "John" };

// map: user => 訪問数
let visitsCountMap = new Map();

// john は map のキー
visitsCountMap.set(john, 123);

// 今 john は去りました。これ以上彼の情報は不要です
john = null;

// map の場合依然として残り続けるので、クリーンが必要です!
alert( visitsCountMap.size ); // 1
// john は未だメモリに存在しています。Mapがキーとして使用しているためです

別の方法は WeakMap を使うことです:

let john = { name: "John" };

let visitsCountMap = new WeakMap();

visitsCountMap.set(john, 123);

// 今 john は去りました。これ以上彼の情報は不要です
john = null;

// WeakMap 以外の参照がなくなりました
// オブジェクトはメモリと visitsCountMap 両方から自動的に削除されます

通常の Map での、ユーザが離れた後のクリーンアップはうんざりするタスクです: メインの格納域(変数または配列)から削除するだけでなく、visitsCountMap のような追加の格納域のクリーンアップも必要になります。また、ユーザがコードの一箇所で管理され、追加の構造は別の場所にあり、削除に関する情報が得られないようなより複雑なケースでは、煩雑になる可能性があります。

WeakMap は自動的にクリーンアップするので、物事をよりシンプルにすることができます。その中にある、上の例の訪問回数のような情報はキーオブジェクトが存在する間だけ生きています。

WeakSet も同じように振る舞います。:

  • Set に似ていますが、WeakSet にはオブジェクトの追加だけができます(プリミティブは追加できません)。
  • どこかから到達可能な間だけ、 set の中でオブジェクトが存在します。
  • Set のように、add, has, delete をサポートしますが、size, keys() はサポートせず、反復処理もありません。

例えば、ある項目が確認済みかどうか追跡するために使うことができます:

let messages = [
    {text: "Hello", from: "John"},
    {text: "How goes?", from: "John"},
    {text: "See you soon", from: "Alice"}
];

// 配列要素(3項目)で埋めます
let unreadSet = new WeakSet(messages);

// message が未読かは unreadSet を見ることで確認できます
alert(unreadSet.has(messages[1])); // true
// 読んだ後にセットから取り除く
unreadSet.delete(messages[1]); // true

// そして、メッセージ履歴を shift すると、set は自動でクリーンアップされます
messages.shift();
// unreadSet をクリーンする必要はありません。
// 項目の正確な数をカウントするメソッドがないため、残念ながらここではお見せすることはできませんが、今は2つの要素になっています。

WeakMapWeakSet の最も注目に値する制限は反復処理がないこと、また現在のすべての中身を取得することができないことです。それらは不便に見えるかもしれませんが、実際には、WeakMap/WeakSet の主な仕事(別の場所で格納/管理されているオブジェクトのための “追加” のデータ記憶域になる)を妨げることはありません。

サマリ

  • Map – はキー付けされた値のコレクションです。

    通常の Object との違いは次の通りです:

    • 任意のキー、オブジェクトをキーにすることができます。
    • 挿入した順番で反復処理します。
    • 追加の便利なメソッド、size プロパティがあります。
  • Set – ユニークな値のコレクションです。

    • 配列とは違い、要素の順序を変更することはできません。
    • 挿入した順序を保持します。
  • WeakMap – オブジェクトのみをキーとして許可し、他の手段でアクセスできなくなると削除を行うMapの変形です。

    • 構造全体に対する操作はサポートしていません: sizeclear()、反復はありません。
  • WeakSet – はオブジェクトのみを格納し、他の手段でアクセスできなくなると削除する Set の変形です。

    • また、size/clear() や反復をサポートしていません。

WeakMapWeakSet は “メインの” オブジェクト記憶域に加えて、“2つ目” のデータ構造として使われます。メインの記憶域からオブジェクトが削除されると、オブジェクトは WeakMap/WeakSet にしか残らず、自動的にクリーンアップされます。

タスク

重要性: 5

arr は配列としてます。

arr のユニークなアイテムを持つ配列を返す関数 unique(arr) を作成してください。

例:

function unique(arr) {
  /* your code */
}

let values = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(values) ); // Hare, Krishna, :-O

P.S ここでは文字列が使われていますが、任意の型の値にすることができます。

P.P.S. ユニークな値を格納するために Set を使ってください。

テストと一緒にサンドボックスを開く

重要性: 4

アナグラム は同じ文字を同じ数だけ持っていますが、異なる順序である単語です。

例えば:

nap - pan
ear - are - era
cheaters - hectares - teachers

アナグラムで整理された配列を返す関数 aclean(arr) を書いてください。

例:

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) ); // "nap,teachers,ear" or "PAN,cheaters,era"

すべてのアナグラム・グループから、どれかは問いませんが1つの単語だけ残してください。

テストと一緒にサンドボックスを開く

すべてのアナグラムを見つけるために、すべての単語を文字に分割してソートしましょう。文字でソートしたとき、すべてのアナグラムは同じです。

例:

nap, pan -> anp
ear, era, are -> aer
cheaters, hectares, teachers -> aceehrst
...

文字でソートされたバリアントをマップキーとして使用して、各キーごとに1つの値しか格納しません。:

function aclean(arr) {
  let map = new Map();

  for (let word of arr) {
    // 単語を文字で分割し、ソートして結合し直します
    let sorted = word.toLowerCase().split('').sort().join(''); // (*)
    map.set(sorted, word);
  }

  return Array.from(map.values());
}

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) );

文字ソートは行 (*) での呼び出しチェーンで行われています。

便利のために、複数行に分割しましょう:

let sorted = arr[i] // PAN
  .toLowerCase() // pan
  .split('') // ['p','a','n']
  .sort() // ['a','n','p']
  .join(''); // anp

2つの異なる単語 'PAN''nap' は同じ文字デソートされた形式 'anp' になります。

次の行は単語をマップにセットしています。:

map.set(sorted, word);

同じ文字でソートされた単語がもう一度あった場合には、マップ内の同じキーで前の値を上書きします。なので、私たちはいつも文字形式毎に最大1つの単語を持ちます。

最後に、Array.from(map.values()) でマップの値の反復をし(結果の中でキーは必要ありません)、それらの配列を返却します。

ここでは、Map の代わりに通常のオブジェクトを使うこともできます。なぜならキーが文字列だからです。

その場合の解答は次のようになります:

function aclean(arr) {
  let obj = {};

  for (let i = 0; i < arr.length; i++) {
    let sorted = arr[i].toLowerCase().split("").sort().join("");
    obj[sorted] = arr[i];
  }

  return Array.from(Object.values(obj));
}

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) );

サンドボックスでテストと一緒に解答を開く

重要性: 5

私たちは map.keys() の配列を取得し、その結果を使って処理を続けたいです(マップ自体とは別に)。

が、問題があります。:

let map = new Map();

map.set("name", "John");

let keys = map.keys();

// Error: numbers.push is not a function
keys.push("more");

なぜでしょう?key.push が機能するためにはコードをどのように直せばよいでしょう?

これは、map.keys() は配列ではなく、反復可能(iterable) を返すためです。

Array.from を使うことで、それを配列に変換できます:

let map = new Map();

map.set("name", "John");

let keys = Array.from(map.keys());

keys.push("more");

alert(keys); // name, more
重要性: 5

メッセージの配列があります:

let messages = [
    {text: "Hello", from: "John"},
    {text: "How goes?", from: "John"},
    {text: "See you soon", from: "Alice"}
];

あなたのコードでそれにアクセスできますが、メッセージは他の誰かのコードで管理されています。そのコードによって新しいメッセージが追加され、古いものが定期的に削除されます。そして、あなたはそれが発生する正確なタイミングを知りません。

今、メッセージを “読んだ” かの情報を格納するために使えるのはどのデータ構造でしょうか?その構造は与えられたメッセージオブジェクトに対して、“読んだか?” という解答を与えるのに適したものでなければなりません。

P.S メッセージが messages から削除されたとき、あなたの構造からも同様に消える必要があります。

P.P.S. 私たちはメッセージオブジェクトを直接変更すべきではありません。ほかの誰かのコードによって管理されている場合、余分なプロパティの追加は悪い結果になる可能性があります。

ここでの良い選択肢は WeakSet です:

let messages = [
    {text: "Hello", from: "John"},
    {text: "How goes?", from: "John"},
    {text: "See you soon", from: "Alice"}
];

let readMessages = new WeakSet();

// 2つのメッセージは読まれました
readMessages.add(messages[0]);
readMessages.add(messages[1]);
// readMessages は2つの要素を持っています

// ...再び最初のメッセージを読みましょう!
readMessages.add(messages[0]);
// readMessages は依然として2つのユニークな要素をもっています

// 答え: message[0] は読んだ?
alert("Read message 0: " + readMessages.has(messages[0])); // true

messages.shift();
// これで readMessages の要素は1つです(技術的にはメモリは後ほどクリーンされるかもしれません)

WeakSet でメッセージのセットを格納し、その中でメッセージの存在を簡単に確認することができます。

それは自動的に自身をクリーンアップします。トレードオフはそれをイテレートすることができないことです。私たちは直接 “すべての既読メッセージ” を取得することができません。しかし、すべての messages をイテレートし、set 中のものをフィルタリングすることでそれを実現できます。

P.S メッセージが誰か他の人のコードで管理されている場合、各メッセージに同時のプロパティを追加することは危険です。が、衝突を回避するためにシンボルでそれをすることができます。

このようになります:

// シンボリックプロパティは我々のコードだけが知っています
let isRead = Symbol("isRead");
messages[0][isRead] = true;

これで、たとえ他のコードがメッセージプロパティのために for..in ループを使っても、隠しフラグは表示されません。

重要性: 5

前のタスクのメッセージ配列があります。状況は似ています。

let messages = [
    {text: "Hello", from: "John"},
    {text: "How goes?", from: "John"},
    {text: "See you soon", from: "Alice"}
];

この問題: “いつメッセージが読まれたか” という情報を格納するのにあなたが提案するのはどのようなデータ構造ですか?

前のタスクでは、“既読/未読” の事実の保持だけが必要でした。今度は日付を保存する必要があります。なお、前と同じようにメッセージが削除されたら現れないでください。

WeakMap を使って日付を格納することができます。:

let messages = [
    {text: "Hello", from: "John"},
    {text: "How goes?", from: "John"},
    {text: "See you soon", from: "Alice"}
];

let readMap = new WeakMap();

readMap.set(messages[0], new Date(2017, 1, 1));
// Date オブジェクトについては後ほど学びます
チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。