前言
本节介绍Vue3源码是如何拦截Set和Map数据结构的,参考《Vue.js设计与实现》
和Vue3的源码
。如下给出了回答简介版
简洁版
- vue3源码如何
代理Set和Map
数据结构?
- 还是利用Set和Map,在get拦截函数中拦截
访问操作
,在set拦截函数中拦截设置
操作
- 只是代理Set和Map时会出现一些问题,所以还是对Set和Map的方法进行了重新拦截设置
- 代理set时,如何避免
访问size属性
报错?
- Proxy包装后,会去代理对象身上找
size
属性,但是代理对象身上没有,就会报错
- 通过
Reflect.get(target, key, target)
修改this指向,当访问size
属性时去原始对象
身上找
- vue3源码如何
避免污染原始数据
?
- 如果把原始对象设置到代理对象身上,会造成访问原始对象也触发响应
- 这时需要在set函数触发时,通过
raw
属性获取到原始值,设置给target对象
- 不管是Map数据结构的
set
还是Set数据结构的add
- 如何拦截
forEach
操作
- 使用Proxy进行拦截,当访问forEach方法时会
触发get拦截函数
进行依赖收集,并对forEach方法进行重写
- 当map的响应式数据发生新增、删除、
修改值
时,会触发forEach对应的依赖
- 使用
wrap函数
对遍历时访问到的值和键进行递归处理
- 使用
ITERATE_KEY
这个Symbol类型的key来作为追踪标识
- 如何拦截
for of
协议、entries、values、keys
- 部署了
Symbol.iterator
方法,才可以被for of方法遍历
- 由于代理对象proxy上没有这个方法,所以必须要重写来执行
原始对象身上自己的这个方法
,并返回Symbol.iterator方法。注意,这几个方法的拦截都需要重写
- 如果要拦截
entries
,还需要重写的对象中返回next
迭代器协议。注意可迭代协议是Symbol.iterator方法,迭代器协议是next方法,这两个不一样!
-
values和keys
需要调用原始对象身上对应的values方法和keys方法。for of, entries, values
都需要在新增 设置 删除
触发,keys
只需要在新增和删除
的时候触发,所以keys要设置新的symobl
的key区分开
更多Vue源码文章:
1. Vue3 源码解析(一):响应式数据和副作用函数、计算属性原理、侦听器原理
2. Vue3源码解析(二):响应式原理,如何拦截对象
3. Vue3源码解析(三):响应式原理,如何拦截数组
4. Vue2源码解析(一):响应式原理,如何拦截对象
5. Vue2源码解析(二):响应式原理,如何拦截数组
1 代理Set和Map
代理Set和Map
的方式与代理普通对象大体相同,在get时进行track依赖追踪,在set时进行trigger触发对应依赖,但是需要解决一些问题
1.1 解决访问Set的size属性报错
当使用Proxy代理Set,并且访问代理对象的size属性会报错
let set = new Set([1, 2, 3]);
let p1 = reactive(set, {});
effect(() => {
console.log(p1.size, "p1.size");
});

报错原因是,访问p1.size
属性时,其内部的this
会指向p1
,并且其内部会检测是否存在[[SetData]]
内部槽,由于p1
代理对象上不存在,只有Set原始对象上存在,所以会报错。
修改方式是:get访问器
函数触发时,如果访问的size属性
,在Reflect.get
函数里面修改this
指向为target
,这样就会去原始对象身上找[[SetData]]
内部方法,能够找到就解决了报错问题,
let proxy1 = new Proxy(set, {
get(target, key, receiver) {
if (key === "size") {
+ return Reflect.get(target, key, target);
}
return Reflect.get(target, key, receiver);
},
});
1.2 解决调用set.delete()方法报错
当调用delete方法时报错:
p1.delete(1);

调用p1.delete
是调用方法
不是访问属
,不能像下面这样写,因为delete函数内部的this
依然指向proxy
代理对象
let proxy1 = new Proxy(set, {
get(target, key, receiver) {
+ if (key === "size" || key === "delete") {
return Reflect.get(target, key, target);
}
return Reflect.get(target, key, receiver);
},
});
应该通过.bind方法修改函数内部调用的this
let proxy1 = new Proxy(set, {
get(target, key, receiver) {
if (key === "size") {
return Reflect.get(target, key, target);
}
+ return target[key].bind(target);
},
});
现在能修改成功

修改createReactive
函数
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === "raw") {
return target;
}
// 针对Set数据结构的拦截
if (key === "size") {
return Reflect.get(target, key, target);
}
// delete走这里
return target[key].bind(target);
},
})
}
之前的调用改为这样
let set = new Set([1, 2, 3]);
+let proxy1 = reactive(set);
effect(() => {
console.log(proxy1.size, "proxy1.size");
});
2 建立响应连接
接下来目标是,当执行代理对象的.add
和.delete
方法时,收集所有访问其size
属性的副作用函数并执行
第一步,需要在get拦截函数里面,触发track收集依赖
get(target, key, receiver) {
if (key === "raw") {
return target;
}
if (key === "size") {
// 触发拦截依赖
+ track(target, ITERATE_KEY);
return Reflect.get(target, key, target);
}
return target[key].bind(target);
},
注意,track传入的key必须是ITERATE_KEY
,之前在拦截数组
的for in
和for of
遍历操作时,增加了这个Symbol键
,当时是只要往数组里面增加值
或者删除值
都会触发这个键对应的副作用函数。现在也用这个键,只要往set
里面增加或者删除值,都要触发size重新响应。
你可以这样理解,假设页面有一个模版
访问了set.size
,当你在js中往set里面增加了值,模版的size也要进行更新,他不涉及具体的key,所以用ITERATE_KEY
这个key
接着我们声明一个对象,里面存储set会用到的方法
const mutableInstrumentations = {
add(key) {
},
};
修改get里面的拦截函数,在get拦截函数里面如果访问proxy.set或者proxy.delete,都会执行上面对象里面的方法。
注意,在对Proxy拦截时,这个get的return mutableInstrumentations[key];
很关键,后续执行map或者set的任何属性或者方法,都会触发mutableInstrumentations[key]
里面对应的函数
get(target, key, receiver) {
if (key === "raw") {
return target;
}
// 针对Set数据结构的拦截
if (key === "size") {
track(target, ``);
return Reflect.get(target, key, target);
}
// 这里
+ return mutableInstrumentations[key];
},
add方法实现如下:
add(key) {
// 通过.raw属性来访问原始对象
const target = this.raw;
// 判断值在不在
const hadKey = target.has(key);
// 使用原始对象执行add方法
const res = target.add(key);
// 触发trigger响应,指定操作类型为ADD
if (!hadKey) {
trigger(target, key, "ADD");
}
return res;
},
- 因为在get拦截函数的末尾,使用的是
return mutableInstrumentations[key];
没有绑定bind函数,所以add里面的this还是指向proxy代理对象,
- 直接通过
this.raw
获取原生对象
,执行add操作
,同时判断如果已经有这个值
,不执行trigger函数触发副作用函数
- trigger函数传入
ADD
类型,就会把ITERATE_KEY
对应的副作用函数拿出来执行
delete方法的实现如下,和add几乎一样,只是执行target.delete,并且要判断有这个值,才能删除
delete(key) {
// 通过.raw属性来访问原始对象
const target = this.raw;
// 判断值在不在
const hadKey = target.has(key);
// 使用原始对象执行delete方法
const res = target.delete(key);
// 值不在时才去触发trigger响应,指定操作类型为DELETE
if (hadKey) {
trigger(target, key, "DELETE");
}
return res;
},

3 避免污染原始数据
3.1 对Map的set和get方法拦截
如下先实现针对Map数据结构的get和set方法的拦截:
const mutableInstrumentations = {
……
get (key) {
const target = this.raw;
const hadKey = target.has(key);
// 追踪key建立响应响应的联系
track(target, key);
if (hadKey) {
const res = target.get(key);
// 如果res是对象,则继续执行reactive递归生成响应式数据
return typeof res === "object" ? reactive(res) : res;
}
},
set (key, value) {
// 原始对象
const target = this.raw;
// 判断读取的key是否存在
const had = target.has(key)
// 旧值
const oldVal = target[key]
target.set(key, value)
// 触发trigger响应,指定操作类型为ADD
if (!had) {
// 新增
trigger(target, key, "ADD");
} else if (oldVal !== value && (oldVal === oldVal && value === value)) {
// 修改
trigger(target, key, "SET");
}
}
};
get方法的逻辑和之前的has类似,需要注意:第一需要使用track
进行依赖收集,第二如果拿到的结果是对象,则需要进行递归处理继续调用reactive
方法
set方法中,需要判断是否有这个值,如果有操作类型就是SET
,如果没有操作类型就是ADD
此时实现的效果是,当你在副作用函数里面访问对应的key时,之后在调用set方法,就会再次触发副作用函数
let map = new Map([
['key', 1],
['name', 2],
]);
let proxy = reactive(map);
effect(() => {
console.log(proxy.get('key'), "proxy1.key");
});
我们在控制台操作效果如下,成功打印了key

3.2 数据污染
原始数据污染是指:当我们把响应式数据设置给原始数据对象时,对原始数据对象的修改也会触发响应。目前来看 Proxy
的拦截会造成数据污染
let map2 = new Map();
let p1 = reactive(map2)
let p2 = reactive(map2)
// 为p1设置一个键值对
p1.set('p2', p2)
effect(() => {
console.log(map2.get('p2').size, 'map2.get p2 . size');
})
如上,p1和p2都是基于map2生成响应式数据,我们进行的操作:
- 第一,把p2作为值设置给p1;
- 第二,访问原始对象的p2的size属性,这之后,当我们对原始map对象进行设置时,也触发了副作用函数:

原始对象变成了响应式数据,问题的原因如下:
set (key, value) {
// 原始对象
const target = this.raw;
// 判断读取的key是否存在
const had = target.has(key)
// 旧值
const oldVal = target[key]
+ // 这里把value响应式数据原封不动赋值给target了
+ target.set(key, value)
// 触发trigger响应,指定操作类型为ADD
if (!had) {
// 新增
trigger(target, key, "ADD");
} else if (oldVal !== value && (oldVal === oldVal && value === value)) {
// 修改
trigger(target, key, "SET");
}
}
应该这样解决:通过raw属性
拿到响应式数据的原始值
,并将原始数据赋值给target
set (key, value) {
// 原始对象
const target = this.raw;
// 判断读取的key是否存在
const had = target.has(key)
// 旧值
const oldVal = target[key]
// target.set(key, value) // 这样赋值就是变量污染
+ const rawValue = value.raw || value
+ target.set(key, rawValue)
// 触发trigger响应,指定操作类型为ADD
if (!had) {
// 新增
trigger(target, key, "ADD");
} else if (oldVal !== value && (oldVal === oldVal && value === value)) {
// 修改
trigger(target, key, "SET");
}
}
如上,如果value是个响应式数据,那么通过.raw能够拿到原始数据。如下操作后就没有触发副作用函数

Set数据结构封装的add方法也会出现响应式数据:
let set = new Set([1,2,3])
let p3 = reactive(set)
let p4 = reactive(set)

应该如下修改:
add(key) {
// 通过.raw属性来访问原始对象
const target = this.raw;
// 判断值在不在
const hadKey = target.has(key);
// 使用原始对象执行add方法
+ const rawKey = key.raw || key
+ const res = target.add(rawKey);
// 触发trigger响应,指定操作类型为ADD
if (!hadKey) {
trigger(target, key, "ADD");
}
return res;
},
此时原始数据正常了

对应源码如下,我截取了片段
/packages/reactivity/src/collectionHandlers.ts
set(this: MapTypes, key: unknown, value: unknown) {
if (!shallow && !isShallow(value) && !isReadonly(value)) {
+ value = toRaw(value)
}
// 获取原始值
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
+ target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
},
4 处理forEach
拦截forEach的操作如下:
- 需要将该对象和
ITERATE_KEY
绑定,因为任何修改map长度的操作都会影响forEach操作
const mutableInstrumentations = {
……
forEach (callback) {
const target = this.raw
// 与原始值建立响应练习
+ track(target, ITERATE_KEY)
// 执行原始值的forEach,将回调传过去
target.forEach(callback)
}
};
测试代码如下:
let map = new Map([
['key', 1],
['name', 2],
]);
let p1 = reactive(map);
effect(() => {
p1.forEach(function (value, key) {
console.log(value, 'forEach触发了 value');
console.log(key, 'forEach触发了 value');
})
});

4.1 递归处理响应值
当执行p1.get(key).delete(2)
时并不会触发副作用函数执行,因为value
是Set数据结构,是一个原始数据类型,访问value.size
无法建立响应链接
let key = {key: 1}
let value = new Set([1,2,3])
let map = new Map([
[key, value],
]);
let p1 = reactive(map);
effect(() => {
p1.forEach(function (value, key) {
console.log(value.size, '副作用函数 value.size');
})
});
p1.get(key).delete(2)
在forEach拦截函数中,将数据深层次递归,转化为响应式。并将最新的this传递过去
+forEach (callback, thisArg) {
+ const wrap = (val) => typeof val === 'object' ? reactive(val) : val
const target = this.raw
// 与原始值建立响应练习
track(target, ITERATE_KEY)
// 执行原始值的forEach,将回调传过去
+ target.forEach((v, k) => {
+ callback.call(thisArg, wrap(v), wrap(k), this)
+ })
}

4.2 区分for in和 forEach遍历
- for in遍历只关心键,只有对象数量发生变化,新增和删除才会触发对应副作用函数,修改不会
- forEach会访问值,所以当SET操作触发修改值,也应该要触发forEach对应的副作用函数
测试数据如下:
let map = new Map([
['a', 1],
['b', 2],
]);
let p1 = reactive(map);
effect(() => {
p1.forEach(function (value, key) {
console.log(value, '副作用函数 value');
})
});
此时修改'a'的值并没有触发forEach遍历,

trigger函数是这样修改:
- 判断是SET操作,并且是Map数据结构,则应该也要触发对应的副作用函数
function trigger(target, key, type, newVal) {
let depsMap = bucket.get(target);
if (!depsMap) return;
// 取得与key相关联的副作用函数
const effects = depsMap.get(key);
const effectsToRun = new Set();
// 将与key相关联的副作用函数添加到effectsToRun
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
// 操作类型是ADD的时候,把length对应的副作用函数取出来,加入到effectsToRun中拿出来执行
if (Array.isArray(target) && type === "ADD") {
const lengthOfEffects = depsMap.get("length");
lengthOfEffects &&
lengthOfEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
+ if (type === "ADD" || type === "DELETE" || (type === 'SET' && +Object.prototype.toString.call(target) === '[object Map]')) {
// 取得与ItERATE_KEY关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY);
// 将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
// 如果操作目标是数组,并且修改了数组的key属性
if (Array.isArray(target) && key === "length") {
depsMap.forEach((effects, effectKey) => {
if (effectKey >= newVal) {
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
});
}
effectsToRun &&
effectsToRun.forEach((effectFn) => {
if (effectFn.options && effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
源码如下:
// packages/reactivity/src/dep.ts
switch (type) {
case TriggerOpTypes.ADD:
if (!targetIsArray) {
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isArrayIndex) {
// new index added to array -> length changes
run(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!targetIsArray) {
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
+ case TriggerOpTypes.SET:
// 如果是SET操作,并且是Map,执行run触发依赖
+ if (isMap(target)) {
+ run(depsMap.get(ITERATE_KEY))
+ }
+ break
}
}
5. 迭代器方法
5.1 处理for of
Proxy直接拦截会报错
let map = new Map([
['a', 1],
['b', 2],
]);
let p1 = reactive(map);
effect(() => {
for (const [key, value] of p1.entries()) {
console.log(key, 'key');
console.log(value, 'value');
}
});

当使用for of遍历代理对象,会试图从代理对象身上找迭代器协议Symbol.iterator
方法,找不到就报错了。应该重写该方法
[Symbol.iterator] () {
// 1. 包装写法1
// const target = this.raw
// const itr = target[Symbol.iterator]()
// return itr
// 2. 包装写法2
const target = this.raw
const itr = target[Symbol.iterator]()
const wrap = (val) => {
return typeof val === 'object' && val !== null ? reactive(val) : val
}
// 建立响应依赖联系
track(target, ITERATE_KEY)
return {
next () {
// itr.next()的结果是['a', 1]和['b', 2]
const { value, done} = itr.next()
return {
// 非undefined则进行包裹
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done
}
}
}
}
测试复杂数据类型的值
let map = new Map([
['a', new Set([1,2,3])],
['b', new Set([1,2,3,4])],
]);
let p1 = reactive(map);
effect(() => {
for (const [key, value] of p1) {
console.log(value.size, 'value.size');
}
});
能够触发响应:

5.2 处理entries
首先需要理解一点,在map中map2[Symbol.iterator]
和map2.entries
是等价的:

但是直接拦截p1.entries会报错,如下:
let map = new Map([
['a', new Set([1,2,3])],
['b', new Set([1,2,3,4])],
]);
let p1 = reactive(map);
effect(() => {
for (const [key, value] of p1.entries()) {
console.log(value.size, 'value.size');
}
});

原因在于,该返回值有next
方法,有迭代器协议
,但是没有可迭代协议
。迭代器协议就是next方法
,而可迭代协议是Symbol.iterator
方法(数组、set、map都部署了)。
之前写法中返回了next方法,但是没有返回Symbol.iterator方法。
return {
+ next () { // 有next
// itr.next()的结果是['a', 1]和['b', 2]
const { value, done} = itr.next()
return {
// 非undefined则进行包裹
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done
}
}
+ Symbol.iterator() {} // 还应该有这个
}
修改如下:
return {
// 迭代器协议
next () {
// itr.next()的结果是['a', 1]和['b', 2]
const { value, done} = itr.next()
return {
// 非undefined则进行包裹
value: value ? [wrap(value[0]), wrap(value[1])] : value,
done
}
},
// 可迭代协议
+ [Symbol.iterator] () {
+ return this // this返回的是代理对象
+ }
}
备注:在set中,这两个是不等价的,如下图所示

5.3 处理values和keys方法
代理values方法,如下增加valueMethod
方法,
function valueMethod () {
const target = this.raw
+ const itr = target.values()
const wrap = (val) => {
return typeof val === 'object' && val !== null ? reactive(val) : val
}
// 建立响应依赖联系
track(target, ITERATE_KEY)
return {
// 迭代器协议
next () {
const { value, done} = itr.next()
return {
// 非undefined则进行包裹
+ value: wrap(value), // // value的值是1 2
done
}
},
// 可迭代协议
[Symbol.iterator] () {
return this
}
}
}
拦截keys
方法
function keyMethod () {
const target = this.raw
+ const itr = target.keys()
const wrap = (val) => {
return typeof val === 'object' && val !== null ? reactive(val) : val
}
// 建立响应依赖联系
track(target, ITERATE_KEY)
return {
// 迭代器协议
next () {
// itr.next()的结果是['a', 1]和['b', 2]
const { value, done} = itr.next()
return {
// 非undefined则进行包裹
+ value: wrap(value),
done
}
},
// 可迭代协议
[Symbol.iterator] () {
return this
}
}
}
修改拦截对象:
const mutableInstrumentations = {
……
[Symbol.iterator]: iterationMethod,
+ entries: iterationMethod,
+ values: valueMethod,
+ keys: keyMethod
};
测试数据
let map = new Map([
['a', 1],
['b', 2],
]);
let p1 = reactive(map);
effect(() => {
for (const value of p1.values()) {
console.log(value, 'value');
}
});
effect(() => {
for (const key of p1.keys()) {
console.log(key, 'key');
}
});
效果:

5.4 解决keys存在的问题
目前来看,修改
map的值也会触发keys
对应的副作用函数执行,但是key的数量是没有增加或者减少的,不应该触发

解决方式是:
keys拦截用新的key
+const MAP_KEY_ITERATOR_KEY = Symbol()
function keyMethod () {
const target = this.raw
const itr = target.keys()
const wrap = (val) => {
return typeof val === 'object' && val !== null ? reactive(val) : val
}
// 建立响应依赖联系
+ track(target, MAP_KEY_ITERATOR_KEY)
return {
// 迭代器协议
next () {
// itr.next()的结果是['a', 1]和['b', 2]
const { value, done} = itr.next()
return {
// 非undefined则进行包裹
value: wrap(value),
done
}
},
// 可迭代协议
[Symbol.iterator] () {
return this
}
}
}
只有新增和删除,执行这个key对应的副作用函数,这样当SET
操作时不会触发keys拦截
对应的副作用函数啦
function trigger(target, key, type, newVal) {
……
// map的keys拦截,只有add和delete时才能触发,走这里
+ if ((type === "ADD" || type === "DELETE") && (Object.prototype.toString.call(target) +=== '[object Map]')) {
+ // 取得与ItERATE_KEY关联的副作用函数
+ const iterateEffects = depsMap.get(MAP_KEY_ITERATOR_KEY);
+ // 将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
+ iterateEffects &&
+ iterateEffects.forEach((effectFn) => {
+ if (effectFn !== activeEffect) {
+ effectsToRun.add(effectFn);
+ }
+ });
+ }
// 如果是set类型则触发这里`ITERATE_KEY`
if (type === "ADD" || type === "DELETE" || (type === 'SET' && Object.prototype.toString.call(target) === '[object Map]')) {
// 取得与ItERATE_KEY关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY);
// 将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
……
}
请看源码
// packages/reactivity/src/collectionHandlers.ts
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean,
) {
return function (
this: IterableCollections,
...args: unknown[]
): Iterable<unknown> & Iterator<unknown> {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
// 在这里区分entries values kyes方法
+ const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
+ const isKeyOnly = method === 'keys' && targetIsMap
const innerIterator = target[method](...args)
// 包装方法
+ const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly &&
track(
rawTarget,
TrackOpTypes.ITERATE,
+ isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY, // 如果是keys方法,则执行MAP_KEY_ITERATE_KEY的key
)
// return a wrapped iterator which returns observed versions of the
// values emitted from the real iterator
return {
// iterator protocol
next() {
const { value, done } = innerIterator.next()
return done
? { value, done }
: {
+ value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), // 区分(entries)和(values、keys)
done,
}
},
// iterable protocol
+ [Symbol.iterator]() { // 封装的可迭代协议
return this
},
}
}
}