本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-07-03
ES6 提供了新的数据结构 Set
。它类似于数组,但是成员的值都是唯一的,没有重复的值。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
// add方法是Set类型的实例方法,用来添加成员
// 可用来做数组去重
const s1 = new Set([1, 2, 3, 4, 4]);
[...s1]
// [1, 2, 3, 4]
Set
内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”
,它类似于精确相等运算符(===
),主要的区别是向 Set
加入值时认为NaN
等于自身,而精确相等运算符认为NaN
不等于自身。
Set.prototype.constructor
:构造函数,默认就是Set
函数。
Set.prototype.size
:返回Set
实例的成员总数。
Set.prototype.add(value)
:添加某个值,返回 Set
结构本身。
Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。
Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员。
Set.prototype.clear()
:清除所有成员,没有返回值。
Array.from
方法可以将 Set
结构转为数组。
Set.prototype.keys()
:返回键名的遍历器
Set.prototype.values()
:返回键值的遍历器
Set.prototype.entries()
:返回键值对的遍历器
Set.prototype.forEach()
:使用回调函数遍历每个成员
由于 Set
结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致。故entries
方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
let s = new Set(['red', 'green', 'blue']);
for (let item of s.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
Set
结构的实例默认可遍历,它的默认遍历器生成函数就是它的values
方法。这意味着,可以省略values
方法,直接用for...of
循环遍历 Set
。
Set.prototype[Symbol.iterator] === Set.prototype.values
// true
let s = new Set(['red', 'green', 'blue']);
for (let x of s) {
console.log(x);
}
// red
// green
// blue
使用 Set
可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// Set {2, 3}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1} 以上语句返回a中有而b中没有的
WeakSet
结构与 Set
类似,也是不重复的值的集合。但是,它与 Set
有两个区别。
首先,WeakSet
的成员只能是对象,而不能是其他类型的值。
其次,WeakSet
中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet
对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。,即WeakSet
中存储的对象,只要外部不再引用就会被回收。
let a = [1, 2];
let b = [3, 4];
let ws = new WeakSet([a, b]);
a = null; // 即可以回收a代表的这个对象,尽管ws中还引用着
这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为0,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet
里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet
适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet
里面的引用就会自动消失。
WeakSet
可以接受一个数组或类似数组的对象作为参数
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
// 3, 4 不是对象
注意,是a数组的成员成为 WeakSet
的成员,而不是a数组本身。这意味着,数组的成员只能是对象。数组b的成员不是对象,加入 WeaKSet
就会报错。
WeakSet.prototype.add(value)
:向 WeakSet
实例添加一个新成员。
WeakSet.prototype.delete(value)
:清除 WeakSet
实例的指定成员。
WeakSet.prototype.has(value)
:返回一个布尔值,表示某个值是否在 WeakSet
实例之中。
没有size
和forEach
属性,所以不能遍历,是因为成员都是弱引用,随时可能消失(外部不再引用),遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet
的一个用处,是储存 DOM
节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
JavaScript
的对象Object
,本质上是键值对的集合: Hash
结构,但是传统上只能用字符串当作键。
const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
// 等同于
data['[object HTMLDivElement]'] = 'metadata';
// 因为
'[object HTMLDivElement]' === element.toString();
为了解决这个问题,ES6 提供了 Map
数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object
结构提供了“字符串—值”的对应,Map
结构提供了“值—值”的对应,是一种更完善的 Hash
结构实现。如果你需要“键值对”的数据结构,Map
比 Object
更合适。
size
属性返回 Map
结构的成员总数。
Map.prototype.set(key, value)
:set
方法设置键名key
对应的键值为value
,然后返回整个 Map
结构。如果key
已经有值,则键值会被更新,否则就新生成该键。
// set方法返回的是当前的Map对象,因此可以采用链式写法。
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
Map.prototype.get(key)
: get
方法读取key
对应的键值,如果找不到key
,返回undefined
。
Map.prototype.has(key)
: has
方法返回一个布尔值,表示某个键是否在当前 Map
对象之中
Map.prototype.delete(key)
: delete
方法删除某个键,返回true
。如果删除失败,返回false
。
Map.prototype.clear()
: clear
方法清除所有成员,没有返回值。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
Map
也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
只有对同一个对象的引用,Map
结构才将其视为同一个键。这一点要非常小心。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
// 两个['a']的内存地址明显不一样
const map = new Map();
const k1 = ['a'];
const k2 = ['a'];
map
.set(k1, 111)
.set(k2, 222);
map.get(k1) // 111
map.get(k2) // 222
Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键,这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
Map.prototype.keys()
:返回键名的遍历器。
Map.prototype.values()
:返回键值的遍历器。
Map.prototype.entries()
:返回所有成员的遍历器。
Map.prototype.forEach()
:遍历 Map
的所有成员。
Map
的遍历顺序就是插入顺序。
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
// Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
Map
结构转为数组结构,比较快速的方法是使用扩展运算符(…)。
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
WeakMap
结构与Map
结构类似,也是用于生成键值对的集合。
首先,WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。
其次,WeakMap
的键名所指向的对象,不计入垃圾回收机制。
WeakMap
的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
// e1 = null; // 这样删除不了引用
// 只有这样手动删除引用
arr[0] = null;
arr[1] = null;
e1
和e2
是两个对象,我们通过arr
数组对这两个对象添加一些文字说明。这就形成了**arr
对e1
和e2
的引用**。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1
和e2
占用的内存。
WeakMap
就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap
里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
// WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。
// 也就是说,上面的 DOM 节点对象的引用计数是1,而不是2
// 即DOM节点对象只被element变量引用 而不被wm引用
// 一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放
// wm保存的这个键值也会自动消失
WeakMap
弱引用的**只是键名,而不是键值。**键值依然是正常引用。
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}
var v = {
a: '123'
}
var o = {
'b': v,
}
v = {
a: '456'
};
console.log(o['b']) // Object { a: 123 }
// 因为变量 v 是对象,指向 内存地址 v1
// 变量 o 也是对象, o['b'] 也是一个对象,指向了 内存地址 v1
// v = {} , 而由于o['b'] 依然引用 v1 内存地址,所以 v1 不会被删除。
// 只是删除了 v 对于 v1 的指向,创建了一块新的内存地址n1,并让v 指向 n1
WeakMap
只有四个方法可用:get()、set()、has()、delete()。
首先,打开 Node 命令行。
$ node --expose-gc
// --expose-gc参数表示允许手动执行垃圾回收机制。
/ 手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc();
undefined
// 查看内存占用的初始状态,heapUsed 为 4M 左右
> process.memoryUsage();
{ rss: 21106688,
heapTotal: 7376896,
heapUsed: 4153936,
external: 9059 }
> let wm = new WeakMap();
undefined
// 新建一个变量 key,指向一个 5*1024*1024 的数组
> let key = new Array(5 * 1024 * 1024);
undefined
// 设置 WeakMap 实例的键名,也指向 key 数组
// 这时,key 数组实际被引用了两次,
// 变量 key 引用一次,WeakMap 的键名引用了第二次
// 但是,WeakMap 是弱引用,对于引擎来说,引用计数还是1
> wm.set(key, 1);
WeakMap {}
> global.gc();
undefined
// 这时内存占用 heapUsed 增加到 45M 了
> process.memoryUsage();
{ rss: 67538944,
heapTotal: 7376896,
heapUsed: 45782816,
external: 8945 }
// 清除变量 key 对数组的引用,
// 但没有手动清除 WeakMap 实例的键名对数组的引用
> key = null;
null
// 再次执行垃圾回收
> global.gc();
undefined
// 内存占用 heapUsed 变回 4M 左右,
// 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收
> process.memoryUsage();
{ rss: 20639744,
heapTotal: 8425472,
heapUsed: 3979792,
external: 8956 }
使用 Map 执行一次,会发现 内存没有被回收 Map 的键名引用阻止了 gc 对内存的回收
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
节点删除,该状态就会自动消失,不存在内存泄漏风险。
另一个用处是部署私有属性
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
// _counter 以 this 为key,存储实例传进来的counter为 value
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
// 相当于就是获取实例传进来的counter 只能通过这种方法获取
// 因为没有在constructor 中使用 this.counter = counter
// 所有此时就相当于是私有属性,实例无法访问
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