在 ECMAScript 6 之前,能够应用 Object
的对象属性作为键,再应用属性援用值的形式来实现 key/value
存储;而 ECMAScript 6 标准中,新增了 Map
和 WeakMap
两种汇合类型来存储 key/value
。
Map
ECMAScript 6 新增的 Map
汇合能够应用 new
和 Map
构造函数创立一个空映射:
const map = new Map();
也能够在创立映射时,就将键值对依照迭代程序插入到新映射实例中:
// 应用嵌套数组初始化映射
const m1 = new Map([["name", "小赵"],
["age", 12],
["sex", "男"]
]);
alert(m1.size); // 3
// 应用自定义迭代器初始化映射
const m2 = new Map({[Symbol.iterator]: function*() {yield ["name", "小赵"];
yield ["age", 12];
yield ["sex", "男"];
}
});
alert(m2.size); // 3
// 映射期待的键 / 值对,无论是否提供
const m3 = new Map([[]]);
alert(m3.has(undefined)); // true
alert(m3.get(undefined)); // undefined
API
初始化之后,在想要往映射表中增加 key/value
,就须要应用 set()
办法:
const map = new Map();
map.set("name", "小赵");
map.set("age", 24);
map.set("sex", "男");
然而,set()
办法返回的是 Map
实例,因而能够初始化能够如下申明:
const map = new Map().set("name", "小赵");
map.set("age", 24).set("sex", "男");
寄存进去的 key/value
能够通过 get()
和 has()
办法进行查问,has()
办法用于判断 key
是否存在 Map
中,而 get()
用于获取 key
对应的 value
:
console.log(map.has("score")); // false
console.log(map.get("name")); // "小赵"
能够通过 size
属性获取 Map
中 key/value
的数量:
console.log(map.size); // 3
如果要删除 Map
中的 key/value
,能够应用 delete()
和 clear()
办法,delete()
办法用于删除指定的 key
,而 clear()
用于革除 Map
中的所有 key/value
:
map.delete("age");
console.log(map.size); // 2
map.clear();
console.log(map.size); // 0
如果须要以插入程序返回 Map
中每个元素的 [key, value]
数组,能够应用 entries()
办法或者 Symbol.iterator
属性获取 Map
中的迭代器。
const m = new Map([["name", "小赵"],
["age", 12],
["sex", "男"]
]);
console.log(m.entries === m[Symbol.iterator]); // true
for (let kv of m.entries()) {console.log(kv);
}
// ['name', '小赵']
// ['age', 12]
// ['sex', '男']
for (let kv of m[Symbol.iterator]()) {console.log(kv);
}
// ['name', '小赵']
// ['age', 12]
// ['sex', '男']
for (let [k, v] of m.entries()) {console.log(k + "=" + v);
}
// name = 小赵
// age = 12
// sex = 男
因为 entries()
是默认迭代器,能够间接对 Map
实例应用扩大操作,把 Map
转换成数组:
const m = new Map([["name", "小赵"],
["age", 12],
["sex", "男"]
]);
console.log([...m]); // [[ 'name', '小赵'], ['age', 12], ['sex', '男'] ]
也能够应用 forEach(callback, opt_thisArg)
办法并传入回调,顺次迭代每个 key/value
。传入的回调接管可选的第二个参数,这个参数用于重写回调外部 this
的值:
m.forEach((val, key) => console.log(`${key} -> ${val}`));
// name -> 小赵
// age -> 12
// sex -> 男
keys()
和 values()
别离返回以插入程序生成 key
和 value
的迭代器:
for (let k of m.keys()) {console.log(k);
}
// name
// age
// sex
for (let v of m.values()) {console.log(v);
}
// 小赵
// 12
// 男
键和值在迭代器遍历时是能够批改的,但映射外部的援用则无奈批改。当然,这并不障碍批改作为键或值的对象外部的属性,因为这样并不影响它们在映射实例中的身份:
const m1 = new Map([["name", "小赵"]
]);
// 作为键的字符串原始值是不能批改的
for (let key of m1.keys()) {
key = 10010;
console.log(key); // 10010
console.log(m1.get("name")); // 小赵
}
const key2 = {id: 1};
const m2 = new Map([[key2, "小新"]
]);
// 批改了作为键的对象的属性,但对象在映射外部依然援用雷同的值
for (let key of m2.keys()) {
key.id = 10086;
console.log(key); // {id: 10086}
console.log(m2.get(key2)); // 小新
}
console.log(key2); // {id: 10086}
键的相等性
在 Map
中,键的比拟是基于 someValueZero
算法:
NaN
是与NaN
相等的(尽管NaN !== NaN
),剩下所有其它的值是依据===
运算符的后果判断是否相等。- 在目前的 ECMAScript 标准中,
-0
和+0
被认为是相等的,只管这在晚期的草案中并不是这样的。
与 Object 比拟
Map
与 Object
相似,都容许按 key
存取 value
、删除 key
、检测 key
是否绑定了 value
。但 Map
和 Object
也在内存和性能上的确存在显著的差异。
通常存储单个 key/value
所占用的内存会随着 key
的数量线性减少,也会受不同浏览器的影响,然而对于规定大小的内存,Map
能够比 Object
多存储 key/value
;而要波及大量插入,专为存储 key/value
设计的 Map
,它的性能更佳。
对于 Map
而言,存储 Map
的 key
能够是任意值,而且 key
的程序是由插入的程序决定的;delete()
办法也比插入和查找要更快。
而 Object
的 key
只能是 String
或 Symbol
,而且插入的程序是无序的;而且在 Object
中删除属性,会呈现一些伪删除,包含把属性值设置为 undefined
或 null
。
WeakMap
WeakMap
也是 ECMAScript 6 新增的映射表,而且还是 Map
的子集。然而 WeakMap
寄存的 key
必须是对象类型,而且还是 “弱映射”
,这会使得 key
所指的值没有其余中央援用的时候,它就会 GC 回收;value
能够是任意类型。
WeakMap
与 Map
的 API 根本都雷同,然而 WeakMap
中的键只能是 Object
或者继承自 Object
的类型,尝试应用非对象设置键会抛出 TypeError
,值的类型没有限度。
但 WeakMap
曾经弃用 clear()
办法,但能够通过创立一个空的 WeakMap
并替换原对象来实现。
let wm = new WeakMap();
vm.set("name", "小钱").set("age", 24).set("sex", "男");
// 清空 WeakMap
vm = new WeakMap();
弱键
WeakMap
中的 "weak"
示意的是 key
存在,key/value
就会存在 WeakMap
中,并当作 value
的援用,不会被 GC 回收;而 key
不存在时,不会阻止 GC 回收。
const wm = new WeakMap();
wm.set({}, "weak value");
set()
办法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其余援用,所以当这行代码执行实现后,这个对象键就会被当作垃圾回收。而后,这个键 / 值对就从弱映射中隐没了,使其成为一个空映射。在这个例子中,因为值也没有被援用,所以这对键 / 值被毁坏当前,值自身也会成为垃圾回收的指标。再看一个略微不同的例子:
const wm = new WeakMap();
const container = {key: {}
};
wm.set(container.key, "weak val");
function removeReference() {container.key = null;}
这一次,container
对象保护着一个 container.key
的援用,因而这个对象键不会被 GC 回收。不过,如果调用了 removeReference()
,就会捣毁 key
的最初一个援用,就会被 GC 回收。
不可迭代键
因为 WeakMap
中的 key/value
任何时候都可能被销毁,也没有提供 entries()
、keys
、values
等可迭代的办法,也没有提供 clear()
一次性革除所有 key/value
的办法。
之所以限度只能用对象作为 key
,是为了保障只有通过 key
的援用能力获得值。如果容许原始值,那就没方法辨别初始化时应用的字符串字面量和初始化之后应用的一个相等的字符串了。
利用
因为 WeakMap
中 key
的弱援用,因而能够在相应的许多方面利用它。
公有变量
公有变量是不会被外界拜访到的,不能被多个实例共享,因而能够应用 WeakMap
来实现。如下所示:
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
这样,拿不到弱映射中的健,也就无奈获得弱映射中对应的值。尽管这避免了后面提到的拜访,但整个代码也齐全陷入了 ES6 之前的闭包公有变量模式。
DOM 节点元数据
应用 WeakMap
实例存储的 DOM
元素时在被革除时,对应的 WeakMap
记录会被 GC 回收,因而非常适合保留关联元数据。来看上面这个例子,其中应用了惯例的 Map
:
const m = new Map();
const lb = document.querySelector('#login');
// 给这个节点关联一些元数据
m.set(lb, {disabled: true});
假如在下面的代码执行后,页面被 JavaScript 扭转了,原来的登录按钮从 DOM
树中被删掉了。但因为映射中还保留着按钮的援用,所以对应的 DOM
节点依然会勾留在内存中,除非明确将其从映射中删除或者等到映射自身被销毁。
如果这里应用的是弱映射,如以下代码所示,那么当节点从 DOM
树中被删除后,垃圾回收程序就能够立刻开释其内存(假如没有其余中央援用这个对象):
const wm = new WeakMap();
const lb = document.querySelector('#login');
// 给这个节点关联一些元数据
wm.set(lb, {disabled: true});
更多内容请关注公众号「 海人为记 」