Map
Map
的大多数特性都可以通过Object
类型实现,但二者之间还是存在一些细微的差异。
与Object
的区别
-
"键"的类型
Object
只能使用数值、字符串或符号作为键
Map
可以使用任何JavaScript数据类型作为键
-
Map实例会维护键值对的插入顺序
-
性能方面
-
内存占用
Map
大约可以比Object
多存储50%的键/值对
-
插入性能
Map
的性能更佳
-
查找速度
-
大型 -- 差异极小
-
小型 --
Object
有时更快[注] 在把
Object
当成数组
使用的情况下(比如使用连续整数作为属性)
-
-
删除性能
- 使用
delete
删除Object
属性的性能差 Map
的delete()
操作都比插入和查找更快
- 使用
-
基本API
-
使用嵌套数组初始化
const m1 = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]) m1 // Map {"key1" => "val1", "key2" => "val2", "key3" => "val3"}
-
使用自定义迭代器初始化
const m2 = new Map({ [Symbol.iterator]: function*() { yield ["key1", "val1"]; yield ["key2", "val2"]; yield ["key3", "val3"]; } }); m2 // Map {"key1" => "val1", "key2" => "val2", "key3" => "val3"}
-
使用
set()
方法添加键/值对const demo_map = new Map() demo_map.set("key1", "val1") .set("key2", "val2") demo_map // Map {"key1" => "val1", "key2" => "val2"}
-
使用
get()
和has()
进行查询,通过size
属性获取长度const demo_map = new Map([ ["key1", "val1"], ["key2", "val2"] ]) demo_map.has("key1") // true demo_map.get("key2") // "val2" demo_map.size // 2
-
使用
delete()
和clear()
删除值const demo_map = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"] ]) demo_map // Map {"key1" => "val1", "key2" => "val2", "key3" => "val3"} // delete() 删除指定项 demo_map.delete("key1") demo_map // Map {"key2" => "val2", "key3" => "val3"} // clear() 清除所有 demo_map.clear() demo_map // Map {}
-
可以使用任何JavaScript数据类型作为键,使用严格对象相等的标准来检查键的匹配性。
[注]
Map
内部使用SameValueZero比较操作(ECMAScript规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。const demo_map = new Map() const functionKey = function() {}; const symbolKey = Symbol(); const objectKey = new Object(); demo_map.set(functionKey, "functionValue"); demo_map.set(symbolKey , "symbolValue"); demo_map.set(objectKey , "objectValue"); demo_map.get( functionKey ) // "functionValue" demo_map.get( symbolKey ) // "symbolValue" demo_map.get( objectKey ) // "objectValue" // 使用严格对象相等的标准来检查键的匹配性 demo_map.get( function() {} ) // undefined // 引用值作为键时,修改其属性仍可访问到指定值 objectKey.selfKey = 'selfVal' demo_map.get( objectKey ) // "objectValue"
顺序与迭代
[注] 会维护值插入时的顺序
-
entries()
(或者Symbol.iterator
属性,它引用entries()
)const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] ) demo_map.entries === demo_map[Symbol.iterator] // true demo_map.entries() // MapIterator {'key1' => 'val1', 'key2' => 'val2'}
-
entries()
配合使用for of
实现遍历const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] ) for ( let pair of demo_map.entries() ) { console.log(pair) } // ['key1', 'val1'] // ['key2', 'val2']
-
扩展操作
[...]
实现Map
转Array
const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] ) [...demo_map] // [['key1', 'val1'], ['key2', 'val2']]
-
forEach(callback, opt_thisArg)
进行遍历[注] 传入的回调接收可选的第二个参数,这个参数用于重写回调内部
this
的值const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] ) demo_map.forEach( (val, key) => console.log(`${key} -> ${val}`) ) // key1 -> val1 // key2 -> val2
-
keys()
和values()
分别返回以插入顺序生成键和值的迭代器:const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] ) demo_map.keys() // MapIterator {'key1', 'key2'} demo_map.values() // MapIterator {'val1', 'val2'} for ( let key of demo_map.keys() ) { console.log(key) } // key1 // key2 for ( let value of demo_map.values() ) { console.log(value) } // val1 // val2
WeakMap
键只能是
Object
或者继承自Object
的类型,尝试使用非对象设置键会抛出TypeError
。
WeakMap
是Map
的“兄弟”类型,其API也是Map
的子集。
WeakMap
中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱映射”中键的方式。
基本API
set()
添加键/值对get()
和has()
查询delete()
删除,跟clear()
清除Map
不一样,WeakMap
没有clear()
特性
-
键只能是
Object
或者继承自Object
的类型const objKey = { id: 1 } const demo_weakMap = new WeakMap( [ [objKey, "val1"] ] ) demo_weakMap // WeakMap {{…} => 'val1'} /* 原始值不可以直接当 WeakMap 的键 */ const demo_weakMap = new WeakMap( [ ["key1", "val1"] ] ) // Uncaught TypeError: Invalid value used as weak map key
[注] 书中表示 原始值可以先包装成对象再用作键, 但是经过测试,Chrome 和 Safari 浏览器中控制台均报错
-
键的引用要是不在了,
weakMap
就会被当作垃圾回收const demo_weakMap = new WeakMap() demo_weakMap.set({}, 'val') // WeakMap {{…} => 'val'} /* 再次打印 demo_weakMap,经多次测试,非立即必现 */ demo_weakMap // WeakMap {}
不可迭代键
因为WeakMap
中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力
使用弱映射
-
私有变量
-
可能遇到的问题,外部代码只需要拿到对象实例的引用和弱映射,就可以取得“私有”变量了。
const demo_weakMap = new WeakMap(); // 设计初衷,将 id 私有化,无法通过除 user.getId() 之外的途径获取到 class User { constructor(id) { this.idProperty = Symbol('id'); this.setId(id); } setPrivate(property, value) { const privateMembers = demo_weakMap.get(this) || {}; privateMembers[property] = value; demo_weakMap.set(this, privateMembers); } getPrivate(property) { return demo_weakMap.get(this)[property]; } setId(id) { this.setPrivate(this.idProperty, id); } getId() { return this.getPrivate(this.idProperty); } } const user = new User(123); user.getId(); // 123 user.setId(456); user.getId(); // 456 // 并不是真正私有的 demo_weakMap.get(user)[user.idProperty]; // 456
-
用一个闭包把
WeakMap
包装起来,这样就可以把弱映射与外界完全隔离开了// 设计初衷,将 id 私有化,无法通过 user.id 直接获取到 const User = (() => { const demo_weakMap = new WeakMap(); class User { constructor(id) { this.idProperty = Symbol('id'); this.setId(id); } setPrivate(property, value) { const privateMembers = demo_weakMap.get(this) || {}; privateMembers[property] = value; demo_weakMap.set(this, privateMembers); } getPrivate(property) { return demo_weakMap.get(this)[property]; } setId(id) { this.setPrivate(this.idProperty, id); } getId(id) { return this.getPrivate(this.idProperty); } } return User; })(); const user = new User(123); user.getId(); // 123 user.setId(456); user.getId(); // 456
-
-
DOM节点元数据
-
相较于
Map()
const demo_map = new Map(); const loginButton = document.querySelector('#login'); // 给这个节点关联一些元数据 demo_map.set(loginButton, {disabled: true}); // 假设在上面的代码执行后,页面被JavaScript改变了,原来的登录按钮从DOM树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的DOM节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。
-
WeakMap()
的垃圾回收机制优势就体现出来了const demo_weakMap = new Map(); const loginButton = document.querySelector('#login'); // 给这个节点关联一些元数据 demo_weakMap.set(loginButton, {disabled: true}); // (假设没有其他地方引用这个对象) // 那么当节点从DOM树中被删除后,垃圾回收程序就可以立即释放其内存
-
Set
Set
在很多方面都像是加强的Map
,这是因为它们的大多数API和行为都是共有的。
基本API
-
使用嵌套数组初始化
const s1 = new Set(["val1", "val2", "val3"]); s1 // Set(3) {'val1', 'val2', 'val3'}
-
使用自定义迭代器初始化
const s2 = new Set({ [Symbol.iterator]: function*() { yield "val1"; yield "val2"; yield "val3"; } }); s2 // Set(3) {'val1', 'val2', 'val3'}
-
使用
add()
方法添加元素const demo_set = new Set() demo_set.add("val1") .add("val2") demo_set // Set(2) {'val1', 'val2'} // add() 同样的值会默认略过 demo_set.add("val1") demo_set // Set(2) {'val1', 'val2'}
-
使用
has()
进行查询,通过size
属性获取长度const demo_set = new Set(["val1", "val2"]); demo_set.has("val1") // true demo_set.size // 2
-
使用
delete()
和clear()
删除值const demo_set = new Set(["val1", "val2", "val3"]); demo_set // Set(3) {'val1', 'val2', 'val3'} // delete() 删除指定项 // 返回 Set 中是否存在被删除的值 demo_set.delete('val1') // true demo_set.delete('val1') // false demo_set // Set(2) {'val2', 'val3'} // clear() 清除所有 demo_set.clear() demo_set // Set(0) {size: 0}
-
可以使用任何JavaScript数据类型作为值,使用严格对象相等的标准来检查键的匹配性。
[注]
Set
内部使用SameValueZero比较操作(ECMAScript规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。const demo_set = new Set() const functionVal = function() {}; const symbolVal = Symbol(); const objectVal = new Object(); demo_set.add( functionVal ); demo_set.add( symbolVal ); demo_set.add( objectVal ); demo_set.has( functionVal ) // true demo_set.has( symbolVal ) // true demo_set.has( objectVal ) // true // 使用严格对象相等的标准来检查键的匹配性 demo_set.has( function() {} ) // false // 引用值作为键时,修改其属性仍可访问到指定值 objectVal.selfKey = 'selfVal' demo_set.has( objectVal ) // true
顺序与迭代
[注] 会维护值插入时的顺序
-
values()
及其别名方法keys()
(或者Symbol.iterator
属性,它引用values()
)const demo_set = new Set(["val1", "val2"]); demo_set.values === demo_set[Symbol.iterator] // true demo_set.keys === demo_set[Symbol.iterator] // true demo_set.values() // SetIterator {'val1', 'val2'}
-
values()
配合使用for of
实现遍历const demo_set = new Set(["val1", "val2"]); for ( let value of demo_set.values() ) { console.log(value) } // val1 // val2
-
扩展操作
[...]
实现Set
转Array
const demo_set = new Set(["val1", "val2"]); [...demo_set] // ["val1", "val2"]
-
entries()
方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元素是集合中每个值的重复出现const demo_set = new Set(["val1", "val2"]); for ( let pair of demo_set.entries() ) { console.log(pair) } // ['val1', 'val1'] // ['val2', 'val2']
-
forEach(callback, opt_thisArg)
进行遍历[注] 传入的回调接收可选的第二个参数,这个参数用于重写回调内部
this
的值const demo_set = new Set(["val1", "val2"]); demo_set.forEach( (val, dupVal) => console.log(`${val} -> ${dupVal}`) ) // val1 -> val1 // val2 -> val2
特定操作
-
返回两个或更多集合的并集
class XSet extends Set { union(...sets) { return XSet.union(this, ...sets) } // 返回两个或更多集合的并集 static union(a, ...bSets) { const unionSet = new XSet(a); for (const b of bSets) { for (const bValue of b) { unionSet.add(bValue); } } return unionSet; } } const _xSet = new XSet() _xSet.union( new Set([1, 2]), new Set([3, 4]), new Set([5, 6]) ) // XSet(6) {1, 2, 3, 4, 5, 6}
-
返回两个或更多集合的交集
class XSet extends Set { union(...sets) { return XSet.union(this, ...sets) } intersection(...sets) { return XSet.intersection(this, ...sets); } // 返回两个或更多集合的并集 static union(a, ...bSets) { const unionSet = new XSet(a); for (const b of bSets) { for (const bValue of b) { unionSet.add(bValue); } } return unionSet; } /** * 原著中 intersection 函数有误(也可能是我没get到他的函数使用方法),并不能返回交集 * 经过修改后,支持返回集合交集。 */ // 返回两个或更多集合的交集 static intersection(a, ...bSets) { // const intersectionSet = new XSet(a) const intersectionSet = new XSet(a).union( ...bSets ); for (const aValue of intersectionSet) { for (const b of bSets) { if (!b.has(aValue)) { intersectionSet.delete(aValue); } } } return intersectionSet; } } const _xSet = new XSet() _xSet.intersection( new Set([1, 2, 3]), new Set([2, 3, 4]), new Set([2, 3, 6]) ) // XSet(2) {2, 3}
WeakSet
WeakSet
是Set
的“兄弟”类型,其API也是Set
的子集。WeakSet
中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱集合”中值的方式。
基本API
add()
添加值has()
查询delete()
删除,跟clear()
清除Set
不一样,WeakSet
没有clear()
特性
-
值只能是
Object
或者继承自Object
的类型const objVal = { id: 1 } const demo_weakSet = new WeakSet( [ objVal ] ) demo_weakSet // WeakSet {{…}} /* 原始值不可以直接当 WeakSet 的键 */ const demo_weakSet = new WeakSet( [ 'val' ] ) // Uncaught TypeError: Invalid value used in weak set
[注] 书中表示 原始值可以先包装成对象再用作键, 但是经过测试,Chrome 和 Safari 浏览器中控制台均报错
-
键的引用要是不在了,
WeakSet
就会被当作垃圾回收const demo_weakSet = new WeakSet() demo_weakSet.add({}) // WeakSet {{…}} /* 再次打印 demo_weakSet,经多次测试,非立即必现*/ demo_weakSet // WeakSet {}
不可迭代键
因为WeakSet
中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力
使用弱集合
相比于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树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存