引言
在探究 Vue 响应式原理之前,笔者想先弄懂 Object.defineProperty 和 Proxy,毕竟它们是 Vue3 和 Vue2 响应式的一大区别。初学 JS 时,笔者对这些概念一知半解,现在该弄明白了
对象属性的分类
JS 对象属性有 数据属性(Data Property) 和 访问器属性(Accessor Property) 两种
- 数据属性:就是我们平常最常见的那种,有一个具体的值
- 访问器属性:没有固定的值,而是通过函数(getter / setter)动态计算
属性描述符
对象属性具有 属性描述符,之所以对象属性会有这两类,归根结底是属性描述符造成的。属性描述符有两种,数据描述符 和 访问器描述符。关于这些,MDN 中有较为详尽的描述:Object.defineProperty() - JavaScript | MDN。笔者将 MDN 的主要部分整理如下:
- 数据描述符:是一个具有可写或不可写值的属性
- 访问器描述符:属性值不是直接存储,而是通过 getter / setter 函数动态计算或设置
描述符可以通过 Object.defineProperty 的第三个参数设置,也可以通过 Object.defineProperties 批量设置
描述符都是对象,只能是两种类型之一,不能同时为两者。它们共享以下可选键:
-
configurable默认值为
false当设置为
false时:- 该属性的类型不能在数据属性和访问器属性之间更改
- 该属性不可被删除
- 其描述符的其他属性也不能被更改(但是,如果它是一个可写的数据描述符,则
value可以被更改,writable可以更改为false)。
-
enumerable默认值为
false为
false时,该属性不可枚举,用 for...in、Object.keys 是遍历不出来的。(关于可枚举与可迭代,可以查看本篇的 可枚举与可迭代)
此外,对于 数据描述符,有以下可选键值:
-
value默认值为
undefined与属性相关联的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)
-
writable默认值为
false如果与属性相关联的值可以使用 赋值运算符 更改,则为
true
访问器描述符还具有以下可选键值:
-
get默认值为undefined用作属性 getter 的函数。当访问该属性时,将不带参地调用此函数,并将
this设置为通过该属性访问的对象。返回值将被用作该属性的值 -
set默认值为
undefined用作属性 setter 的函数。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将
this设置为通过该属性分配的对象
如果描述符没有 value、writable、get 和 set 键中的任何一个,它将被视为数据描述符。如果描述符同时具有 [value 或 writable] 和 [get 或 set] 键,则会抛出异常
它们是可以转换的,通过提供不同类型的描述符,可以在数据属性和访问器属性之间切换。例如,如果新描述符是数据描述符(带有 value 或 writable),则原始描述符的 get 和 set 属性都将被删除
关于属性访问器的行为
// 读取
obj.f // get f
// 写入
obj.f = 's' // set f
// 删除
delete obj.f
可以发现:
- 单纯的读取时,只有 get 被触发了
- 写入时,只有 set 属性被触发了。笔者一开始还以为会先触发 get 再触发 set,结果不是
笔者注:这涉及到 JS 的赋值机制。可以把赋值式等号左边的叫左值,右边的叫右值。左值提供槽位,右值提供数据。在执行赋值时,只需要知道左值那个槽位,直接覆盖,而不需要知道它原来的值,所以不会触发 getter
- 删除时,谁也不触发,只是会检查
configurable属性
实践
我们可以打印一下试试,由于省略的选项默认值是 false,笔者就直接不写了。以下操作均在浏览器控制台进行
const obj = {}
let _name = '7'
Object.defineProperties(obj, {
a: {
configurable: true,
writable: true,
value: 'a',
enumerable: true
},
// 不可配置,但可枚举
b: {
writable: true,
value: 'b',
enumerable: true
},
// 不可配置,不可枚举,不可写
c: {
value: 'c'
},
// 可配置,可枚举,不可写
d: {
configurable: true,
value: 'd',
enumerable: true
},
// 访问器属性,可枚举
e: {
get() {
console.log('get e')
},
set() {
console.log('set e')
},
configurable: true,
enumerable: true
},
// 访问器属性,不可配置
f: {
get: () => console.log('get f'),
set: () => console.log('set f'),
enumerable: true
},
// 访问器属性,可配置,不可枚举
g: {
get() {
console.log('get name')
return _name
},
set(v) {
console.log('set name', v)
_name = v
},
configurable: true
}
})
我们来删除不可配置的属性,发现返回 false,删除失败
delete c // false
来遍历这个对象的键,发现不可枚举的遍历不出来
for (key in obj) console.log(key) // a, b, d, e, f
来试着写入不可写入的属性,发现写入不成功
obj.c = 'cc'
console.log(obj.c) // 'c'
来调用一下访问器属性
obj.f = '4' // set f
// 此处调用了 setter,但没有改变任何数据。这与 configurable 无关,是它的 setter 本来就没做什么造成的
obj.e = '5' // 似乎没有奇怪的事情发生
obj.g = '2' // set name, '2'。此后 _name = '2',g 返回 '2'
obj // 输出里面有一个 g: (...),点开会调用 getter,然后打印 'get name',变为 g: '2'
delete obj.g // true,因为 g 可配置
可枚举与可迭代
可枚举(Enumerable)
当我们定义一个对象的属性时,可以通过设置 enumerable 属性描述符来控制该属性是否可枚举。默认情况下,通过字面量或直接赋值创建的属性都是可枚举的
可以使用 propertyIsEnumerable 方法来判断一个属性是否可枚举,或者使用 Object.getOwnPropertyDescriptor 获取属性描述符查看 enumerable 的值
有一些操作与属性的可枚举性有关:
- for...in:遍历对象自身和继承到的可枚举字符串属性
- Object.keys() :返回对象自身的可枚举字符串属性的数组
- JSON.stringify() :只序列化对象自身的可枚举属性
- Object.assign() :只复制对象自身的可枚举属性
可迭代(Iterable)
可迭代是指一个对象或者它原型链上的某个对象有一个 Symbol.iterator 属性,该属性是一个函数,返回一个迭代器对象,这称为可迭代协议。迭代器对象有一个 next 方法,每次调用返回一个包含 value(当前迭代的值) 和 done(布尔值,表示迭代是否结束) 属性的对象,这称为迭代器协议。迭代器涉及的知识太多,笔者推荐一篇好文:Js 中迭代器、生成器详解!
JS 中有一些常见的可迭代对象,如 Array、String、Map、Set、arguments 对象、NodeList 对象 等
笔者注:可能有人会觉得 arguments 对象和 NodeList 对象是数组,其实不是的,它们是类数组对象,有数组的某些特性(arguments 和 NodeList 都有 length 属性,但 arguments 不能被 forEach 遍历,需要转换成 Array 才行;NodeList 因为原型链上自己实现了 forEach,就可以被 forEach 遍历了),但本质上不是数组
有一些操作与对象的可迭代性有关:
- for...of:用于遍历可迭代对象
- 展开语法(...) :可以将可迭代对象展开
- Array.from() :可以将可迭代对象或类数组转换为数组
- Map、Set、WeakMap、WeakSet 的构造函数:可以接受可迭代对象
总结:可枚举 影响属性遍历;可迭代 影响对象本身迭代能力
笔者有点累了,关于 Proxy 的知识明日再补充