2024年9月24日

Map と Set

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

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

しかし、実際にはこれだけでは不十分です。そのために、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[key]Map を使用する正しい方法ではありません

例えば、map[key] = 2 のように、map[key] でも動作しますが、これは map を通常の JavaScript オブジェクトとして扱っているので、対応するすべての制限(文字列/シンボルキーのみなど)があることを意味します。

なので、map メソッドを使用するべきです: set, get など.

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

例:

let john = { name: "John" };

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

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

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

オブジェクトをキーとして使用することは、最も注目に値する重要な Map の機能の1つです。同じことを Object で行うことはできません。Object ではキーとして文字列を使用することは問題ありませんが、オブジェクトを使用することはできません。

やってみましょう:

let john = { name: "John" };
let ben = { name: "Ben" };

let visitsCountObj = {}; // オブジェクトを使用

visitsCountObj[ben] = 234; // ben オブジェクトをキーに使用
visitsCountObj[john] = 123; // john オブジェクトをキーに使用。ben オブジェクトは置換されます

// That's what got written!
alert( visitsCountObj["[object Object]"] ); // 123

visitsCountObj はオブジェクトなので、john などのすべてのキーを文字列に変換します。そのため、文字列キー "[object Object]" となります。間違いなくこれは望むものではありません。

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

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

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

チェーン

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

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

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 メソッドを持っています。

// 各 (key, value) ペアに対して関数を実行
recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

Object.entries: オブジェクトから Map を生成

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

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

alert( map.get('1') ); // str1

通常のオブジェクトがあり、そこから Map を生成したい場合は、オブジェクトのキー/値のペアの配列を、その形式で返す組み込みのメソッド Object.entries(obj) を使用できます。

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

let obj = {
  name: "John",
  age: 30
};

let map = new Map(Object.entries(obj));

alert( map.get('name') ); // John

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

Object.fromEntries: Map から オブジェクト

つい先程、通常のオブジェクトから Object.entries(obj) を使用して Map を作成する方法を見ました。

逆のことをする Object.fromEntries メソッドもあります。: [key, value] ペアの配列が与えられ、そこからオブジェクトを作成します:

let prices = Object.fromEntries([
  ['banana', 1],
  ['orange', 2],
  ['meat', 4]
]);

// prices = { banana: 1, orange: 2, meat: 4 }

alert(prices.orange); // 2

Map から通常のオブジェクトを取得する際に Object.fromEntries が使えます。

E.g. Map にデータを保持しているが、通常のオブジェクトを期待するサードパーティのコードにわたす必要がある場合。

やってみましょう:

let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);

let obj = Object.fromEntries(map.entries()); // 通常のオブジェクトを作成します (*)

// 完了!
// obj = { banana: 1, orange: 2, meat: 4 }

alert(obj.orange); // 2

map.entries() への呼び出しはキー/値ペアの配列を返し、それはまさに Object.fromEntries の正しい形式です。

また、行 (*) をより短くすることもできます:

let obj = Object.fromEntries(map); // omit .entries()

これは同じことです。なぜなら、Object.fromEntries は引数に反復可能なオブジェクトを期待するからです。つまり配列である必要はありません。そして、map の標準のイテレーションは map.entries() と同じキー/値を返します。したがって、map と同じキー/値を持つプレーンなオブジェクトが取得できます。

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.add(value) の繰り返しの呼び出しでは何もしないことです。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 との互換性のために作られています。少しおかしく見えるかもしれません。が、特定ケースにおいて簡単に MapSet に、またその逆を行うのに役立ちます。

Map がイテレーションのために持っているメソッドと同じメソッドもサポートしています:

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

サマリ

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

メソッドとプロパティです:

  • new Map([iterable]) – 初期化ではオプションで [key,value] ペアの iterable(e.g. 配列 ) で map を作成します
  • map.set(key, value) – キーで値を保存します
  • map.get(key) – キーで値を返します。key が map にない場合は undefined が返ります
  • map.has(key)key が存在すれば true を、そうでなければ false を返します
  • map.delete(key) – 指定されたキーで値を削除します
  • map.clear() – map からすべてを削除します
  • map.size – 現在の要素数を返します

通常の Object との違いです:

  • 任意のキー、オブジェクトをキーにすることができます。
  • 追加の便利なメソッド、size プロパティ。

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 の要素数です。

MapSet のイテレーションは常に挿入順で行われます。そのため、これらのコレクションが順序付けられていないとは言えませんが、要素を並べ替えたり、その番号で要素を直接取得することはできません。

タスク

重要性: 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 を使ってください。

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

function unique(arr) {
  return Array.from(new Set(arr));
}

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

重要性: 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) );
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());
}

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

重要性: 5

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

が、問題があります。:

let map = new Map();

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

let keys = map.keys();

// Error: keys.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
チュートリアルマップ