Object.defineProperty详解与Vue2响应式原理
来自网络
有次接手一份老代码,配置对象上的某个字段赋值怎么都不生效,console.log 打出来还是旧值,也不报错。找了很久才发现前面有人用 Object.defineProperty 把它定成了 writable: false,非严格模式下这种赋值是静默失败的,连个警告都没有。
Object.defineProperty 平时写业务确实用得少,但它是 JS 对象模型的底层入口。Object.freeze 怎么冻住对象、class 的方法为什么 for...in 遍历不到、Vue 2 的响应式是怎么做的,答案全在属性描述符这套机制里。这篇把四个数据描述符和存取器描述符逐个过一遍,再用它解释一遍 Vue 2 的响应式和它的短板。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 属性描述符解决了什么问题
value、writable、enumerable、configurable各自管什么- 用
defineProperty定义属性时那个「默认全是 false」的坑 - 严格模式和非严格模式下违规操作的不同表现
get/set存取器描述符的用法和它的互斥规则- Vue 2 的响应式为什么必须用它,以及三个绕不过去的短板
Proxy是怎么把这些短板补上的- 配套的几个 API,
getOwnPropertyDescriptor、defineProperties、freeze
# 一、简介
先说兼容性,在 IE8 下 Object.defineProperty 只能在 DOM 对象上使用,尝试在原生的对象上使用会报错。这条限制今天已经没有实际意义了,主流浏览器和 Node 都完整支持它,写它只是为了让你在读老代码里那些奇怪的兼容分支时知道来由。
定义对象可以使用构造函数或字面量的形式。
var obj = new Object; //obj = {}
obj.name = "张三"; //添加描述
obj.say = function(){}; //添加行为
除了以上添加属性的方式,还可以使用 Object.defineProperty 定义新属性或修改原有的属性。
两者的差别不只是写法。用 obj.name = '张三' 这种赋值方式创建的属性,是可写、可枚举、可配置的,也就是一个「什么都能干」的普通属性。而 Object.defineProperty 允许你把这三个开关单独关掉,这才是它存在的理由。
# 二、Object.defineProperty()
# 2.1 定义
Object.defineProperty(obj, prop, descriptor)
参数说明:
obj必需,目标对象prop必需,需定义或修改的属性的名字descriptor必需,目标属性所拥有的特性
返回值是传入函数的对象,即第一个参数 obj。注意它是原地修改,返回的就是那个对象本身,不是副本。
针对属性我们可以设置一些特性,比如是否只读不可以写,是否可以被 for...in 或 Object.keys() 遍历。
给对象的属性添加特性描述,目前提供两种形式,数据描述和存取器描述。这两种形式是互斥的,同一个属性只能选一种。
# 2.2 数据描述
当修改或定义对象的某个属性的时候,给这个属性添加一些特性。
var obj = {
test:"hello"
}
//对象已有的属性添加特性描述
Object.defineProperty(obj,"test",{
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
});
//对象新添加的属性的特性描述
Object.defineProperty(obj,"newKey",{
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
});
数据描述中的属性都是可选的,来看一下设置每一个属性的作用。
# 2.2.1 value
属性对应的值,可以是任意类型的值,默认为 undefined。
var obj = {}
//第一种情况:不设置value属性
Object.defineProperty(obj,"newKey",{
});
console.log( obj.newKey ); //undefined
------------------------------
//第二种情况:设置value属性
Object.defineProperty(obj,"newKey",{
value:"hello"
});
console.log( obj.newKey ); //hello
上面这两段用分隔线隔开了,是因为它们必须分别运行。第一种情况执行完之后 newKey 的 configurable 是 false,同一个 obj 上再调一次 defineProperty 会直接抛错。这个细节后面讲 configurable 的时候还会再遇到一次。
# 2.2.2 writable
属性的值是否可以被重写。设置为 true 可以被重写,设置为 false 不能被重写,默认为 false。
var obj = {}
//第一种情况:writable设置为false,不能重写。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey ); //hello
//第二种情况:writable设置为true,可以重写
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey ); //change value
第一种情况里那次赋值就是我开头踩的坑。非严格模式下它悄无声息地失败,什么都不发生。开了严格模式('use strict')或者在 ES 模块里(模块默认严格模式),同样的代码会抛 TypeError: Cannot assign to read only property。
这个差别在实践中很重要。现在用打包工具的项目源码基本都是 ESM,天生严格模式,所以你反而更容易第一时间看到报错。真正难查的是那些跑在非严格模式下的老脚本。
# 2.2.3 enumerable
此属性是否可以被枚举(使用 for...in 或 Object.keys())。设置为 true 可以被枚举,设置为 false 不能被枚举,默认为 false。
var obj = {}
//第一种情况:enumerable设置为false,不能被枚举。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false
});
//枚举对象的属性
for( var attr in obj ){
console.log( attr );
}
//第二种情况:enumerable设置为true,可以被枚举。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:true
});
//枚举对象的属性
for( var attr in obj ){
console.log( attr ); //newKey
}
不可枚举不等于不存在。enumerable: false 的属性,for...in、Object.keys、JSON.stringify、展开运算符都拿不到它,但 obj.newKey 直接访问是有值的,Object.getOwnPropertyNames(obj) 也能列出来,'newKey' in obj 同样返回 true。
这个特性在语言内部用得很多。Array.prototype.push、Object.prototype.toString 这些内置方法全是不可枚举的,否则你 for...in 一个数组会把所有原型方法都遍历出来。class 里定义的方法默认也是不可枚举的,而 ES5 时代手动挂在 prototype 上的函数是可枚举的,这是两者一个容易被忽略的差别。
# 2.2.4 configurable
是否可以删除目标属性,或是否可以再次修改属性的特性(writable、configurable、enumerable)。设置为 true 可以被删除或可以重新设置特性,设置为 false 不能被删除也不可以重新设置特性,默认为 false。
这个属性起到两个作用:
- 目标属性是否可以使用
delete删除 - 目标属性是否可以再次设置特性
//-----------------测试目标属性是否能被删除------------------------
var obj = {}
//第一种情况:configurable设置为false,不能被删除。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:false
});
//删除属性
delete obj.newKey;
console.log( obj.newKey ); //hello
//第二种情况:configurable设置为true,可以被删除。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:true
});
//删除属性
delete obj.newKey;
console.log( obj.newKey ); //undefined
这两段代码同样要分开跑,原文没说清楚这点。放在一起顺序执行的话,第二次 defineProperty 会因为上一次设了 configurable: false 而直接抛 TypeError: Cannot redefine property,根本走不到 delete 那行。
delete 失败的表现也分模式。非严格模式下 delete obj.newKey 返回 false 然后什么都不发生,严格模式下直接抛 TypeError。