Object代理

本节着手实现响应式数据。前面我们使用get去拦截对属性的读取操作。但在响应系统中,读取是一个很宽泛的概念,有很多操作都涉及到读取这一行为。下面列出了对一个普通 对象的所有可能的读取操作:

  • 访问属性:obj.foo
  • 判断对象或原型上是否存在给定的 key:key in obj
  • 使用 for...in 循环遍历对象:for (const key in obj) {}

对于属性读取(obj.foo),我们知道这可以通过get实现,而对于in和for in应该如何拦截呢?

这是我们在上一节里完成的部分基本代码,在此基础上实现后续功能:

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
60
61
62
63
64
65
66
67
68
69
70
71
const bucket = new WeakMap();
let activeEffect = null;
const effectStack = [];

// effect函数用于注册副作用函数
// fn副作用函数,options选项参数,允许用户指定调度器
function effect(fn,options = {}){
const effectFn = () => {
cleanup(effectFn) // 调用前清理副作用函数的依赖关系,防止无效依赖触发
activeEffect = effectFn; // 设置当前副作用函数
effectStack.push(effectFn); // 将副作用函数压入栈中
const res = fn(); // 执行副作用函数并保存结果
effectStack.pop(); // 执行后弹出栈
activeEffect = effectStack[effectStack.length - 1]; //重新设置activeEffect
return res; // 设置res为副作用函数的返回值
}
effectFn.options = options; // 将选项参数挂载到副作用函数上
effectFn.deps = [] // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
if(!options.lazy){ // 如果不是延迟执行,则立即执行副作用函数
effectFn()
}
return effectFn // 返回副作用函数。如果将此函数看作返回任何值的getter,就可以视作computed的实现
}

function track(target,key){
if(!activeEffect){
return
}
let depsMap = bucket.get(target)
if(!depsMap){
bucket.set(target,(depsMap = new Map()))
}
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key,(deps = new Set()))
}
deps.add(activeEffect) // deps:副作用函数集合
activeEffect.deps.push(deps)
}
// trigger函数用于触发副作用函数
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap){
return
}
const depsEffects = depsMap.get(key)
const effectsToRun = new Set() // 存储要执行的副作用函数
depsEffects && depsEffects.forEach(effectFn => {
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行,防止死循环
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
effectsToRun&&effectsToRun.forEach(fn => {
// 若存在调度器函数,则执行调度器函数并将函数作为参数传递,否则直接执行副作用函数
// console.log("trigger",fn.options.scheduler)
if(fn.options.scheduler){
fn.options.scheduler(fn) // 调度器函数
}else{
fn() // 执行副作用函数
}
});
}
// cleanup清理副作用函数的依赖关系,防止无效依赖触发
function cleanup(effectFn){
for(let i =0;i<effectFn.deps.length;i++){
const deps = effectFn.deps[i] // set集合
deps.delete(effectFn) // 删除副作用函数
}
effectFn.deps.length = 0
}

in操作符

使用in操作符检查对象上是否具有给定的key属于读取操作。

以下是原书中对in操作符拦截的思路:

image-20250426181932120

image-20250426182009085

in 操作符的运算结果是通过调用抽象方法HasProperty 得到的, 而抽象方法HasProperty的返回值是通过 调用对象的内部方法 [[HasProperty]] 得到的,它对应的拦截函数名叫 has,因此我们可以通过 has拦截函数实现对 in 操作符的代理:

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {foo:1,bar:2}

const p =new Proxy(obj,{
has(target,key){
track(target,key)
return Reflect.has(target,key)
}
})

effect(()=>{
'foo' in p // 触发track函数,收集依赖
})

这样,当我们在副作用函数中通过 in 操作符操作响应式数据时, 就能够建立依赖关系。

for…in循环

以下是原书中for in循环的拦截思路:

image-20250426182214790

image-20250426182257681

可以看到EnumerateObjectProperties是一个generator 函数,接收一个参数 obj。实际上,obj 就是被 for…in 循环遍历的对象,其关键点在于使用 Reflect.ownKeys(obj)来获取只属于对象自身拥有的键。

也就是说,我们可以使用 ownKeys 拦截函数来拦截 for...in里发生的Reflect.ownKeys操作,即可间接拦截 for...in循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {foo:1}
const ITERATE_KEY = Symbol()

const p =new Proxy(obj,{
ownKeys(target){
track(target,ITERATE_KEY)
return Reflect.ownKeys(target)
}
})

effect(()=>{
for(let key in p){ // 触发ownKeys,收集依赖
console.log('key',key)
}
})

ownKeys用来获取一个对象的所有属于自己的键值,参数只能拿到目标对象target。在调用track时,为了传入for...in操作对应的key,我们需要自己设置一个key的标识ITERATE_KEY。在触发响应trigger时,我们同样传入这个标识。

这里有一个问题,当我们设置一个新属性(比如p.bar = 3)时,会触发 set 拦截函数执行。此时 set 拦截函数接收到和传给trigger 函数的 key 就是字符串 ‘bar’。于此同时,由于添加了新属性,for in的循环执行也会从原来的一次变成两次,这对副作用函数的执行有影响。但根据前文,for…in 循环是在副作用函数与 ITERATE_KEY 之间建立联系,和 ‘bar’ 没有关系,导致我们无法触发ITERATE_KEY的副作用函数。

问题的关键就是触发此副作用函数,所以解决方法也就出来了,在添加属性操作时,我们同样执行ITERATE_KEY的副作用函数就可以了。于是我们修改trigger函数:

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
// trigger函数用于触发副作用函数
function trigger(target,key){
const depsMap = bucket.get(target)
if(!depsMap){
return
}
const depsEffects = depsMap.get(key)
const iterateEffects = depsMap.get(ITERATE_KEY) // ITERATE_KEY的副作用函数
const effectsToRun = new Set()
depsEffects && depsEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
// 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
effectsToRun&&effectsToRun.forEach(fn => {
if(fn.options.scheduler){
fn.options.scheduler(fn)
}else{
fn()
}
});
}

但这样又有了新问题,我们添加属性时需要执行ITERATE_KEY相关联的副作用函数,但在修改属性时,我们是不需要执行其副作用函数的。但由于我们都是通过set拦截的,要想区分这两种,我们需要修改我们的拦截函数:

1
2
3
4
5
6
7
8
9
const p =new Proxy(obj,{
set(target,key,newVal,receiver){
// 判断属性是否存在,设置trigger的type
const type = Object.prototype.hasOwnProperty.call(target,key)?'SET':'ADD'
const res = Reflect.set(target,key,newVal,receiver)
trigger(target,key,type)
return res
}
})

我们给trigger传入第三个参数type用于区分操作类型,并且只有当操作类型 type 为 ‘ADD’ 时,才会触发与 ITERATE_KEY 相关联的副作用函数重新执行,避免了不必要的性能损耗:

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
function trigger(target,key,type){
const depsMap = bucket.get(target)
if(!depsMap){
return
}
const depsEffects = depsMap.get(key)
const effectsToRun = new Set()
depsEffects && depsEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
// 只有当操作类型为 'ADD' 时,才触发与 ITERATE_KEY 相关联的副作用函数重新执行
if(type === 'ADD'){
const iterateEffects = depsMap.get(ITERATE_KEY) // 触发迭代器的副作用函数
iterateEffects && iterateEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
}
effectsToRun&&effectsToRun.forEach(fn => {
if(fn.options.scheduler){
fn.options.scheduler(fn)
}else{
fn()
}
});
}

delete操作符

关于对象的代理,还剩下最后一项工作需要做,即删除属性操作的代理:delete p.foo

以下是原书的描述:

image-20250426200326965

image-20250426200347191

delete 操作符的行为依赖 [[Delete]]内部方法,这个内部方法通过使用deleteProperty拦截。我们先检查是否存在此属性,再通过Reflect.deleteProperty进行属性删除,最后调用trigger函数重新执行副作用函数:

1
2
3
4
5
6
7
8
9
10
11
12
const p =new Proxy(obj,{
deleteProperty(target,key){
// 检查被操作的属性是否是对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target,key)
// 使用 Reflect.deleteProperty 完成属性的删除
const res = Reflect.deleteProperty(target,key)
if(res && hadKey){
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target,key,'DELETE')
}
}
})

由于delete删除了对象,减少了for in的循环次数,我们也需要触发ITERATE_KEY 相关联的副作用函数重新执行:

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
function trigger(target,key,type){
const depsMap = bucket.get(target)
if(!depsMap){
return
}
const depsEffects = depsMap.get(key)

const effectsToRun = new Set()
depsEffects && depsEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
// 当操作类型为 ADD 或 DELETE 时,需要触发与 ITERATE_KEY 相关联的副作用函数重新执行
if(type === 'ADD'||type === 'DELETE'){
const iterateEffects = depsMap.get(ITERATE_KEY) // 触发迭代器的副作用函数
iterateEffects && iterateEffects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn)
}
})
}
effectsToRun&&effectsToRun.forEach(fn => {
if(fn.options.scheduler){
fn.options.scheduler(fn)
}else{
fn()
}
});
}

reactive响应触发

我们从规范的角度详细介绍了如何代理对象,在这个过程中,处理了很多边界条件。但想要合理地触发响应,还有许多工作要做。从这里开始我们逐步实现并完善reactive对各种类型的对象的代理。

值无变化取消响应

首先第一个问题,当setter设置的值没有发生变化时, 不应该触发响应。

为了满足需求,我 们需要修改set 拦截函数的代码,在调用 trigger 函数触发响应之 前,需要检查值是否真的发生了变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
const p =new Proxy(obj,{
set(target,key,newVal,receiver){
// 获取旧值
const oldVal = target[key] // 访问的原始对象,不触发代理对象的getter
const type = Object.prototype.hasOwnProperty.call(target,key)?'SET':'ADD'
const res = Reflect.set(target,key,newVal,receiver)
if(oldVal !== newVal){
// 只有当新值与旧值不相等时,才触发更新
trigger(target,key,type)
}
return res
},
)

我们在 set 拦截函数内首先获取旧值 oldVal,接着比较新值与旧值,只有当它们不全等的时候才触发响应。但还有一个问题,对于NaN来说,全等比较的结果是false:

1
2
NaN === NaN // false
NaN !== NaN // true

因此我们需要对NaN这个特例进行条件判断:

1
2
3
4
5
6
7
8
9
10
11
set(target,key,newVal,receiver){
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target,key)?'SET':'ADD'
const res = Reflect.set(target,key,newVal,receiver)
// 比较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应
// NaN和所有值都不相等,包括自己。可以通过此特点进行判断:
if(oldVal !== newVal&&(oldVal === oldVal || newVal === newVal)){
trigger(target,key,type)
}
return res
},

这样我们就解决了 NaN 的问题。

从原型上继承属性

接下来我们讨论一种从原型上继承属性的情况,从现在起我们将先前的proxy封装为一个reactive函数,它接收一个对象作为参数,返回为其创建的响应式数据:

1
2
3
4
5
6
function reactive(obj){
return new Proxy(obj,{
getter...
setter...
})
}

我们用reactive函数设置一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const proto = {foo:1}
const data = {}
const parent = reactive(proto)
const child = reactive(data)
// 将parent设置为child的原型
Object.setPrototypeOf(child,parent)

effect(()=>{
console.log('foo',child.foo)
})
child.foo = 2
child.foo = 3

// console:foo 1
// foo 2
// foo 2
// foo 3

我们定义了空对象data 和对象 proto,分别为二者创建了对应的响应式数据 child 和 parent,并且使用 Object.setPrototypeOf 将 parent 设置为 child 的原型。接着访问child.foo属性,由于child本身没有设置foo属性,因此这个foo值是从原型parent上继承来的,控制台输出foo 1(没有设置原型会输出undefined)。

但当我们赋值child.foo = 2时,控制台输出了两次,也就是说这里的副作用函数执行了两次,产生了一次不必要的更新。

以下是原书中对原因的分析:

image-20250509194224966

image-20250509194414717

image-20250509194455066

image-20250509194708582

总结一下,当我们添加副作用函数时,console.log('foo',child.foo)触发了child的getter,但由于child本身没有这个属性,系统就会取它的原型parent并调用原型的getter,副作用函数同时被child和parent收集了。同样的,当我们设置child.foo = 2时,触发了child的setter,但由于child本身没有这个属性,系统就会取它的原型parent并调用原型的setter,于是就会导致副作用函数被再次执行。

于是解决方法就明确了,只要屏蔽其中一次就可以了。我们选择将parent的副作用函数执行屏蔽,在setter里区分这两次更新。

我们来看两次setter有何不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// child setter
// 先调用
set(target, key, value, receiver) {
// target 是原始对象 data
// receiver 是代理对象 child
// receiver就是target的代理对象
}

// parent setter
// 后调用
set(target, key, value, receiver) {
// target 是原始对象 proto
// receiver 仍然是代理对象 child
// receiver不是target的代理对象
}

在 JavaScript 的 Proxy 机制中,receiver 参数代表 实际触发属性操作的对象,即最初发起操作的代理对象。我们可以看到,child和parent的receiver都是 child,而target 则是变化的。对child来说receiver就是target的代理对象,但parent却不是。所以解决办法就有了:判断 receiver 是否是 target 的代理对象,只有当 receiver 是 target 的代理对象时才触发更新。

我们为getter添加一个能力,使代理对象在访问一个特定属性raw时,可以得到它的原始对象:

1
2
3
4
5
6
7
8
9
10
11
12
get(target,key,receiver){
// 代理对象访问raw属性时返回其原始对象
if(key === 'raw'){
return target
}
track(target,key)
return Reflect.get(target,key,receiver)
},

// example
child.raw === data // true
parent.raw === proto // true

这样我们就可以通过访问raw得到其原始对象。于是我们就可以在setter里进行判断了:

1
2
3
4
5
6
7
8
9
10
11
12
set(target,key,newVal,receiver){
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target,key)?'SET':'ADD'
const res = Reflect.set(target,key,newVal,receiver)
// receiver.raw === target => receiver的原始对象是target
if(receiver.raw === target){
if(oldVal !== newVal&&(oldVal === oldVal || newVal === newVal)){
trigger(target,key,type)
}
}
return res
},

这样我们就成功屏蔽掉原型引起的更新了,从而避免不必要的更新操作。

浅响应与深相应

目前我们实现的reactive是浅响应的,简单来说,我们的响应式对象的属性如果包含了另一个对象,修改此对象并不能触发响应:

1
2
3
4
5
6
7
const obj = reactive({ foo: { bar: 1 } })

effect(()=>{
console.log('foo',obj.foo)
})

obj.foo.bar = 4 // 不能触发响应

我们来看一下此时的reactive实现:

1
2
3
4
5
6
7
8
get(target,key,receiver){
if(key === 'raw'){
return target
}
track(target,key)
// 读取属性时直接返回结果
return Reflect.get(target,key,receiver)
},

当我们读取obj.foo.bar时,首先要通过getter访问obj.foo,但这里通过Reflect.get 得到的结果是一个普通对象{bar:1},并不是响应式对象。因此要实现对象属性的响应,我们需要对Reflect.get的返回值进行一次封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
get(target,key,receiver){
if(key === 'raw'){
return target
}
track(target,key)
// 读取原始值结果
const res = Reflect.get(target,key,receiver)
// 判断是否为对象
if(typeof res === 'object' && res !== null){
return reactive(res) // 包装成响应式数据
}
return res
},

这样我们访问对象属性时,通过递归调用reactive将其包装成响应式数据,就能触发副作用函数重新执行了。reactive成功实现了深响应。

整理以上代码,使用createReactive函数替代reactive的代理内容,并使用shallowReactive浅响应(只响应对象的第一层属性),与原本的reactive深响应区分开:

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
function createReactive(obj,isShallow = false){
return new Proxy(obj,{
get(target,key,receiver){
if(key === 'raw'){
return target
}
track(target,key)
const res = Reflect.get(target,key,receiver)
// 浅响应则直接返回
if(isShallow){
return res
}

if(typeof res === 'object' && res !== null){
return reactive(res) // 递归代理嵌套对象
}
return res
},
...,
...
})
}
// 深响应
function reactive(obj){
return createReactive(obj)
}
// 浅响应
function shallowReactive(obj){
return createReactive(obj,true)
}

只读与浅只读

我们希望一些数据是只读的。只读本质上也是对数据对象的代理,同样可以使用上文的createReactive函数实现。我们添加第三个参数isReadonly,并修改代理内容:

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
function createReactive(obj,isShallow = false,isReadonly = false){
return new Proxy(obj,{
set(target,key,newVal,receiver){
// 如果只读则直接返回
if(isReadonly){
console.warn(`属性${key}是只读的,不能被修改`)
return true
}
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target,key)?'SET':'ADD'
const res = Reflect.set(target,key,newVal,receiver)
if(receiver.raw === target){
if(oldVal !== newVal&&(oldVal === oldVal || newVal === newVal)){
trigger(target,key,type)
}
}
return res
},
deleteProperty(target,key){
// 如果只读则直接返回
if(isReadonly){
console.warn(`属性${key}是只读的,不能被删除`)
return true
}
const hadKey = Object.prototype.hasOwnProperty.call(target,key)
const res = Reflect.deleteProperty(target,key)
if(res && hadKey){
trigger(target,key,'DELETE')
}
}
}
get(target,key,receiver){
if(key === 'raw'){
return target
}
// 如果只读则不触发track副作用函数收集函数,因为值不能修改,不会触发trigger
if(!isReadonly){
track(target,key)
}
const res = Reflect.get(target,key,receiver)
if(isShallow){
return res
}

if(typeof res === 'object' && res !== null){
return reactive(res)
}
return res
},

),

}

function readonly(obj){
return createReactive(obj,false,true)
}

如上代码所示,我们使用createReactive创建代理对象时,通过第三个参数isReadonly指定是否创建一个只读的代理对象,并修改setdeleteProperty 拦截函数,一旦数据是只读的,则会自动拦截修改操作,提示这是非法操作。由于数据是只读的无法修改,因此不会触发set的trigger函数,也就不需要事先搜集副作用函数依赖了。我们在get拦截函数里进行判断,只读时取消track函数的触发。

基于以上操作,我们就可以实现readonly函数,通过调用createReactive实现只读。

和先前的浅响应与深相应一样,只读也有类似的问题。以上的readonly只实现了浅只读。我们需要对get拦截进行类似的数据封装:

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
get(target,key,receiver){
if(key === 'raw'){
return target
}
if(!isReadonly){
track(target,key)
}
const res = Reflect.get(target,key,receiver)
// 浅响应则直接返回
if(isShallow){
return res
}
if(typeof res === 'object' && res !== null){
// 只读则调用 readonly 对值进行包装
return isReadonly ? readonly(res) : reactive(res)
}
return res
},


// 深只读
function readonly(obj){
return createReactive(obj,false,true)
}
// 浅只读
function shallowReadonly(obj){
return createReactive(obj,true,true)
}

异质对象的代理

在 JavaScript 中有两种对象:常规对象和异质对象。这两类对象内部犯法的逻辑大体是相同的,但后者会有一些特殊方法与处理。为了更好地实现reactive的对象代理,我们需要针对这些和常规对象有差异的对象进行进一步的处理。

原书针对数组和集合的代理展开了长篇幅的深入介绍,由于篇幅实在太长,暂时就跳过了,有空再单独更新重点部分。

代理数组

数组对象除了 [[DefineOwnProperty]] 这个内部方法之 外,其他内部方法的逻辑都与常规对象相同。因此,当实现对数组的代理时,用于代理普通对象的大部分代码可以继续使用:

1
2
3
4
5
6
7
const arr = reactive([1,2,3])

effect(()=>{
console.log('数组',arr[0])
})

arr[0] = 2 // 可以触发effect

当我们通过索引读取或 设置数组元素的值时,代理对象的 get/set 拦截函数也会执行,因此 我们不需要做任何额外的工作,就能够让数组索引的读取和设置操作是响应式的了。

但对数组的操作与对普通对象的操作仍然存在不同,下面总结了所有对数组元素或属性的“读取”操作。

  • 通过索引访问数组元素值:arr[0]。
  • 访问数组的长度:arr.length。
  • 把数组作为对象,使用 for…in 循环遍历。
  • 使用 for…of 迭代遍历数组。
  • 数组的原型方法,如 concat/join/every/some/find/findIndex/includes 等,以及其他所有不改变原数组的原型方法。

可以看到,对数组的读取操作要比普通对象丰富得多。我们再来看看对数组元素或属性的设置操作有哪些。

  • 通过索引修改数组元素值:arr[1] = 3。
  • 修改数组长度:arr.length = 0。
  • 数组的栈方法:push/pop/shift/unshift。
  • 修改原数组的原型方法:splice/fill/sort 等。

除了通过数组索引修改数组元素值这种基本操作之外,数组本身 还有很多会修改原数组的原型方法。有些方法的操作语义是“读取”,而有些方法的操作语义是“设置”。 因此,当这些操作发生时,也应该正确地建立响应联系或触发响应。接下来,我们针对这些数组的操作挨个说起。

数组索引index与length

遍历数组

数组的查找

数组的长度修改

代理集合 Set & Map

本节介绍集合类型数据的响应式方案。集合类型包括 Map/Set 以及 WeakMap/WeakSet。使用 Proxy 代理集合类型的 数据不同于代理普通对象,因为集合类型数据的操作与普通对象存在 很大的不同。下面总结了 Set 和 Map 这两个数据类型的原型属性和方法。

Set 类型的原型属性和方法如下

  • size:返回集合中元素的数量。
  • add(value):向集合中添加给定的值。
  • clear():清空集合。
  • delete(value):从集合中删除给定的值。
  • has(value):判断集合中是否存在给定的值。
  • keys():返回一个迭代器对象。可用于 for…of 循环,迭代 器对象产生的值为集合中的元素值。
  • values():对于 Set 集合类型来说,keys() 与 values() 等 价。
  • entries():返回一个迭代器对象。迭代过程中为集合中的每一 个元素产生一个数组值 [value, value]。
  • forEach(callback[, thisArg]):forEach 函数会遍历集 合中的所有元素,并对每一个元素调用 callback 函数。 forEach 函数接收可选的第二个参数 thisArg,用于指定 callback 函数执行时的 this 值。

Map 类型的原型属性和方法如下

  • size:返回 Map 数据中的键值对数量。
  • clear():清空 Map。
  • delete(key):删除指定 key 的键值对。
  • has(key):判断 Map 中是否存在指定 key 的键值对。
  • get(key):读取指定 key 对应的值。
  • set(key, value):为 Map 设置新的键值对。
  • keys():返回一个迭代器对象。迭代过程中会产生键值对的 key 值。
  • values():返回一个迭代器对象。迭代过程中会产生键值对的 value 值。
  • entries():返回一个迭代器对象。迭代过程中会产生由 [key, value] 组成的数组值。
  • forEach(callback[, thisArg]):forEach 函数会遍历 Map 数据的所有键值对,并对每一个键值对调用 callback 函 数。forEach 函数接收可选的第二个参数 thisArg,用于指定 callback 函数执行时的 this 值。

可以发现Map和Set还是挺类似的,区别主要在于添加和读取元素的方法:Set 类型使用 add(value) 方添加元素,而Map 类型使用 set(key, value) 方法设置键值对,并且Map类型可以使用 get(key) 方法读取相应的值。

如何代理 Set 和 Map

建立响应联系

避免污染原始数据

处理 forEach

迭代器方法

values 与 keys 方法