JavaScript的ES6~ES13的Symbol、Set、Map

一. Symbol

1.1. Symbol 的基本使用

Symbol 是什么呢?Symbol 是 ES6 中新增的一个基本数据类型,翻译为符号。

那么为什么需要 Symbol 呢?

  • 在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;

  • 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;

  • 比如我们前面在讲 apply、call、bind 实现时,我们有给其中添加一个 fn 属性,那么如果它内部原来已经有了 fn 属性了呢?

  • 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;

Symbol 就是为了解决上面的问题,用来生成一个独一无二的值。

  • Symbol 值是通过 Symbol 函数来生成的,生成后可以作为属性名;

  • 也就是在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol;

1
2
const s = Symbol();
console.log(s); // Symbol()

Symbol 即使多次创建值,它们也是不同的:

  • Symbol 函数执行后每次创建出来的值都是独一无二的;
1
2
3
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false

我们也可以在创建 Symbol 值的时候传入一个描述 description:

  • 这个是 ES2019(ES10)新增的特性;
1
2
const s3 = Symbol("abc");
console.log(s3.description);

1.2. Symbol 作为属性名

我们通常会使用 Symbol 在对象中表示唯一的属性名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const s1 = Symbol("abc");
const s2 = Symbol("cba");
const obj = {};

// 1.写法一: 属性名赋值
obj[s1] = "abc";
obj[s2] = "cba";

// 2.写法二: Object.defineProperty
Object.defineProperty(obj, s1, {
enumerable: true,
configurable: true,
writable: true,
value: "abc"
});

// 3.写法三: 定义字面量是直接使用
const info = {
[s1]: "abc",
[s2]: "cba"
};

之后我们可以通过 Symbol 值来获取值:

1
2
3
4
5
console.log(info[s1]); // abc
console.log(info[s2]); // cba

// 不能这样获取
console.log(info.s1); // undefined

但是通过 Symbol 添加的属性名,在遍历时,是无法获取到的:

1
2
console.log(Object.keys(info));
console.log(Object.getOwnPropertyNames(info));

如果我们希望获取 Symbol 的 key,那么需要通过 Object.getOwnPropertyNames

1
2
3
4
5
console.log(Object.getOwnPropertySymbols(info));
const symbolKeys = Object.getOwnPropertySymbols(info);
for (const key of symbolKeys) {
console.log(info[key]);
}

1.3. 相同值的 Symbol

前面我们讲 Symbol 的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的 Symbol 应该怎么来做呢?

  • 我们可以使用 Symbol.for 方法来做到这一点;

  • 并且我们可以通过 Symbol.keyFor 方法来获取对应的 key;

1
2
3
4
5
6
7
const s1 = Symbol.for("abc");
const s2 = Symbol.for("abc");
console.log(s1 === s2); // true
const key = Symbol.keyFor(s1);
console.log(key); // abc
const s3 = Symbol.for(key);
console.log(s2 === s3); // true

二. Set 集合

在 ES6 之前,我们存储数据的结构主要有两种:数组、对象。

在 ES6 中新增了另外两种数据结构:Set、Map,以及它们的另外形式 WeakSet、WeakMap。

2.1. Set 的基本使用

Set 是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。

创建 Set 我们需要通过 Set 构造函数(暂时没有字面量创建的方式):

1
2
3
4
5
6
7
8
const set1 = new Set();
set1.add(10);
set1.add(14);
set1.add(16);
console.log(set1); // Set(3) { 10, 14, 16 }

const set2 = new Set([11, 15, 18, 11]);
console.log(set2); // Set(3) { 11, 15, 18 }

我们可以发现 Set 中存放的元素是不会重复的,那么 Set 有一个非常常用的功能就是给数组去重。

1
2
3
4
5
const arr = [10, 20, 10, 44, 78, 44];
const set = new Set(arr);
const newArray1 = [...set];
const newArray2 = Array.from(set);
console.log(newArray1, newArray2);

2.2. Set 的常见方法

Set 常见的属性:

  • size:返回 Set 中元素的个数;

Set 常用的方法:

  • add(value):添加某个元素,返回 Set 对象本身;

  • delete(value):从 set 中删除和这个值相等的元素,返回 boolean 类型;

  • has(value):判断 set 中是否存在某个元素,返回 boolean 类型;

  • clear():清空 set 中所有的元素,没有返回值;

  • forEach(callback, [, thisArg]):通过 forEach 遍历 set;

1
2
3
4
5
6
7
set.add(100);
set.delete(100);
set.has(100);
set.clear();
set.forEach(item => {
console.log(item);
});

另外 Set 是支持 for of 的遍历的:

1
2
3
for (const item of set) {
console.log(item);
}

2.3. WeakSet 使用

和 Set 类似的另外一个数据结构称之为 WeakSet,也是内部元素不能重复的数据结构。

那么和 Set 有什么区别呢?

  • 区别一:WeakSet 中只能存放对象类型,不能存放基本数据类型;

  • 区别二:WeakSet 对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么 GC 可以对该对象进行回收;

1
2
const wset = new WeakSet(); // TypeError: Invalid value used in weak set
wset.add(10);

WeakSet 常见的方法:

  • add(value):添加某个元素,返回 WeakSet 对象本身;

  • delete(value):从 WeakSet 中删除和这个值相等的元素,返回 boolean 类型;

  • has(value):判断 WeakSet 中是否存在某个元素,返回 boolean 类型;

注意:WeakSet 不能遍历

  • 因为 WeakSet 只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。

  • 所以存储到 WeakSet 中的对象是没办法获取的;

那么这个东西有什么用呢?

  • 事实上这个问题并不好回答,我们来使用一个 Stack Overflow 上的答案;

比如我们有一个类 Person,里面有一个 running 方法,我们只希望你通过 Person 对象来调用:

1
2
3
4
5
6
7
8
9
class Person {
running() {
console.log("running", this);
}
}

const p = new Person();
p.running();
p.running.call({ name: "why" });

我们可以通过 WeakSet 来存储创建出来的对象,并且在调用方法时判断这个对象是否存在于 WeakSet 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const pwset = new WeakSet();

class Person {
constructor() {
pwset.add(this);
}
running() {
if (!pwset.has(this)) throw new Error("不能通过其他对象调用running方法");
console.log("running", this);
}
}

const p = new Person();
p.running();
p.running.call({ name: "why" });

三. Map 映射

3.1. Map 的基本使用

另外一个新增的数据结构是 Map,用于存储映射关系。

但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢?

  • 事实上我们对象存储映射关系只能用字符串(ES6 新增了 Symbol)作为属性名(key);

  • 某些情况下我们可能希望通过其他类型作为 key,比如对象,这个时候会自动将对象转成字符串来作为 key;

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: "why"
};

const info = {
[obj]: "kobe"
};

console.log(info); // { '[object Object]': 'kobe' }

const obj2 = {};
console.log(info[obj2]); // kobe

所以这显然不是我们想要的效果,那么我们就可以使用 Map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj1 = {
name: "why"
};

const obj2 = {
age: 18
};

const map = new Map();
map.set(obj1, "abc");
map.set(obj2, "cba");

console.log(map.get(obj1));
console.log(map.get(obj2));

我们也可以在创建 Map 的时候,传入一个数组结构,数组结构中是一个个键值对的数组类型:

1
2
3
4
5
6
7
const map = new Map([
[obj1, "abc"],
[obj2, "cba"]
]);

console.log(map.get(obj1));
console.log(map.get(obj2));

另外 Map 的 key 值也是不可以重复的:

1
2
3
4
5
6
7
8
const map = new Map([
[obj1, "abc"],
[obj2, "cba"],
[obj1, "nba"]
]);

console.log(map.get(obj1));
console.log(map.get(obj2));

3.2. Map 的常见方法

Map 常见的属性:

  • size:返回 Map 中元素的个数;

Map 常见的方法:

  • set(key, value):在 Map 中添加 key、value,并且返回整个 Map 对象;

  • get(key):根据 key 获取 Map 中的 value;

  • has(key):判断是否包括某一个 key,返回 Boolean 类型;

  • delete(key):根据 key 删除一个键值对,返回 Boolean 类型;

  • clear():清空所有的元素;

  • forEach(callback, [, thisArg]):通过 forEach 遍历 Map;

1
2
3
4
5
6
7
8
9
const obj = {};
map.set(obj, "mba");
map.get(obj);
map.has(obj);
map.delete(obj);
map.clear();
map.forEach((value, key, map) => {
console.log(value, key, map);
});

Map 也可以通过 for of 进行遍历:

1
2
3
for (const item of map) {
console.log(item);
}

3.3. WeakMap 使用

和 Map 类型的另外一个数据结构称之为 WeakMap,也是以键值对的形式存在的。

那么和 Map 有什么区别呢?

  • 区别一:WeakMap 的 key 只能使用对象,不接受其他的类型作为 key;

  • 区别二:WeakMap 的 key 对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么 GC 可以回收该对象;

1
2
3
const weakMap = new WeakMap(); // Invalid value used as weak map key
weakMap.set(1, "abc"); // Invalid value used as weak map key
weakMap.set("aaa", "cba");

WeakMap 常见的方法有四个:

  • set(key, value):在 Map 中添加 key、value,并且返回整个 Map 对象;

  • get(key):根据 key 获取 Map 中的 value;

  • has(key):判断是否包括某一个 key,返回 Boolean 类型;

  • delete(key):根据 key 删除一个键值对,返回 Boolean 类型;

注意:WeakMap 也是不能遍历的

  • 因为没有 forEach 方法,也不支持通过 for of 的方式进行遍历;
1
2
3
4
// TypeError: weakMap is not iterable
for (const item of weakMap) {
console.log(item);
}

那么我们的 WeakMap 有什么作用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => {
effect();
});
}
}

let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}

// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap();
function getDep(target, key) {
// 1.根据对象(target)取出对应的Map对象
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}

// 2.取出具体的dep对象
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}

return dep;
}

// vue3对raw进行数据劫持
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key);
dep.depend();
return target[key];
},
set(target, key, newValue) {
const dep = getDep(target, key);
target[key] = newValue;
dep.notify();
}
});
}

上面的代码其实是通过 WeakMap 来收集响应式对象的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const obj1 = {
name: "why",
age: 18
};

const obj2 = {
address: "北京市"
};

function nameFn1() {
console.log("nameFn1");
}

function nameFn2() {
console.log("nameFn2");
}

function ageFn1() {
console.log("ageFn1");
}

function addressFn1() {
console.log("addressFn1");
}

const obj1Map = new Map();
obj1Map.set("name", [nameFn1, nameFn2]);
obj1Map.set("age", [ageFn1]);
weakMap.set(obj1, obj1Map);

const obj2Map = new Map();
obj2Map.set("address", [addressFn1]);
weakMap.set(obj2, obj2Map);

文章转载于coderwhy | JavaScript 高级系列(十三) - ES6~ES13-ES6 核心知识点