size 而不是 length?ECMAScript 6 中新增了以下四种数据结构:Map、WeakMap、Set 和 WeakSet。
映射的键可以是任意值
> const map = new Map(); // create an empty Map
> const KEY = {};
> map.set(KEY, 123);
> map.get(KEY)
123
> map.has(KEY)
true
> map.delete(KEY);
true
> map.has(KEY)
false
您可以使用包含 [键,值] 对的数组(或任何可迭代对象)来设置映射中的初始数据
const map = new Map([
[ 1, 'one' ],
[ 2, 'two' ],
[ 3, 'three' ], // trailing comma is ignored
]);
集合是唯一元素的集合
const arr = [5, 1, 5, 7, 7, 5];
const unique = [...new Set(arr)]; // [ 5, 1, 7 ]
如您所见,如果将可迭代对象(示例中的 arr)传递给构造函数,则可以使用元素初始化集合。
弱映射是一种映射,它不会阻止对其键进行垃圾回收。这意味着您可以将数据与对象相关联,而不必担心内存泄漏。例如
//----- Manage listeners
const _objToListeners = new WeakMap();
function addListener(obj, listener) {
if (! _objToListeners.has(obj)) {
_objToListeners.set(obj, new Set());
}
_objToListeners.get(obj).add(listener);
}
function triggerListeners(obj) {
const listeners = _objToListeners.get(obj);
if (listeners) {
for (const listener of listeners) {
listener();
}
}
}
//----- Example: attach listeners to an object
const obj = {};
addListener(obj, () => console.log('hello'));
addListener(obj, () => console.log('world'));
//----- Example: trigger listeners
triggerListeners(obj);
// Output:
// hello
// world
JavaScript 一直拥有非常简陋的标准库。非常缺少的是用于将值映射到值的数据结构。在 ECMAScript 5 中,您能获得的最佳选择是字符串到任意值的映射,方法是滥用对象。即使那样,也有一些陷阱可能会让您绊倒。
ECMAScript 6 中的 Map 数据结构允许您使用任意值作为键,这是非常受欢迎的。
处理单个条目
> const map = new Map();
> map.set('foo', 123);
> map.get('foo')
123
> map.has('foo')
true
> map.delete('foo')
true
> map.has('foo')
false
确定映射的大小并清除它
> const map = new Map();
> map.set('foo', true);
> map.set('bar', false);
> map.size
2
> map.clear();
> map.size
0
您可以通过键值“对”(包含 2 个元素的数组)的可迭代对象来设置映射。一种可能性是使用数组(它是可迭代的)
const map = new Map([
[ 1, 'one' ],
[ 2, 'two' ],
[ 3, 'three' ], // trailing comma is ignored
]);
或者,set() 方法是可链接的
const map = new Map()
.set(1, 'one')
.set(2, 'two')
.set(3, 'three');
任何值都可以是键,甚至是对象
const map = new Map();
const KEY1 = {};
map.set(KEY1, 'hello');
console.log(map.get(KEY1)); // hello
const KEY2 = {};
map.set(KEY2, 'world');
console.log(map.get(KEY2)); // world
大多数映射操作都需要检查一个值是否等于其中一个键。它们通过内部操作 SameValueZero 来实现,该操作的工作方式类似于 ===,但将 NaN 视为与其自身相等。
让我们首先看看 === 如何处理 NaN
> NaN === NaN
false
相反,您可以像使用任何其他值一样在映射中使用 NaN 作为键
> const map = new Map();
> map.set(NaN, 123);
> map.get(NaN)
123
与 === 一样,-0 和 +0 被视为相同的值。这通常是处理两个零的最佳方式(详细信息在“Speaking JavaScript”中解释)。
> map.set(-0, 123);
> map.get(+0)
123
不同的对象始终被视为不同的。这是无法配置的(目前),如常见问题解答中稍后所述。
> new Map().set({}, 1).set({}, 2).size
2
获取未知键会产生 undefined
> new Map().get('asfddfsasadf')
undefined
让我们设置一个映射来演示如何对其进行迭代。
const map = new Map([
[false, 'no'],
[true, 'yes'],
]);
映射记录插入元素的顺序,并在迭代键、值或条目时遵循该顺序。
keys() 返回映射中键的可迭代对象
for (const key of map.keys()) {
console.log(key);
}
// Output:
// false
// true
values() 返回映射中值的可迭代对象
for (const value of map.values()) {
console.log(value);
}
// Output:
// no
// yes
entries() 将映射的条目作为 [键,值] 对(数组)的可迭代对象返回。
for (const entry of map.entries()) {
console.log(entry[0], entry[1]);
}
// Output:
// false no
// true yes
解构使您能够直接访问键和值
for (const [key, value] of map.entries()) {
console.log(key, value);
}
迭代映射的默认方式是 entries()
> map[Symbol.iterator] === map.entries
true
因此,您可以使前面的代码段更短
for (const [key, value] of map) {
console.log(key, value);
}
展开运算符 (...) 可以将可迭代对象转换为数组。这使我们能够将 Map.prototype.keys() 的结果(一个可迭代对象)转换为数组
> const map = new Map().set(false, 'no').set(true, 'yes');
> [...map.keys()]
[ false, true ]
映射也是可迭代的,这意味着展开运算符可以将映射转换为数组
> const map = new Map().set(false, 'no').set(true, 'yes');
> [...map]
[ [ false, 'no' ],
[ true, 'yes' ] ]
Map 方法 forEach 具有以下签名
Map.prototype.forEach((value, key, map) => void, thisArg?) : void
第一个参数的签名反映了 Array.prototype.forEach 的回调的签名,这就是为什么值首先出现的原因。
const map = new Map([
[false, 'no'],
[true, 'yes'],
]);
map.forEach((value, key) => {
console.log(key, value);
});
// Output:
// false no
// true yes
您可以 map() 和 filter() 数组,但映射没有这样的操作。解决方案是
我将使用以下映射来演示它是如何工作的。
const originalMap = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
映射 originalMap
const mappedMap = new Map( // step 3
[...originalMap] // step 1
.map(([k, v]) => [k * 2, '_' + v]) // step 2
);
// Resulting Map: {2 => '_a', 4 => '_b', 6 => '_c'}
过滤 originalMap
const filteredMap = new Map( // step 3
[...originalMap] // step 1
.filter(([k, v]) => k < 3) // step 2
);
// Resulting Map: {1 => 'a', 2 => 'b'}
步骤 1 由展开运算符 (...) 执行,我之前已经解释过。
没有用于合并映射的方法,这就是为什么必须使用上一节中的方法来实现的原因。
让我们合并以下两个映射
const map1 = new Map()
.set(1, 'a1')
.set(2, 'b1')
.set(3, 'c1');
const map2 = new Map()
.set(2, 'b2')
.set(3, 'c2')
.set(4, 'd2');
为了合并 map1 和 map2,我通过展开运算符 (...) 将它们转换为数组,并将这些数组连接起来。之后,我将结果转换回映射。所有这些都在第一行完成。
> const combinedMap = new Map([...map1, ...map2])
> [...combinedMap] // convert to Array to display
[ [ 1, 'a1' ],
[ 2, 'b2' ],
[ 3, 'c2' ],
[ 4, 'd2' ] ]
如果映射包含任意(JSON 兼容)数据,我们可以通过将其编码为键值对数组(2 元素数组)将其转换为 JSON。让我们首先研究如何实现这种编码。
展开运算符 允许您将映射转换为键值对数组
> const myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
> [...myMap]
[ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
Map 构造函数允许您将键值对数组转换为映射
> new Map([[true, 7], [{foo: 3}, ['abc']]])
Map {true => 7, Object {foo: 3} => ['abc']}
让我们使用这些知识将任何包含 JSON 兼容数据的映射转换为 JSON 并返回
function mapToJson(map) {
return JSON.stringify([...map]);
}
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
以下交互演示了如何使用这些函数
> const myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
> mapToJson(myMap)
'[[true,7],[{"foo":3},["abc"]]]'
> jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
Map {true => 7, Object {foo: 3} => ['abc']}
只要映射仅包含字符串作为键,就可以通过将其编码为对象将其转换为 JSON。让我们首先研究如何实现这种编码。
以下两个函数在字符串映射和对象之间转换
function strMapToObj(strMap) {
const obj = Object.create(null);
for (const [k,v] of strMap) {
// We don’t escape the key '__proto__'
// which can cause problems on older engines
obj[k] = v;
}
return obj;
}
function objToStrMap(obj) {
const strMap = new Map();
for (const k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
让我们使用这两个函数
> const myMap = new Map().set('yes', true).set('no', false);
> strMapToObj(myMap)
{ yes: true, no: false }
> objToStrMap({yes: true, no: false})
[ [ 'yes', true ], [ 'no', false ] ]
使用这些辅助函数,转换为 JSON 的工作方式如下
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
这是使用这些函数的示例
> const myMap = new Map().set('yes', true).set('no', false);
> strMapToJson(myMap)
'{"yes":true,"no":false}'
> jsonToStrMap('{"yes":true,"no":false}');
Map {'yes' => true, 'no' => false}
构造函数
new Map(entries? : Iterable<[any,any]>)iterable,则会创建一个空映射。如果您确实提供了 [键,值] 对的可迭代对象,则这些对将用于向映射添加条目。例如 const map = new Map([
[ 1, 'one' ],
[ 2, 'two' ],
[ 3, 'three' ], // trailing comma is ignored
]);
处理单个条目
Map.prototype.get(key) : anykey 映射到的 value。如果此映射中没有键 key,则返回 undefined。Map.prototype.set(key, value) : thiskey 的条目,则更新该条目。否则,将创建一个新条目。此方法返回 this,这意味着您可以链接它。Map.prototype.has(key) : booleanMap.prototype.delete(key) : booleankey 的条目,则删除该条目并返回 true。否则,不执行任何操作并返回 false。处理所有条目
get Map.prototype.size : numberMap.prototype.clear() : void**迭代和循环:** 按将条目添加到映射中的顺序进行。
Map.prototype.entries() : Iterable<[any,any]>Map.prototype.forEach((value, key, collection) => void, thisArg?) : voidthisArg,则每次调用都会将 this 设置为它。否则,将 this 设置为 undefined。Map.prototype.keys() : Iterable<any>Map.prototype.values() : Iterable<any>Map.prototype[Symbol.iterator]() : Iterable<[any,any]>Map.prototype.entries。弱映射的工作方式与映射基本相同,但有以下区别
以下各节将解释这些差异。
如果向 WeakMap 添加条目,则键必须是对象。
const wm = new WeakMap()
wm.set('abc', 123); // TypeError
wm.set({}, 123); // OK
WeakMap 中的键是弱引用的:通常,任何存储位置(变量、属性等)都没有引用的对象都可以被垃圾回收。从这个意义上说,WeakMap 键不计为存储位置。换句话说:一个对象作为 WeakMap 中的键不会阻止该对象被垃圾回收。
此外,一旦键消失,其条目也将消失(最终会消失,但无论如何都无法检测到何时消失)。
无法检查 WeakMap 的内部结构,也无法获取它们的概览。这包括无法迭代键、值或条目。换句话说:要从 WeakMap 中获取内容,您需要一个键。也没有办法清除 WeakMap(作为一种解决方法,您可以创建一个全新的实例)。
这些限制实现了一种安全属性。引用 Mark Miller 的话:“只有同时拥有 WeakMap 和键的人才能观察或影响从 WeakMap/键对到值的映射。使用 clear(),只有 WeakMap 的人就可以影响 WeakMap 和键到值的映射。”
此外,迭代将难以实现,因为您必须保证键保持弱引用。
WeakMap 对于将数据与您无法(或不想)控制其生命周期的对象相关联非常有用。在本节中,我们将看两个例子
使用 WeakMap,您可以将先前计算的结果与对象相关联,而不必担心内存管理。以下函数 countOwnKeys 就是一个例子:它将先前结果缓存在 WeakMap cache 中。
const cache = new WeakMap();
function countOwnKeys(obj) {
if (cache.has(obj)) {
console.log('Cached');
return cache.get(obj);
} else {
console.log('Computed');
const count = Object.keys(obj).length;
cache.set(obj, count);
return count;
}
}
如果我们将此函数与对象 obj 一起使用,您可以看到结果仅在第一次调用时计算,而第二次调用则使用缓存值
> const obj = { foo: 1, bar: 2};
> countOwnKeys(obj)
Computed
2
> countOwnKeys(obj)
Cached
2
假设我们想在不更改对象的情况下将监听器附加到对象。您可以将监听器添加到对象 obj
const obj = {};
addListener(obj, () => console.log('hello'));
addListener(obj, () => console.log('world'));
并且您将能够触发监听器
triggerListeners(obj);
// Output:
// hello
// world
这两个函数 addListener() 和 triggerListeners() 可以如下实现。
const _objToListeners = new WeakMap();
function addListener(obj, listener) {
if (! _objToListeners.has(obj)) {
_objToListeners.set(obj, new Set());
}
_objToListeners.get(obj).add(listener);
}
function triggerListeners(obj) {
const listeners = _objToListeners.get(obj);
if (listeners) {
for (const listener of listeners) {
listener();
}
}
}
在这里使用 WeakMap 的优势在于,一旦一个对象被垃圾回收,它的监听器也会被垃圾回收。换句话说:不会有任何内存泄漏。
在以下代码中,WeakMap _counter 和 _action 用于存储 Countdown 实例的虚拟属性的数据
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
有关此技术的更多信息,请参见 关于类的章节。
WeakMap 的构造函数和四种方法的工作方式与其 Map 等效项相同
new WeakMap(entries? : Iterable<[any,any]>)
WeakMap.prototype.get(key) : any
WeakMap.prototype.set(key, value) : this
WeakMap.prototype.has(key) : boolean
WeakMap.prototype.delete(key) : boolean
ECMAScript 5 也没有 Set 数据结构。有两种可能的解决方法
indexOf() 检查它是否包含元素,通过 filter() 删除元素等。这不是一个非常快的解决方案,但它很容易实现。需要注意的一个问题是 indexOf() 找不到值 NaN。ECMAScript 6 具有数据结构 Set,它适用于任意值,速度快并且可以正确处理 NaN。
管理单个元素
> const set = new Set();
> set.add('red')
> set.has('red')
true
> set.delete('red')
true
> set.has('red')
false
确定 Set 的大小并清除它
> const set = new Set();
> set.add('red')
> set.add('green')
> set.size
2
> set.clear();
> set.size
0
您可以通过构成 Set 的元素的可迭代对象来设置 Set。例如,通过数组
const set = new Set(['red', 'green', 'blue']);
或者,add 方法是可链接的
const set = new Set().add('red').add('green').add('blue');
new Set() 最多只有一个参数 Set 构造函数有零个或一个参数
其他参数将被忽略,这可能会导致意外结果
> Array.from(new Set(['foo', 'bar']))
[ 'foo', 'bar' ]
> Array.from(new Set('foo', 'bar'))
[ 'f', 'o' ]
对于第二个 Set,仅使用 'foo'(它是可迭代的)来定义 Set。
与 Map 一样,元素的比较方式类似于 ===,但 NaN 与任何其他值一样。
> const set = new Set([NaN]);
> set.size
1
> set.has(NaN)
true
再次添加元素无效
> const set = new Set();
> set.add('foo');
> set.size
1
> set.add('foo');
> set.size
1
与 === 类似,两个不同的对象永远不会被认为是相等的(目前无法自定义,稍后将在常见问题解答中解释)
> const set = new Set();
> set.add({});
> set.size
1
> set.add({});
> set.size
2
Set 是可迭代的,并且 for-of 循环的工作方式与您预期的一样
const set = new Set(['red', 'green', 'blue']);
for (const x of set) {
console.log(x);
}
// Output:
// red
// green
// blue
如您所见,Set 保留了迭代顺序。也就是说,始终按照插入元素的顺序迭代元素。
前面解释的扩展运算符 (...) 适用于可迭代对象,因此您可以将 Set 转换为数组
const set = new Set(['red', 'green', 'blue']);
const arr = [...set]; // ['red', 'green', 'blue']
我们现在有一种简洁的方法可以将数组转换为 Set 并返回,其作用是从数组中消除重复项
const arr = [3, 5, 2, 2, 5, 5];
const unique = [...new Set(arr)]; // [3, 5, 2]
与数组相比,Set 没有 map() 和 filter() 方法。一种解决方法是将它们转换为数组并返回。
映射
const set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// Resulting Set: {2, 4, 6}
过滤
const set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// Resulting Set: {2, 4}
ECMAScript 6 Set 没有用于计算并集(例如 addAll)、交集(例如 retainAll)或差集(例如 removeAll)的方法。本节介绍如何解决该限制。
并集 (a ∪ b):创建一个包含 Set a 和 Set b 的元素的 Set。
const a = new Set([1,2,3]);
const b = new Set([4,3,2]);
const union = new Set([...a, ...b]);
// {1,2,3,4}
模式始终相同
扩展运算符 (...) 将可迭代对象(例如 Set)的元素插入到数组中。因此,[...a, ...b] 意味着 a 和 b 被转换为数组并连接起来。它等效于 [...a].concat([...b])。
交集 (a ∩ b):创建一个 Set,其中包含 Set a 中也存在于 Set b 中的元素。
const a = new Set([1,2,3]);
const b = new Set([4,3,2]);
const intersection = new Set(
[...a].filter(x => b.has(x)));
// {2,3}
步骤:将 a 转换为数组,过滤元素,将结果转换为 Set。
差集 (a \ b):创建一个 Set,其中包含 Set a 中不存在于 Set b 中的元素。此操作有时也称为减法 (-)。
const a = new Set([1,2,3]);
const b = new Set([4,3,2]);
const difference = new Set(
[...a].filter(x => !b.has(x)));
// {1}
构造函数
new Set(elements? : Iterable<any>)iterable,则会创建一个空的 Set。如果提供,则迭代的值将作为元素添加到 Set 中。例如 const set = new Set(['red', 'green', 'blue']);
单个 Set 元素
Set.prototype.add(value) : thisvalue 添加到此 Set。此方法返回 this,这意味着它可以链接。Set.prototype.has(value) : booleanvalue 是否在此 Set 中。Set.prototype.delete(value) : booleanvalue。所有 Set 元素
get Set.prototype.size : numberSet.prototype.clear() : void迭代和循环
Set.prototype.values() : Iterable<any>Set.prototype[Symbol.iterator]() : Iterable<any>Set.prototype.values。Set.prototype.forEach((value, key, collection) => void, thisArg?)value 和 key 都设置为该元素,因此此方法的工作方式类似于 Map.prototype.forEach。如果提供了 thisArg,则每次调用都会将 this 设置为它。否则,this 将设置为 undefined。与 Map 的对称性:以下两种方法仅存在是为了使 Set 的接口类似于 Map 的接口。每个 Set 元素的处理方式就好像它是一个 Map 条目,其键和值都是该元素。
Set.prototype.entries() : Iterable<[any,any]>Set.prototype.keys() : Iterable<any>entries() 允许您将 Set 转换为 Map
const set = new Set(['a', 'b', 'c']);
const map = new Map(set.entries());
// Map { 'a' => 'a', 'b' => 'b', 'c' => 'c' }
WeakSet 是一个 Set,它不会阻止其元素被垃圾回收。有关 WeakSet 不允许迭代、循环和清除的原因,请参阅有关 WeakMap 的部分。
鉴于您无法迭代它们的元素,因此 WeakSet 的用例并不多。它们确实允许您标记对象。
例如,如果您有一个用于代理的工厂函数,则可以使用 WeakSet 来记录哪些对象是由该工厂创建的
const _proxies = new WeakSet();
function createProxy(obj) {
const proxy = ···;
_proxies.add(proxy);
return proxy;
}
function isProxy(obj) {
return _proxies.has(obj);
}
完整示例显示在 关于代理的章节 中。
_proxies 必须是 WeakSet,因为一旦不再引用代理,正常的 Set 将阻止代理被垃圾回收。
Domenic Denicola 展示了 类 Foo 如何确保其方法仅应用于由它创建的实例
const foos = new WeakSet();
class Foo {
constructor() {
foos.add(this);
}
method() {
if (!foos.has(this)) {
throw new TypeError('Incompatible object!');
}
}
}
WeakSet 的构造函数和三种方法的工作方式与其 Set 等效项相同
new WeakSet(elements? : Iterable<any>)
WeakSet.prototype.add(value)
WeakSet.prototype.has(value)
WeakSet.prototype.delete(value)
size 而不是 length? 数组具有属性 length 来计算条目的数量。Map 和 Set 具有不同的属性 size。
之所以会有这种差异,是因为 length 用于序列,即可以索引的数据结构,例如数组。而 size 用于主要无序的集合,例如映射和集合。
如果有一种方法可以配置哪些映射键和哪些集合元素被认为是相等的,那就太好了。但该功能已被推迟,因为它难以正确有效地实现。
如果您使用键从映射中获取内容,您有时希望指定一个默认值,以便在键不在映射中时返回该值。ES6 映射不允许您直接这样做。但您可以使用 Or 运算符 (||),如下面的代码所示。countChars 返回一个将字符映射到出现次数的映射。
function countChars(chars) {
const charCounts = new Map();
for (const ch of chars) {
ch = ch.toLowerCase();
const prevCount = charCounts.get(ch) || 0; // (A)
charCounts.set(ch, prevCount+1);
}
return charCounts;
}
在 A 行中,如果 ch 不在 charCounts 中并且 get() 返回 undefined,则使用默认值 0。
如果您将字符串以外的任何内容映射到任何类型的数据,则别无选择:您必须使用映射。
但是,如果您要将字符串映射到任意数据,则必须决定是否使用对象。一个粗略的通用准则是
obj.keymap.get(theKey)如果映射键是按值比较的(相同的“内容”意味着两个值被认为是相等的,而不是相同的身份),那么它们才有意义。这排除了对象。有一种用例——将数据外部附加到对象,但这种用例最好由 WeakMap 来处理,在 WeakMap 中,当键消失时,条目也会消失。