Maps

如果你对JS经验丰富,那么你一定知道对象是创建无序键/值对数据结构的主要机制,这也被称为map。然而,将对象作为map的主要缺陷是不能使用一个非字符串值作为键。

例如,考虑如下代码:

  1. var m = {};
  2. var x = { id: 1 },
  3. y = { id: 2 };
  4. m[x] = "foo";
  5. m[y] = "bar";
  6. m[x]; // "bar"
  7. m[y]; // "bar"

这里发生了什么?xy这两个对象都被字符串化为"[object Object]",所以只有这一个键被设置为m

一些人通过在一个值的数组旁边同时维护一个平行的非字符串键的数组实现了山寨的map,比如:

  1. var keys = [], vals = [];
  2. var x = { id: 1 },
  3. y = { id: 2 };
  4. keys.push( x );
  5. vals.push( "foo" );
  6. keys.push( y );
  7. vals.push( "bar" );
  8. keys[0] === x; // true
  9. vals[0]; // "foo"
  10. keys[1] === y; // true
  11. vals[1]; // "bar"

当然,你不会想亲自管理这些平行数组,所以你可能会定义一个数据解构,使它内部带有自动管理的方法。除了你不得不自己做这些工作,主要的缺陷是访问的时间复杂度不再是O(1),而是O(n)。

但在ES6中,不再需要这么做了!使用Map(..)就好:

  1. var m = new Map();
  2. var x = { id: 1 },
  3. y = { id: 2 };
  4. m.set( x, "foo" );
  5. m.set( y, "bar" );
  6. m.get( x ); // "foo"
  7. m.get( y ); // "bar"

唯一的缺点是你不能使用[]方括号访问语法来设置或取得值。但是get(..)set(..)可以完美地取代这种语法。

要从一个map中删除一个元素,不要使用delete操作符,而是使用delete(..)方法:

  1. m.set( x, "foo" );
  2. m.set( y, "bar" );
  3. m.delete( y );

使用clear()你可清空整个map的内容。要得到map的长度(也就是,键的数量),使用size属性(不是length)。

  1. m.set( x, "foo" );
  2. m.set( y, "bar" );
  3. m.size; // 2
  4. m.clear();
  5. m.size; // 0

Map(..)的构造器还可以接受一个可迭代对象(参见第三章的“迭代器”),它必须产生一个数组的列表,每个数组的第一个元素是键,第二元素是值。这种用于迭代的格式与entries()方法产生的格式是一样的,entries()方法将在下一节中讲解。这使得制造一个map的拷贝十分简单:

  1. var m2 = new Map( m.entries() );
  2. // 等同于:
  3. var m2 = new Map( m );

因为一个map实例是一个可迭代对象,而且它的默认迭代器与entries()相同,第二种稍短的形式更理想。

当然,你可以在Map(..)构造器形式中手动指定一个 entries 列表:

  1. var x = { id: 1 },
  2. y = { id: 2 };
  3. var m = new Map( [
  4. [ x, "foo" ],
  5. [ y, "bar" ]
  6. ] );
  7. m.get( x ); // "foo"
  8. m.get( y ); // "bar"

Map 值

要从一个map得到值的列表,使用values(..),它返回一个迭代器。在第二和第三章,我们讲解了几种序列化(像一个数组那样)处理一个迭代器的方法,比如...扩散操作符和for..of循环。另外,第六章的“Arrays”将会详细讲解Array.from(..)方法。考虑如下代码:

  1. var m = new Map();
  2. var x = { id: 1 },
  3. y = { id: 2 };
  4. m.set( x, "foo" );
  5. m.set( y, "bar" );
  6. var vals = [ ...m.values() ];
  7. vals; // ["foo","bar"]
  8. Array.from( m.values() ); // ["foo","bar"]

就像在前一节中讨论过的,你可以使用entries()(或者默认的map迭代器)迭代一个map的记录。考虑如下代码:

  1. var m = new Map();
  2. var x = { id: 1 },
  3. y = { id: 2 };
  4. m.set( x, "foo" );
  5. m.set( y, "bar" );
  6. var vals = [ ...m.entries() ];
  7. vals[0][0] === x; // true
  8. vals[0][1]; // "foo"
  9. vals[1][0] === y; // true
  10. vals[1][1]; // "bar"

Map 键

要得到键的列表,使用keys(),它返回一个map中键的迭代器:

  1. var m = new Map();
  2. var x = { id: 1 },
  3. y = { id: 2 };
  4. m.set( x, "foo" );
  5. m.set( y, "bar" );
  6. var keys = [ ...m.keys() ];
  7. keys[0] === x; // true
  8. keys[1] === y; // true

要判定一个map中是否拥有一个给定的键,使用has(..)

  1. var m = new Map();
  2. var x = { id: 1 },
  3. y = { id: 2 };
  4. m.set( x, "foo" );
  5. m.has( x ); // true
  6. m.has( y ); // false

实质上map让你将一些额外的信息(值)与一个对象(键)相关联,而不用实际上将这些信息放在对象本身中。

虽然在一个map中你可以使用任意种类的值作为键,但是你经常使用的将是对象,就像字符串和其他在普通对象中可以合法地作为键的基本类型。换句话说,你可能将想要继续使用普通对象,除非一些或全部的键需要是对象,在那种情况下map更合适。

警告: 如果你使用一个对象作为一个map键,而且这个对象稍后为了能够被垃圾回收器(GC)回收它占用的内存而被丢弃(解除所有的引用),那么map本身将依然持有它的记录。你需要从map中移除这个记录来使它能够被垃圾回收。在下一节中,我们将看到对于作为对象键和GC来说更好的选择 —— WeakMaps。