一. Symbol
1.1. Symbol的基本使用
Symbol是什么呢?Symbol是ES6中新增的一个基本数据类型,翻译为符号。
那么为什么需要Symbol呢?
- 在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
- 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
- 比如我们前面在讲apply、call、bind实现时,我们有给其中添加一个fn属性,那么如果它内部原来已经有了fn属性了呢?
- 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;Symbol就是为了解决上面的问题,用来生成一个独一无二的值。
- Symbol值是通过Symbol函数来生成的,生成后可以作为属性名;
也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol;
const s = Symbol()
console.log(s) // Symbol()
Symbol即使多次创建值,它们也是不同的:
- Symbol函数执行后每次创建出来的值都是独一无二的;
const s1 = Symbol()
const s2 = Symbol()
console.log(s1 === s2) // false
我们也可以在创建Symbol值的时候传入一个描述description:
- 这个是ES2019(ES10)新增的特性;
const s3 = Symbol("abc")
console.log(s3.description)
1.2. Symbol作为属性名
我们通常会使用Symbol在对象中表示唯一的属性名:
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值来获取值:
console.log(info[s1]) // abc
console.log(info[s2]) // cba
// 不能这样获取
console.log(info.s1) // undefined
但是通过Symbol添加的属性名,在遍历时,是无法获取到的:
console.log(Object.keys(info))
console.log(Object.getOwnPropertyNames(info))
如果我们希望获取Symbol的key,那么需要通过
Object.getOwnPropertyNames:
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;
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构造函数(暂时没有字面量创建的方式):
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有一个非常常用的功能就是给数组去重。
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;
set.add(100)
set.delete(100)
set.has(100)
set.clear()
set.forEach(item => {
console.log(item)
})
另外Set是支持for of的遍历的:
for (const item of set) {
console.log(item)
}
2.3. WeakSet使用
和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。
那么和Set有什么区别呢?
- 区别一:WeakSet中只能存放对象类型,不能存放基本数据类型;
- 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;
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对象来调用:
class Person {
running() {
console.log("running", this)
}
}
const p = new Person()
p.running()
p.running.call({name: "why"})
我们可以通过WeakSet来存储创建出来的对象,并且在调用方法时判断这个对象是否存在于WeakSet中:
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;
const obj = {name: "why"}
const info = {
[obj]: "kobe"
}
console.log(info) // { '[object Object]': 'kobe' }
const obj2 = {}
console.log(info[obj2]) // kobe
所以这显然不是我们想要的效果,那么我们就可以使用Map:
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的时候,传入一个数组结构,数组结构中是一个个键值对的数组类型:
const map = new Map([
[obj1, "abc"],
[obj2, "cba"]
])
console.log(map.get(obj1))
console.log(map.get(obj2))
另外Map的key值也是不可以重复的:
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;
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进行遍历:
for (const item of map) {
console.log(item)
}
3.3. WeakMap使用
和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。
那么和Map有什么区别呢?
- 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key;
- 区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;
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的方式进行遍历;
// TypeError: weakMap is not iterable
for (const item of weakMap) {
console.log(item)
}
那么我们的WeakMap有什么作用呢?
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来收集响应式对象的依赖:
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)