JavaScript高级教程-集合引用类型(二)
四、Map
作为 ECMAScript 6 的新增特性,Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。
1. 基本 API
使用 new 关键字和 Map 构造函数可以创建一个空映射:
const m = new Map();
如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含键/值对数组。可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中:
// 使用嵌套数组初始化映射 const m1 = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]); alert(m1.size); // 3 // 使用自定义迭代器初始化映射 const m2 = new Map({ [Symbol.iterator]: function*() { yield ["key1", "val1"]; yield ["key2", "val2"]; yield ["key3", "val3"]; } }); alert(m2.size); // 3 // 映射期待的键/值对,无论是否提供 const m3 = new Map([[]]); alert(m3.has(undefined)); // true alert(m3.get(undefined)); // undefined
初始化之后,可以使用 set()方法再添加键/值对。另外,可以使用 get()和 has()进行查询,可以通过 size 属性获取映射中的键/值对的数量,还可以使用 delete()和 clear()删除值。
const m = new Map(); alert(m.has("firstName")); // false alert(m.get("firstName")); // undefined alert(m.size); // 0 m.set("firstName", "Matt") .set("lastName", "Frisbie"); //set()方法返回映射实例,因此可以把多个操作连缀起来,包括初始化声明的时候 alert(m.has("firstName")); // true alert(m.get("firstName")); // Matt alert(m.size); // 2 m.delete("firstName"); // 只删除这一个键/值对 alert(m.has("firstName")); // false alert(m.has("lastName")); // true alert(m.size); // 1 m.clear(); // 清除这个映射实例中的所有键/值对 alert(m.has("firstName")); // false alert(m.has("lastName")); // false alert(m.size); // 0
与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。Map 内部使用 SameValueZero 比较操作(ECMAScript 规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。与 Object 类似,映射的值是没有限制的。
const m = new Map(); const functionKey = function() {}; const symbolKey = Symbol(); const objectKey = new Object(); m.set(functionKey, "functionValue"); m.set(symbolKey, "symbolValue"); m.set(objectKey, "objectValue"); alert(m.get(functionKey)); // functionValue alert(m.get(symbolKey)); // symbolValue alert(m.get(objectKey)); // objectValue // SameValueZero 比较意味着独立实例不冲突 alert(m.get(function() {})); // undefined
与严格相等一样,在映射中用作键和值的对象及其他“集合”类型,在自己的内容或属性被修改时仍然保持不变:
const m = new Map(); const objKey = {}, objVal = {}, arrKey = [], arrVal = []; m.set(objKey, objVal); m.set(arrKey, arrVal); objVal.bar = "bar"; arrVal.push("bar"); console.log(m.get(objKey)); // {bar: "bar"} console.log(m.get(arrKey)); // ["bar"]
SameValueZero 比较也可能导致意想不到的冲突:
const m = new Map(); const a = 0/"", // NaN b = 0/"", // NaN pz = +0, nz = -0; alert(a === b); // false alert(pz === nz); // true m.set(a, "foo"); m.set(pz, "bar"); alert(m.get(b)); // foo alert(m.get(nz)); // bar
2. 顺序与迭代
与 Object 类型的一个主要差异是,Map 实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。
映射实例可以提供一个迭代器(Iterator),能以插入顺序生成[key, value]形式的数组。可以通过 entries()方法(或者 Symbol.iterator 属性,它引用 entries())取得这个迭代器:const m = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]); alert(m.entries === m[Symbol.iterator]); // true for (let pair of m.entries()) { alert(pair); } // [key1,val1] // [key2,val2] // [key3,val3] for (let pair of m[Symbol.iterator]()) { alert(pair); } // [key1,val1] // [key2,val2] // [key3,val3]
因为 entries()是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:
console.log([...m]); // [[key1,val1],[key2,val2],[key3,val3]]
如果不使用迭代器,而是使用回调方式,则可以调用映射的forEach(callback, opt_thisArg)方法并传入回调,依次迭代每个键/值对。传入的回调接收可选的第二个参数,这个参数用于重写回调内部 this 的值:
m.forEach((val, key) => alert(`${key} -> ${val}`)); // key1 -> val1 // key2 -> val2 // key3 -> val3
keys()和 values()分别返回以插入顺序生成键和值的迭代器:
for (let key of m.keys()) { alert(key); } // key1 // key2 // key3 for (let key of m.values()) { alert(key); } // value1 // value2 // value3
键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。当然,这并不妨碍修改作为键或值的对象内部的属性,因为这样并不影响它们在映射实例中的身份:
const m1 = new Map([ ["key1", "val1"] ]); // 作为键的字符串原始值是不能修改的 for (let key of m1.keys()) { key = "newKey"; alert(key); // newKey alert(m1.get("key1")); // val1 } const keyObj = {id: 1}; const m = new Map([ [keyObj, "val1"] ]); // 修改了作为键的对象的属性,但对象在映射内部仍然引用相同的值 for (let key of m.keys()) { key.id = "newKey"; alert(key); // {id: "newKey"} alert(m.get(keyObj)); // val1 } alert(keyObj); // {id: "newKey"}
五、WeakMap
ECMAScript 6 新增的“弱映射”(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/值对存储机制。WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集。WeakMap 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式。
1. 基本 API
可以使用 new 关键字实例化一个空的 WeakMap:
const wm = new WeakMap();
弱映射中的键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出TypeError。值的类型没有限制。
如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键/值对数组。可迭代对象中的每个键/值都会按照迭代顺序插入新实例中:const key1 = {id: 1}, key2 = {id: 2}; // 使用嵌套数组初始化弱映射 const wm1 = new WeakMap([ [key1, "val1"], [key2, "val2"] ]); alert(wm1.get(key1)); // val1 alert(wm1.get(key2)); // val2
// 初始化是全有或全无的操作 // 只要有一个键无效就会抛出错误,导致整个初始化失败 const wm2 = new WeakMap([ [key1, "val1"], ["BADKEY", "val2"], [key3, "val3"] ]); // TypeError: Invalid value used as WeakMap key typeof wm2; // ReferenceError: wm2 is not defined
原始值可以先包装成对象再用作键
初始化之后可以使用 set()再添加键/值对,可以使用 get()和 has()查询,还可以使用 delete()删除,set()方法返回弱映射实例,因此可以把多个操作连缀起来,包括初始化声明。使用同Map。
2. 弱键
WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
const wm = new WeakMap(); wm.set({}, "val");
set()方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身也会成为垃圾回收的目标。
再看一个与上面稍微不同的例子:const wm = new WeakMap(); const container = { key: {} }; wm.set(container.key, "val"); function removeReference() { container.key = null; }
这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉。
3. 不可迭代键
因为 WeakMap 中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。当然,也用不着像 clear()这样一次性销毁所有键/值的方法。WeakMap 确实没有这个方法。因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱映射中取得值。即便代码可以访问 WeakMap 实例,也没办法看到其中的内容。
WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
4. 使用弱映射
(1) 私有变量
弱映射造就了在 JavaScript 中实现真正私有变量的一种新方式。前提很明确:私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。
const wm = new WeakMap(); class User { constructor(id) { this.idProperty = Symbol('id'); this.setId(id); } setPrivate(property, value) { const privateMembers = wm.get(this) || {}; privateMembers[property] = value; wm.set(this, privateMembers); } getPrivate(property) { return wm.get(this)[property]; } setId(id) { this.setPrivate(this.idProperty, id); } getId() { return this.getPrivate(this.idProperty); } } const user = new User(123); alert(user.getId()); // 123 user.setId(456); alert(user.getId()); // 456 // 并不是真正私有的 alert(wm.get(user)[user.idProperty]); // 456
对于上面的实现,外部代码只需要拿到对象实例的引用和弱映射,就可以取得“私有”变量了。为了避免这种访问,可以用一个闭包把 WeakMap 包装起来,这样就可以把弱映射与外界完全隔离开了:
const User = (() => { const wm = new WeakMap(); class User { constructor(id) { this.idProperty = Symbol('id'); this.setId(id); } setPrivate(property, value) { const privateMembers = wm.get(this) || {}; privateMembers[property] = value; wm.set(this, privateMembers); } getPrivate(property) { return wm.get(this)[property]; } setId(id) { this.setPrivate(this.idProperty, id); } getId(id) { return this.getPrivate(this.idProperty); } } return User; })(); const user = new User(123); alert(user.getId()); // 123 user.setId(456); alert(user.getId()); // 456
(2) DOM 节点元数据
const m = new Map(); const loginButton = document.querySelector('#login'); // 给这个节点关联一些元数据 m.set(loginButton, {disabled: true});
假设在上面的代码执行后,页面被 JavaScript 改变了,原来的登录按钮从 DOM 树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的 DOM 节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。
如果这里使用的是弱映射,如以下代码所示,那么当节点从 DOM 树中被删除后,垃圾回收程序就可以立即释放其内存(假设没有其他地方引用这个对象):const wm = new WeakMap(); const loginButton = document.querySelector('#login'); // 给这个节点关联一些元数据 wm.set(loginButton, {disabled: true});
六、Set
1. 基本API
使用 new 关键字和 Set 构造函数可以创建一个空集合:
const m = new Set();
如果想在创建的同时初始化实例,则可以给 Set 构造函数传入一个可迭代对象,其中需要包含插入到新集合实例中的元素:
// 使用数组初始化集合 const s1 = new Set(["val1", "val2", "val3"]); alert(s1.size); // 3 // 使用自定义迭代器初始化集合 const s2 = new Set({ [Symbol.iterator]: function*() { yield "val1"; yield "val2"; yield "val3"; } }); alert(s2.size); // 3
初始化之后,可以使用 add()增加值,使用 has()查询,通过 size 取得元素数量,以及使用 delete()和 clear()删除元素,add()返回集合的实例,所以可以将多个添加操作连缀起来,包括初始化。使用同Map。
与 Map 类似,Set 可以包含任何 JavaScript 数据类型作为值。集合也使用 SameValueZero 操作(ECMAScript 内部定义,无法在语言中使用),基本上相当于使用严格对象相等的标准来检查值的匹配性。
const s = new Set(); const functionVal = function() {}; const symbolVal = Symbol(); const objectVal = new Object(); s.add(functionVal); s.add(symbolVal); s.add(objectVal); alert(s.has(functionVal)); // true alert(s.has(symbolVal)); // true alert(s.has(objectVal)); // true // SameValueZero 检查意味着独立的实例不会冲突 alert(s.has(function() {})); // false
与严格相等一样,用作值的对象和其他“集合”类型在自己的内容或属性被修改时也不会改变;add()和 delete()操作是幂等的。delete()返回一个布尔值,表示集合中是否存在要删除的值。
2. 顺序与迭代
Set 会维护值插入时的顺序,因此支持按顺序迭代。集合实例可以提供一个迭代器(Iterator),能以插入顺序生成集合内容。可以通过 values()方法及其别名方法 keys()(或者 Symbol.iterator 属性,它引用 values())取得这个迭代器:
const s = new Set(["val1", "val2", "val3"]); alert(s.values === s[Symbol.iterator]); // true alert(s.keys === s[Symbol.iterator]); // true for (let value of s.values()) { alert(value); } // val1 // val2 // val3 for (let value of s[Symbol.iterator]()) { alert(value); } // val1 // val2 // val3 //因为 values()是默认迭代器,所以可以直接对集合实例使用扩展操作,把集合转换为数组 console.log([...s]); // ["val1", "val2", "val3"]
集合的 entries()方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元素是集合中每个值的重复出现:
for (let pair of s.entries()) { console.log(pair); } // ["val1", "val1"] // ["val2", "val2"] // ["val3", "val3"]
如果不使用迭代器,而是使用回调方式,则可以调用集合的 forEach()方法并传入回调,依次迭代每个键/值对。传入的回调接收可选的第二个参数,这个参数用于重写回调内部 this 的值:
s.forEach((val, dupVal) => alert(`${val} -> ${dupVal}`)); // val1 -> val1 // val2 -> val2 // val3 -> val3
修改集合中值的属性不会影响其作为集合值的身份:
const s1 = new Set(["val1"]); // 字符串原始值作为值不会被修改 for (let value of s1.values()) { value = "newVal"; alert(value); // newVal alert(s1.has("val1")); // true } const valObj = {id: 1}; const s2 = new Set([valObj]); // 修改值对象的属性,但对象仍然存在于集合中 for (let value of s2.values()) { value.id = "newVal"; alert(value); // {id: "newVal"} alert(s2.has(valObj)); // true } alert(valObj); // {id: "newVal"}
七、WeakSet
ECMAScript 6 新增的“弱集合”(WeakSet)是一种新的集合类型,为这门语言带来了集合数据结构。WeakSet 是 Set 的“兄弟”类型,其 API 也是 Set 的子集。WeakSet 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱集合”中值的方式。
1. 基本 API
可以使用 new 关键字实例化一个空的 WeakSet:
const ws = new WeakSet();
弱集合中的值只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置值会抛出 TypeError。
如果想在初始化时填充弱集合,则构造函数可以接收一个可迭代对象,其中需要包含有效的值。可迭代对象中的每个值都会按照迭代顺序插入到新实例中:const val1 = {id: 1}, val2 = {id: 2}, val3 = {id: 3}; // 使用数组初始化弱集合 const ws1 = new WeakSet([val1, val2, val3]); alert(ws1.has(val1)); // true alert(ws1.has(val2)); // true alert(ws1.has(val3)); // true // 初始化是全有或全无的操作 // 只要有一个值无效就会抛出错误,导致整个初始化失败 const ws2 = new WeakSet([val1, "BADVAL", val3]); // TypeError: Invalid value used in WeakSet typeof ws2; // ReferenceError: ws2 is not defined // 原始值可以先包装成对象再用作值 const stringVal = new String("val1"); const ws3 = new WeakSet([stringVal]); alert(ws3.has(stringVal)); // true
初始化之后可以使用 add()再添加新值,可以使用 has()查询,还可以使用 delete()删除;add()方法返回弱集合实例,因此可以把多个操作连缀起来,包括初始化声明。
2. 弱值
WeakSet 中“weak”表示弱集合的值是“弱弱地拿着”的。意思就是,这些值不属于正式的引用,不会阻止垃圾回收。
来看下面的例子:const ws = new WeakSet(); ws.add({});
add()方法初始化了一个新对象,并将它用作一个值。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象值就会被当作垃圾回收。然后,这个值就从弱集合中消失了,使其成为一个空集合。
再看一个稍微不同的例子:const ws = new WeakSet(); const container = { val: {} }; ws.add(container.val); function removeReference() { container.val = null; }
这一次,container 对象维护着一个对弱集合值的引用,因此这个对象值不会成为垃圾回收的目标。不过,如果调用了 removeReference(),就会摧毁值对象的最后一个引用,垃圾回收程序就可以把这个值清理掉。
3. 不可迭代值
因为 WeakSet 中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力。当然,也用不着像 clear()这样一次性销毁所有值的方法。WeakSet 确实没有这个方法。因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱集合中取得值。即便代码可以访问 WeakSet 实例,也没办法看到其中的内容。
WeakSet 之所以限制只能用对象作为值,是为了保证只有通过值对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
4. 使用弱集合
相比于 WeakMap 实例,WeakSet 实例的用处没有那么大。不过,弱集合在给对象打标签时还是有价值的。
来看下面的例子,这里使用了一个普通 Set:const disabledElements = new Set(); const loginButton = document.querySelector('#login'); // 通过加入对应集合,给这个节点打上“禁用”标签 disabledElements.add(loginButton);
这样,通过查询元素在不在 disabledElements 中,就可以知道它是不是被禁用了。不过,假如元素从 DOM 树中被删除了,它的引用却仍然保存在 Set 中,因此垃圾回收程序也不能回收它。
为了让垃圾回收程序回收元素的内存,可以在这里使用 WeakSet:const disabledElements = new WeakSet(); const loginButton = document.querySelector('#login'); // 通过加入对应集合,给这个节点打上“禁用”标签 disabledElements.add(loginButton);
这样,只要 WeakSet 中任何元素从 DOM 树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存(假设没有其他地方引用这个对象)。