WeakSet和WeakMap
(1)WeakSet和WeakMap一些相似的特点
1.WeakSet的成员只能是对象,WeakMap只接受对象(null除外)作为键名
let weakset = new WeakSet([1, 2, 3]);
//TypeError: Invalid value used in weak set
let weakmap=new WeakMap([[0,2],[0,3]]);
//TypeError: Invalid value used as weak map
//这里的0为数字而不是对象
上面的代码均会报错。
let weakmap=new WeakMap();
let key={};
weakmap.set(key,1);
console.log(weakmap);
//WeakMap { {}=>1 }
WeakMap只接受对象(null除外)作为键名,其他类型的值都可以作为键值。
2.不计入垃圾回收机制
WeakSet中的对象不计入垃圾回收机制,WeakMap中键名指向的对象不计入垃圾回收机制。
常见垃圾收集方式——标记清除
我们先来复习一下垃圾回收机制:在编写javascript程序时,开发人员不用关心内存的使用问题,所需内存的分配以及无用内存的回收实现了自动管理,垃圾收集器会按照固定的时间间隔,找出那些不再继续使用的变量,然后释放其占用的内存。
javascript中通常有两种垃圾收集方式:
1.标记清除(Mark-and-Sweep)
2.引用计数(Reference-counting)
大多数浏览器使用的是标记清除策略,因为引用计数策略无法解决因为循环引用而造成内存泄露问题。
标记清除指的是:
当变量进入执行环境,就将这个变量标记为“进入环境”,当变量离开环境时,将其标记为“离开环境”。垃圾收集器在运行的时候会给储存在内存中的所有变量加上标记(不同的浏览器可能使用不同的标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后仍然被加上标记的变量将被视为准备删除的变量,之后垃圾收集器运行的时候(不同浏览器垃圾收集器运行的时间间隔互有不同)销毁带标记的值并回收它们占用的内存。
3.WeakSet和WeakMap没有遍历操作,没有size属性,没有clear方法
WeakSet中的对象是弱引用,WeakMap的键名所引用的对象也是弱引用。垃圾回收机制不将它们的引用考虑在内。一旦不再需要,垃圾回收机制会自动将它们回收,不用手动删除,避免了内存的泄露。
因为WeakSet的成员和WeakMap引用的键名可能会随时消失,所以WeakSet和WeakMap没有遍历操作,也没有size属性,并且两者都没有clear方法。
对于WeakMap来说,弱引用的只是键名而不是键值
let weakmap = new WeakMap();
let key = {};
let obj = {"foo": 1};
weakmap.set(key, obj);
console.log(weakmap);
//WeakMap { {}=>{"foo":1} }
obj = null;
console.log(weakmap);
//WeakMap { {}=>{"foo":1} }
键值obj是正常引用的。所以,即使在WeakMap外部消除了obj的引用,WeakMap内部的引用依然存在。
let key = {};
let obj = {"foo": 1};
let weakmap = new WeakMap([[key,obj]]);
console.log(weakmap);
//WeakMap { {}=>{"foo":1} }
key = {"rrr":1};
obj = {"foo": 2};
console.log(weakmap);
//WeakMap { {}=>{"foo":1} }
即使在WeakMap外部改变了键名key和键值obj的外部引用,但在WeakMap内部依然引用的是构造时的引用值。
(2)WeakSet
WeakSet结构与Set类似,也是不重复的值的集合。
1.基本语法
WeakSet构造函数
任何具有iterable接口的对象都可以成为WeakSet构造函数的参数。以数组为例,数组的所有成员都会自动成为WeakSet实例对象的成员,而数组的成员只能是对象。
let weakset = new WeakSet([[1, 2], [2, 3]]);
console.log(weakset);
// WeakSet [[1,2],[2,3]]
// WeakSet [[2,3],[1,2]]
//都有可能出现
let set=new Set([[1, 2], [2, 3]]);
console.log(set);
//Set [[1,2],[2,3]]
以上代码可以看到WeakSet中的元素顺序和Set中的元素顺序是不同的。
WeakSet中元素顺序并不是确定的,当使用add()后,元素的顺序还会发生改变:
let weakset = new WeakSet([[1, 2], [2, 3]]);
console.log(weakset);
// WeakSet [[4,5],[1,2],[2,3]]
// WeakSet [[2,3],[4,5],[1,2]]
//等等各种顺序都有可能出现
weakset.add([4,5]);
console.log(weakset);
// WeakSet [[4,5],[1,2],[2,3]]
// WeakSet [[2,3],[4,5],[1,2]]
//等等各种顺序都有可能出现
两次运行相同的代码,在WeakSet中元素的顺序有可能是不同的。
2.操作方法
WeakSet没有遍历操作,没有size属性,没有clear方法。
add(value)
向WeakSet实例添加一个新成员。
delete(value)
清除WeakSet实例的指定成员。
has(value)
返回一个布尔值,表示某个值是否在WeakSet实例中。
3.使用WeakSet的例子
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
}
}
}
上面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用 WeakSet 的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。
(3)WeakMap
WeakMap结构与Map结构类似也用于生成键值对的集合。
1.基本语法
WeakMap构造函数
WeakMap可以接受一个数组,作为构造函数的参数:
let k1 = [1, 2];
let k2 = [2, 3];
let weakmap = new WeakMap([[k1, 123], [k2, 234]]);
console.log(weakmap);
//WeakMap { [2,3]=>234,[1,2]=>123 }
//WeakMap { [1,2]=>123,[2,3]=>234 }
//都有可能出现
let map=new Map([[k1, 123], [k2, 234]]);
console.log(map);
//WeakMap { [2,3]=>234,[1,2]=>123 }
以上代码可以看到WeakMap中的元素顺序和Map中的元素顺序是不同的。
WeakMap中元素顺序并不是确定的,当使用set()后,元素的顺序还会发生改变:
let k1 = [1, 2];
let k2 = [2, 3];
let weakmap = new WeakMap([[k1, 123], [k2, 234]]);
console.log(weakmap);
//WeakMap { [2,3]=>234,[1,2]=>123,[4,5]=>456}
//WeakMap { [2,3]=>234,[4,5]=>456,[1,2]=>123}
//WeakMap { [4,5]=>456,[2,3]=>234,[1,2]=>123}
//等等各种顺序都有可能出现
weakmap.set([4,5],456);
console.log(weakmap);
//WeakMap { [2,3]=>234,[1,2]=>123,[4,5]=>456}
//WeakMap { [2,3]=>234,[4,5]=>456,[1,2]=>123}
//WeakMap { [4,5]=>456,[2,3]=>234,[1,2]=>123}
//等等各种顺序都有可能出现
两次运行相同的代码,在WeakMap中元素的顺序有可能是不同的。
2.操作方法
WeakMap没有遍历操作,没有size属性,没有clear方法。
WeakMap只有以下四种方法:
get()
set()
has()
delete()
3.使用WeakMap的例子
以DOM作为键名的场景
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
上面代码中,myElement是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
实现注册监听事件的listener对象
const listener = new WeakMap();
listener.set(element1, handler1);
listener.set(element2, handler2);
element1.addEventListener("click",listener.get(element1),false);
element2.addEventListener("click",listener.get(element2),false);
上面的代码中,监听函数放在WeakMap里面。一旦DOM对象消失,与它绑定的监听函数也会自动消失。
部署私有属性
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)();
}
}
}
const c = new Countdown(2, () => console.log('DONE'));
c.dec()
c.dec()
// DONE
上面代码中,Countdown类的两个内部属性_counter和_action,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。