2022年11月14日

MapとSet

これまで、以下の複合データ構造について学習してきました。

  • オブジェクトは、キー付きコレクションの格納に使用されます。
  • 配列は、順序付けられたコレクションの格納に使用されます。

しかし、それは現実世界では不十分です。そのため、MapSetも存在します。

Map

Mapは、Objectのように、キー付きデータ項目のコレクションです。しかし、主な違いは、Mapはあらゆるタイプのキーを許可することです。

メソッドとプロパティは以下のとおりです。

  • new Map() – マップを作成します。
  • map.set(key, value) – キーで値を格納します。
  • map.get(key) – キーで値を返します。キーがマップに存在しない場合はundefinedを返します。
  • map.has(key) – キーが存在する場合はtrue、存在しない場合はfalseを返します。
  • map.delete(key) – キーで要素(キーと値のペア)を削除します。
  • map.clear() – マップからすべてを削除します。
  • map.size – 現在の要素数を返します。

例えば

let map = new Map();

map.set('1', 'str1');   // a string key
map.set(1, 'num1');     // a numeric key
map.set(true, 'bool1'); // a boolean key

// remember the regular Object? it would convert keys to string
// Map keeps the type, so these two are different:
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3

ご覧のように、オブジェクトとは異なり、キーは文字列に変換されません。あらゆるタイプのキーが可能です。

map[key]は、Mapを使用する正しい方法ではありません。

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

そのため、setgetなどのmapメソッドを使用する必要があります。

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

例えば

let john = { name: "John" };

// for every user, let's store their visits count
let visitsCountMap = new Map();

// john is the key for the map
visitsCountMap.set(john, 123);

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

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

試してみましょう

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

let visitsCountObj = {}; // try to use an object

visitsCountObj[ben] = 234; // try to use ben object as the key
visitsCountObj[john] = 123; // try to use john object as the key, ben object will get replaced

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

visitsCountObjはオブジェクトであるため、上記のjohnbenなどのすべてのObjectキーを同じ文字列"[object Object]"に変換します。明らかに、これは望ましい動作ではありません。

Mapがキーを比較する方法

キーの等価性をテストするために、MapSameValueZeroアルゴリズムを使用します。これは厳密な等価性===とほぼ同じですが、NaNNaNと等しいと見なされる点が異なります。そのため、NaNもキーとして使用できます。

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

チェーン

すべてのmap.set呼び出しはマップ自体を返すため、呼び出しを「チェーン」できます。

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

Mapの反復処理

mapをループ処理するには、3つのメソッドがあります。

  • map.keys() – キーのイテラブルを返します。
  • map.values() – 値のイテラブルを返します。
  • map.entries() – エントリ[key, value]のイテラブルを返します。for..ofではデフォルトで使用されます。

例えば

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

// iterate over keys (vegetables)
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// iterate over values (amounts)
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// iterate over [key, value] entries
for (let entry of recipeMap) { // the same as of recipeMap.entries()
  alert(entry); // cucumber,500 (and so on)
}
挿入順序が使用されます。

反復処理は、値が挿入された順序と同じ順序で行われます。Mapは、通常のObjectとは異なり、この順序を保持します。

それに加えて、Mapには、Arrayと同様の組み込みforEachメソッドがあります。

// runs the function for each (key, value) pair
recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

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

Mapを作成する場合、初期化のためにキーと値のペアを含む配列(またはその他のイテラブル)を渡すことができます。

// array of [key, value] pairs
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);

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

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

そのため、次のようにしてオブジェクトからマップを作成できます。

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]
]);

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

alert(prices.orange); // 2

Object.fromEntriesを使用して、Mapから通常のオブジェクトを取得できます。

例えば、データをMapに格納しますが、通常のオブジェクトを期待するサードパーティのコードに渡す必要があるとします。

このようにします。

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

let obj = Object.fromEntries(map.entries()); // make a plain object (*)

// done!
// 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]) – セットを作成します。iterableオブジェクト(通常は配列)が提供されている場合、その値をセットにコピーします。
  • set.add(value) – 値を追加します。セット自体を返します。
  • set.delete(value) – 値を削除します。呼び出し時に値が存在した場合はtrue、存在しなかった場合はfalseを返します。
  • set.has(value) – 値がセットに存在する場合はtrue、存在しない場合はfalseを返します。
  • set.clear() – セットからすべてを削除します。
  • set.size – 要素数です。

主な機能は、同じ値でset.add(value)を繰り返し呼び出しても何も行われないことです。そのため、各値はSetに一度だけ表示されます。

例えば、訪問者が来ており、全員を覚えたいとします。しかし、繰り返し訪問しても重複してはなりません。訪問者は一度だけ「カウント」される必要があります。

Setはまさにそのためにあるものです。

let set = new Set();

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

// visits, some users come multiple times
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// set keeps only unique values
alert( set.size ); // 3

for (let user of set) {
  alert(user.name); // John (then Pete and Mary)
}

Setの代替案としては、ユーザーの配列と、arr.findを使用して挿入ごとに重複をチェックするコードが考えられます。しかし、このメソッドは配列全体を走査してすべての要素をチェックするため、パフォーマンスははるかに悪くなります。Setは、一意性のチェックに関して内部的に最適化されています。

Setの反復処理

for..ofまたはforEachを使用してセットをループ処理できます。

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

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

// the same with forEach:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

面白いことに、forEachに渡されるコールバック関数は、value、そして同じ値valueAgain、そしてターゲットオブジェクトの3つの引数を持っています。実際、同じ値が引数に2回出現します。

これは、forEachに渡されるコールバックが3つの引数を持つMapとの互換性のためです。確かに少し奇妙に見えます。しかし、これにより、特定のケースでMapSetに簡単に置き換えたり、その逆を行ったりできる可能性があります。

イテレータ用のMapと同じメソッドもサポートされています。

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

概要

Map – キー付き値のコレクションです。

メソッドとプロパティ

  • new Map([iterable]) – マップを作成します。初期化のために[key,value]ペアのiterable(例えば配列)をオプションで指定できます。
  • map.set(key, value) – キーで値を格納します。マップ自体を返します。
  • map.get(key) – キーで値を返します。キーがマップに存在しない場合はundefinedを返します。
  • map.has(key) – キーが存在する場合はtrue、存在しない場合はfalseを返します。
  • map.delete(key) – キーで要素を削除します。呼び出し時にキーが存在した場合はtrue、存在しなかった場合はfalseを返します。
  • map.clear() – マップからすべてを削除します。
  • map.size – 現在の要素数を返します。

通常のObjectとの違い

  • あらゆるキー、オブジェクトをキーとして使用できます。
  • 便利な追加メソッド、sizeプロパティ。

Set – 一意の値のコレクションです。

メソッドとプロパティ

  • new Set([iterable]) – セットを作成します。初期化のために値のiterable(例えば配列)をオプションで指定できます。
  • set.add(value) – 値を追加します(value が既に存在する場合は何も行いません)。Set 自体を返します。
  • set.delete(value) – 値を削除します。呼び出し時に値が存在した場合はtrue、存在しなかった場合はfalseを返します。
  • set.has(value) – 値がセットに存在する場合はtrue、存在しない場合はfalseを返します。
  • set.clear() – セットからすべてを削除します。
  • set.size – 要素数です。

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

補足: ここでは文字列を使用していますが、任意の型の値を使用できます。

補足2: 一意な値を格納するために 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) {
    // split the word by letters, sort them and join back
    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 = word // 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 Object.values(obj);
}

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

alert( aclean(arr) );

テストを含む解答をサンドボックスで開きます。

重要度: 5

map.keys()の配列を変数に取得し、.pushなどの配列固有のメソッドを適用したいと考えています。

しかし、これは機能しません。

let map = new Map();

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

let keys = map.keys();

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

なぜでしょうか?keys.pushが機能するようにコードを修正するにはどうすればよいでしょうか?

これは、map.keys()がイテラブルを返すためですが、配列ではないためです。

Array.fromを使用して配列に変換できます。

let map = new Map();

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

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

keys.push("more");

alert(keys); // name, more
チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点の提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。