web-dev-qa-db-ja.com

JavaScriptでのマップとオブジェクト

Chromestatus.comを発見しましたが、1日の数時間を失った後、 この機能エントリ :を見つけました。

マップ:マップオブジェクトは、単純なキー/値マップです。

それは私を混乱させました。通常のJavaScriptオブジェクトは辞書なので、Mapは辞書とどう違うのですか?概念的には、それらは同一です( マップと辞書の違いは何ですか?

Chromestatus参照のドキュメントも役に立ちません:

マップオブジェクトは、キーと値の両方が任意のECMAScript言語値であるキー/値ペアのコレクションです。明確なキー値は、マップのコレクション内の1つのキー/値ペアでのみ発生します。マップの作成時に選択される比較アルゴリズムを使用して区別される、異なるキー値。

Mapオブジェクトは、挿入順に要素を反復できます。マップオブジェクトは、ハッシュテーブルまたは他のメカニズムを使用して実装する必要があります。これらのメカニズムは、平均して、コレクション内の要素数に比例しないアクセス時間を提供します。このMapオブジェクト仕様で使用されるデータ構造は、Mapオブジェクトに必要な観察可能なセマンティクスを記述することのみを目的としています。実行可能な実装モデルになることを意図していません。

…まだ私には物のように聞こえるので、明らかに私は何かを見逃しました。

JavaScriptが(十分にサポートされている)Mapオブジェクトを取得しているのはなぜですか?それは何をするためのものか?

219
Dave

Mozillaによると:

Mapオブジェクトは、要素を挿入順に反復できます。for..ofループは、反復ごとに[key、value]の配列を返します。

そして

オブジェクトは、キーを値に設定し、それらの値を取得し、キーを削除し、キーに何かが保存されているかどうかを検出できるという点でマップに似ています。このため、オブジェクトは歴史的にマップとして使用されてきました。ただし、オブジェクトとマップの間には、マップの使用を改善する重要な違いがあります。

オブジェクトにはプロトタイプがあるため、マップにはデフォルトのキーがあります。ただし、これはmap = Object.create(null)を使用してバイパスできます。オブジェクトのキーは文字列であり、マップの任意の値にすることができます。オブジェクトのサイズを手動で追跡する必要がある間、マップのサイズを簡単に取得できます。

キーが実行時まで不明で、すべてのキーが同じタイプで、すべての値が同じタイプである場合、オブジェクトにマップを使用します。

個々の要素を操作するロジックがある場合は、オブジェクトを使用します。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

反復可能性は、すべてのブラウザーで同じパフォーマンスを保証することもあるため、開発者が長らく求めていた機能です。私にとってそれは大きなものです。

myMap.has(key)メソッドは特に便利で、myMap.sizeプロパティも便利です。

211
user2625787

主な違いは、オブジェクトは文字列キーのみをサポートするという点です。Mapsは多かれ少なかれ任意のキータイプをサポートします。

Obj [123] = trueを実行してからObject.keys(obj)を実行すると、[123]ではなく["123"]が取得されます。 Mapはキーのタイプを保持し、[123]を返しますが、これは素晴らしいことです。マップでは、オブジェクトをキーとして使用することもできます。従来、これを行うには、オブジェクトをハッシュするためのある種の一意の識別子を指定する必要がありました(標準の一部としてJSのgetObjectIdのようなものを見たことはないと思います)。また、マップは順序の保存を保証するため、保存のすべての面で優れており、場合によってはいくつかの並べ替えを行う必要がなくなります。

実際には、マップとオブジェクトの間には、いくつかの長所と短所があります。オブジェクトは、JSのコアに非常に緊密に統合されているため、長所と短所の両方が得られます。

当面の利点は、オブジェクトへの構文サポートがあり、要素に簡単にアクセスできることです。 JSONを使用して直接サポートすることもできます。ハッシュとして使用する場合、プロパティがまったくないオブジェクトを取得するのは面倒です。デフォルトでは、オブジェクトをハッシュテーブルとして使用する場合、それらは汚染され、プロパティにアクセスするときにオブジェクトに対してhasOwnPropertyを呼び出す必要があります。ここでは、デフォルトでオブジェクトがどのように汚染されているか、ハッシュとして使用するために汚染されていないオブジェクトを作成する方法を見ることができます:

({}).toString
    toString() { [native code] }
JSON.parse('{}').toString
    toString() { [native code] }
(Object.create(null)).toString
    undefined
JSON.parse('{}', (k,v) => (typeof v === 'object' && Object.setPrototypeOf(v, null) ,v)).toString
    undefined

オブジェクトの汚染は、コードを煩わしくしたり、遅くしたりするだけでなく、セキュリティに潜在的な影響を与える可能性があります。

オブジェクトは純粋なハッシュテーブルではありませんが、より多くのことを試みています。 hasOwnPropertyのような頭痛があり、長さ(Object.keys(obj).length)などを簡単に取得できません。オブジェクトは、純粋にハッシュマップとして使用されることを意図したものではなく、動的に拡張可能なオブジェクトとしても使用されるため、純粋なハッシュテーブルの問題が発生したときにそれらを使用します。

さまざまな一般的な操作の比較/リスト:

    Object:
       var o = {};
       var o = Object.create(null);
       o.key = 1;
       o.key += 10;
       for(let k in o) o[k]++;
       var sum = 0;
       for(let v of Object.values(m)) sum += v;
       if('key' in o);
       if(o.hasOwnProperty('key'));
       delete(o.key);
       Object.keys(o).length
    Map:
       var m = new Map();
       m.set('key', 1);
       m.set('key', m.get('key') + 10);
       m.foreach((k, v) => m.set(k, m.get(k) + 1));
       for(let k of m.keys()) m.set(k, m.get(k) + 1);
       var sum = 0;
       for(let v of m.values()) sum += v;
       if(m.has('key'));
       m.delete('key');
       m.size();

他にもいくつかのオプションがあり、アプローチ、方法論など、さまざまな浮き沈みがあります(パフォーマンス、簡潔、ポータブル、拡張可能など)。オブジェクトは言語の中核であることは少し奇妙なので、オブジェクトを操作するための静的メソッドがたくさんあります。

キーの種類を保持するマップの利点に加えて、オブジェクトのようなものをキーとしてサポートできることの他に、オブジェクトが持つ副作用から隔離されています。 Mapは純粋なハッシュです。同時にオブジェクトになろうとすることについて混乱はありません。マップは、プロキシ機能を使用して簡単に拡張することもできます。現在、オブジェクトにはProxyクラスがありますが、パフォーマンスとメモリ使用量は厳しいです。実際、Map for Objectsのように見える独自のプロキシを作成することは、現在、Proxyよりも優れています。

Mapsの大きな欠点は、JSONで直接サポートされていないことです。解析は可能ですが、いくつかのハングアップがあります。

JSON.parse(str, (k,v) => {
    if(typeof v !== 'object') return v;
    let m = new Map();
    for(k in v) m.set(k, v[k]);
    return m;
});

上記は深刻なパフォーマンスヒットをもたらし、文字列キーもサポートしません。 JSONエンコードはさらに難しく、問題があります(これは多くのアプローチの1つです)。

// An alternative to this it to use a replacer in JSON.stringify.
Map.prototype.toJSON = function() {
    return JSON.stringify({
        keys: Array.from(this.keys()),
        values: Array.from(this.values())
    });
};

Mapsを純粋に使用している場合、これはそれほど悪くはありませんが、タイプを混合したり、キーとして非スカラー値を使用している場合に問題が発生します(JSONは、そのような問題をそのままに、IE循環オブジェクト参照)。私はそれをテストしていませんが、文字列化と比較してパフォーマンスが大幅に低下する可能性があります。

他のスクリプト言語では、Map、Object、およびArrayに対して明示的な非スカラー型があるため、多くの場合、このような問題は発生しません。 Web開発は、PHPがプロパティにA/Mを使用してオブジェクトとArray/Mapをマージし、JSがM/Oを拡張する配列とMap/Objectをマージするようなものに対処する必要がある非スカラータイプでは、しばしば苦痛です。 。複合型のマージは、高レベルのスクリプト言語の悪魔の悩みの種です。

これまでのところ、これらは主に実装に関する問題ですが、基本的な操作のパフォーマンスも重要です。エンジンと使用方法に依存するため、パフォーマンスも複雑です。間違いを排除することはできないので、一粒の塩でテストを受けてください(これを急ぐ必要があります)。また、独自のテストを実行して、非常に具体的な単純なシナリオのみを調べて、大まかな目安を示すことを確認してください。非常に大きなオブジェクト/マップのChromeのテストによると、オブジェクトのパフォーマンスは、O(1)ではなく、キーの数に何らかの形で比例しているように見えるため、より悪いです。

Object Set Took: 146
Object Update Took: 7
Object Get Took: 4
Object Delete Took: 8239
Map Set Took: 80
Map Update Took: 51
Map Get Took: 40
Map Delete Took: 2

Chromeは、取得と更新に関して明らかに強力な利点を持っていますが、削除のパフォーマンスは恐ろしいものです。この場合、マップはわずかな量のメモリ(オーバーヘッド)を使用しますが、数百万のキーでテストされているオブジェクト/マップは1つだけなので、マップのオーバーヘッドの影響は適切に表現されません。メモリ管理では、オブジェクトを優先する利点の1つである可能性があるプロファイルを正しく読み取っていれば、オブジェクトも以前に解放されているように見えます。

この特定のベンチマークのFireFoxでは、別の話です。

Object Set Took: 435
Object Update Took: 126
Object Get Took: 50
Object Delete Took: 2
Map Set Took: 63
Map Update Took: 59
Map Get Took: 33
Map Delete Took: 1

この特定のベンチマークでは、FireFoxのオブジェクトから削除しても問題は発生しませんが、他のベンチマークでは、特にChromeのように多くのキーがある場合に問題が発生します。 FireFoxでは、大規模なコレクションのマップが明らかに優れています。

しかし、これで話は終わりではありません。多くの小さなオブジェクトやマップはどうでしょうか?私はこれの簡単なベンチマークを行いましたが、上記の操作で少数のキーで最高のパフォーマンス(設定/取得)が得られるわけではありません。このテストは、メモリと初期化に関するものです。

Map Create: 69    // new Map
Object Create: 34 // {}

繰り返しますが、これらの数字は異なりますが、基本的にオブジェクトは良いリードを持っています。マップを介したオブジェクトのリードは極端な場合(最大10倍)ですが、平均で約2倍から3倍でした。極端なパフォーマンスの急上昇は、両方の方法で機能するようです。これをChromeと作成でテストし、メモリ使用量とオーバーヘッドをプロファイルしました。 Chromeで、1つのキーを持つマップが1つのキーを持つオブジェクトの約30倍のメモリを使用しているように見えることに非常に驚きました。

上記のすべての操作(4つのキー)で多くの小さなオブジェクトをテストするには:

Chrome Object Took: 61
Chrome Map Took: 67
FireFox Object Took: 54
FireFox Map Took: 139

メモリ割り当ての点では、これらは解放/ GCの点では同じように動作しましたが、Mapは5倍のメモリを使用しました。このテストでは4つのキーを使用しましたが、前回のテストのように1つのキーのみを設定したため、メモリオーバーヘッドの削減を説明できます。このテストを数回実行しましたが、Map/Objectは全体的な速度の面でChromeに対して全体的に多少なりとも首と首が一致しています。 FireFox for Small Objectsには、マップ全体に比べて明確なパフォーマンス上の利点があります。

もちろんこれには、大幅に異なる可能性のある個々のオプションは含まれていません。これらの数値を使用して微最適化することはお勧めしません。これから得られることは、経験則として、非常に大きなキーバリューストアと小さなキーバリューストアのオブジェクトにはマップをより強く考慮することです。

それを超えて、これら2つの最高の戦略を実装し、最初に機能させるだけです。プロファイリングを行うとき、オブジェクトキーの削除の場合に見られるようなエンジンの奇妙な動作のために、見たときに遅くないと思われるものが信じられないほど遅くなる場合があることに留意することが重要です。

83
jgmjgm

これまでの回答で次の点が言及されているとは思わないので、言及する価値があると思いました。


マップは大きくなる可能性があります

chromeでは、Mapname__で16.7ミリオン対通常のオブジェクトで11.1ミリオンを取得できます。 Mapname__との組み合わせは、ほぼ50%正確です。どちらもクラッシュする前に約2GBのメモリを占有するため、chrome(Editによるメモリ制限と関係があるのではないかと思います。はい、2 Mapsname__を入力してくださいクラッシュするまでに、それぞれ830万ペアしか取得できません)。このコードを使用して自分でテストできます(明らかに同時にではなく、個別に実行します)。

var m = new Map();
var i = 0;
while(1) {
    m.set(((10**30)*Math.random()).toString(36), ((10**30)*Math.random()).toString(36));
    i++;
    if(i%1000 === 0) { console.log(i/1000,"thousand") }
}
// versus:
var m = {};
var i = 0;
while(1) {
    m[((10**30)*Math.random()).toString(36)] = ((10**30)*Math.random()).toString(36);
    i++;
    if(i%1000 === 0) { console.log(i/1000,"thousand") }
}

オブジェクトにはすでにいくつかのプロパティ/キーがあります

これは以前に私をつまずかせました。通常のオブジェクトには、toStringname __、constructorname __、valueOfname __、hasOwnPropertyname __、isPrototypeOfname__、およびその他の既存のプロパティがあります。これはほとんどのユースケースでは大きな問題ではないかもしれませんが、以前は問題を引き起こしていました。

マップが遅くなる場合があります。

.get関数呼び出しのオーバーヘッドと内部最適化の欠如により、Map かなり遅くなる可能性があります いくつかのタスクの単純な古いJavaScriptオブジェクトよりも。

19
user993683

他の答えに加えて、Mapsはオブジェクトよりも扱いにくく、操作が難しいことがわかりました。

obj[key] += x
// vs.
map.set(map.get(key) + x)

これは重要です。短いコードは読みやすく、より直接的に表現力があり、より良い プログラマーの頭に留めておく

別の側面:set()は値ではなくマップを返すため、割り当てを連鎖させることはできません。

foo = obj[key] = x;  // Does what you expect
foo = map.set(key, x)  // foo !== x; foo === map

マップのデバッグも苦痛です。以下では、マップ内のキーを実際に見ることはできません。そのためにはコードを書く必要があります。

Good luck evaluating a Map Iterator

オブジェクトはどのIDEでも評価できます。

WebStorm evaluating an object

6
Dan Dascalescu

Javascriptは動的に入力されるため、オブジェクトは辞書のように動作しますが、実際にはそうではありません。

新しいMap()機能は、通常のget/set/has/deleteメソッドを持ち、単なる文字列ではなくキーの任意の型を受け入れ、反復するときに使いやすく、プロトタイプやその他のプロパティが表示されるEdgeケースがないため、はるかに優れています。また、非常に高速であり、エンジンが良くなるにつれて高速になり続けます。 99%の時間で、Map()を使用する必要があります。

ただし、文字列ベースのキーのみを使用しており、最大の読み取りパフォーマンスが必要な場合は、オブジェクトの方が適しています。詳細は、(ほとんどすべての)JavaScriptエンジンがオブジェクトをバックグラウンドでC++クラスにコンパイルすることです。これらのタイプは「アウトライン」によってキャッシュおよび再利用されるため、まったく同じプロパティを持つ新しいオブジェクトを作成すると、エンジンは既存のバックグラウンドクラスを再利用します。これらのクラスのプロパティへのアクセスパスは非常に最適化されており、Map()のルックアップよりもはるかに高速です。

プロパティを追加または削除すると、キャッシュされたバッキングクラスが再コンパイルされます。これが、多くのキーの追加と削除を伴う辞書としてのオブジェクトの使用が非常に遅い理由ですが、オブジェクトを変更せずに既存のキーの読み取りと割り当てが非常に高速です。

したがって、文字列キーを使用した1回だけの読み取りが多いワークロードがある場合は、objectを特殊な高性能辞書として使用しますが、他のすべての場合はMap()を使用します

4
Mani Gandham

概要:

  • Object:データがキーと値のペアとして格納されるデータ構造。オブジェクトでは、キーは数字、文字列、または記号でなければなりません。値は任意であるため、他のオブジェクト、関数なども使用できます。オブジェクトは、非順序付きデータ構造です。つまり、キーと値のペアの挿入シーケンスは覚えていない
  • ES6 Map:データがキーと値のペアとして保存されるデータ構造。ここで、一意のキーは値にマッピングされます。キーと値の両方は、任意のデータ型になります。マップは反復可能なデータ構造です。これは、挿入の順序が記憶され、たとえばfor..ofループ

主な違い:

  • Mapは順序付けられており、反復可能ですが、オブジェクトは順序付けられておらず反復可能ではありません

  • オブジェクトはMapキーとして任意のタイプのデータを配置できますが、オブジェクトはキーとして数値、文字列、またはシンボルのみを持つことができます。

  • MapMap.prototypeを継承します。これにより、あらゆる種類のユーティリティ関数とプロパティが提供され、Mapオブジェクトの操作が非常に簡単になります。

例:

オブジェクト:

let obj = {};

// adding properties to a object
obj.prop1 = 1;
obj[2]    =  2;

// getting nr of properties of the object
console.log(Object.keys(obj).length)

// deleting a property
delete obj[2]

console.log(obj)

マップ:

const myMap = new Map();

const keyString = 'a string',
    keyObj = {},
    keyFunc = function() {};

// setting the values
myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, 'value associated with keyObj');
myMap.set(keyFunc, 'value associated with keyFunc');

console.log(myMap.size); // 3

// getting the values
console.log(myMap.get(keyString));    // "value associated with 'a string'"
console.log(myMap.get(keyObj));       // "value associated with keyObj"
console.log(myMap.get(keyFunc));      // "value associated with keyFunc"

console.log(myMap.get('a string'));   // "value associated with 'a string'"
                         // because keyString === 'a string'
console.log(myMap.get({}));           // undefined, because keyObj !== {}
console.log(myMap.get(function() {})) // undefined, because keyFunc !== function () {}

ソース:MDN

3

明確に定義された順序で反復可能であり、任意の値をキーとして使用する機能(-0を除く)に加えて、マップは次の理由で便利です。

  • 仕様では、マップ操作が平均的に準線形になるように強制しています。

    オブジェクトの非愚かな実装では、ハッシュテーブルなどが使用されるため、プロパティルックアップはおそらく平均して一定です。その場合、オブジェクトはマップよりも高速になる可能性があります。しかし、それは仕様では要求されていません。

  • オブジェクトには、予期しない動作が発生する可能性があります。

    たとえば、新しく作成されたオブジェクトfooobjプロパティを設定しなかったため、obj.fooが未定義を返すと想定します。ただし、fooは、Object.prototypeから継承された組み込みプロパティにすることができます。または、割り当てを使用してobj.fooを作成しようとしますが、値を保存する代わりにObject.prototypeの一部のセッターが実行されます。

    マップはこのようなことを防ぎます。ええ、Map.prototypeで混乱するスクリプトがない限り。また、Object.create(null)も機能しますが、単純なオブジェクト初期化構文は失われます。

3
Oriol

次の2つのヒントは、マップを使用するかオブジェクトを使用するかを決定するのに役立ちます。

  • キーが実行時まで不明で、すべてのキーが同じタイプで、すべての値が同じタイプである場合、オブジェクトにマップを使用します。

  • オブジェクトは各キーを数値、ブール値、またはその他のプリミティブ値のいずれかの文字列として扱うため、プリミティブ値をキーとして保存する必要がある場合にマップを使用します。

  • 個々の要素を操作するロジックがある場合は、オブジェクトを使用します。

ソース: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Keyed_Collections#Object_and_Map_compared

0
Cakedy