先记住这个答案
从读写上看两种写法没有区别,读属性都会调 getter,赋值都会调 setter。真正的差别在属性特性的默认值:对象字面量里写的 get 和 set,它们的 enumerable 和 configurable 默认都是 true;用 Object.defineProperty 定义时,没写的特性一律按 false 处理。所以后者定义的访问器 Object.keys 看不到,JSON.stringify 不输出,delete 也删不掉。另外 defineProperty 能给一个已经存在的对象补访问器,字面量只能在创建对象时写。
- 两种写法调用 getter、setter 的方式完全一样
- 字面量访问器默认可枚举、可配置
- 描述符省略特性默认
false defineProperty能给已经存在的对象补访问器
同一个访问器,两种写法对比
先看一段对比,a 用字面量写,b 用 defineProperty 写,getter 和 setter 的逻辑一模一样。
const a = {
_n: 1,
get n() { return this._n },
set n(v) { this._n = v }
}
const b = { _n: 1 }
Object.defineProperty(b, 'n', {
get() { return this._n },
set(v) { this._n = v }
// 没写 enumerable 和 configurable,默认都是 false
})
console.log(a.n, b.n)
console.log(Object.keys(a).join(','))
console.log(Object.keys(b).join(','))
console.log(JSON.stringify(b))
console.log(delete b.n, b.n)查看输出与解释
1 1
_n,n
_n
{"_n":1}
false 1a.n 和 b.n 都读到 1,说明访问器本身没差别。差别出在后面三行:b 的 n 不可枚举,Object.keys 和 JSON.stringify 都跳过了它;它又不可配置,delete 返回 false,属性还在。展开运算符 { ...b } 也只会复制 _n。
这就是面试里最想听到的点:不是 defineProperty 更高级,而是它的默认值更保守。想让它和字面量表现一致,要显式写上 enumerable: true, configurable: true。
- 不可配置意味着什么
configurable: false之后,这个属性不能删除,也不能再用defineProperty改成别的样子,比如把访问器换成普通值,或者重新改成可枚举,都会抛TypeError。所以在第三方库里调用defineProperty时,不写configurable等于把这个属性锁死了,后面谁想覆盖都覆盖不了。
什么时候非用 defineProperty 不可
字面量写法只能在创建对象的那一刻用。对象已经存在、而且要拦截它原有的属性时,只能用 defineProperty。
function defineReactive(obj, key) {
let val = obj[key] // 先把原来的值存进闭包
Object.defineProperty(obj, key, {
enumerable: true, // 显式打开,否则 v-for、JSON 序列化都看不到
configurable: true,
get() {
// 这里收集依赖:谁读了这个属性
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// 这里通知依赖更新视图
}
})
}Vue 2 拿到用户传进来的 data 对象后,逐个属性调用类似的函数,把普通属性原地替换成访问器。这件事字面量做不到,因为对象不是它创建的。注意这里 enumerable 被显式写成 true,不然遍历数据时这些属性就消失了。
这种做法的代价也很明显:只能拦截定义时已经存在的属性,后加的属性拦不到,所以 Vue 2 才需要 Vue.set。数组下标其实也能用 defineProperty 拦,但数组可能很长,Vue 2 出于性能考虑没这么做,改成重写 push 和 splice 这些会改动数组的方法。Vue 3 改用 Proxy,拦截的是整个对象,就没有这个限制。
- 字面量写法更适合日常业务
- 业务代码里给对象加一个计算属性,比如
get fullName(),直接写在字面量或 class 里更直观。class 里的get和set定义在原型上,不可枚举,但可配置,所有实例共享一份。
容易答错的地方
- 说两种写法「完全一样」
- 读写行为一样,但特性默认值不同,结果就是遍历、序列化、删除时表现不一样。只答「语法不同、行为一致」会被追问卡住。
- 在描述符里同时写
value和get - 描述符只能是数据描述符(
value和writable)或访问器描述符(get和set)其中一种,混着写会抛TypeError。想让访问器有初始值,就像上面那样把值存在闭包或另一个属性里。 - 只写 getter 就以为属性是只读的「常量」
- 只有
get没有set时,赋值不会生效:非严格模式静默失败,严格模式抛TypeError。但如果configurable是 true,别人照样能用defineProperty把它整个换掉,并不是真正锁死。
面试官还会怎么问?
怎么查看一个属性是用哪种方式定义的?
用 Object.getOwnPropertyDescriptor(obj, key) 看描述符。字面量访问器得到的是 { get, set, enumerable: true, configurable: true },而 defineProperty 没写特性时这两个都是 false。
Object.defineProperties 和它有什么区别?
只是批量版本,一次传一个对象,给多个属性分别写描述符,默认值规则完全一样。
为什么 Vue 3 不再用 defineProperty?
defineProperty 是按属性拦截的,要在初始化时递归遍历所有属性,后加的属性监听不到,数组也只能靠重写方法兜底。Proxy 拦截整个对象的所有操作,新增、删除属性都能感知,还可以在访问到深层对象时才去代理,初始化成本更低。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。