Object.defineProperty 用法
此方法可以直接在对象上定义一个新属性,也可以修改对象的现有属性。
语法:Object.defineProperty(obj, prop, descriptor)
- obj:要定义或修改属性的对象
- prop:要定义或修改的属性的名称或 Symbol
- descriptor:该属性的描述符
- 返回值:传入的对象obj
该方法的核心在于属性描述符(descriptor 参数),存在两种写法,分别为数据描述符与存取描述符
这两种描述符都是对象。它们共享以下键值:
-
configurable 表示该属性的描述符能否被改变以及该属性能否从对象上删除,默认为 false
-
enumerable 表示该属性是否会出现在对象的枚举属性中,默认为 false
数据描述符还具有以下键值:
-
value 表示该属性的值,默认为 undefined
-
writable 表示该属性是否可写,默认为 false
存取描述符还具有以下键值:
-
get 表示该属性的取函数,在读取改属性时会执行此函数,函数返回值作为属性的值
-
set 表示该属性的存函数,修改属性值时会调用此函数,并接受一个参数(被赋予的新值)
两种描述符不可同时配置,如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常
用一个简单的示例展示其用法:
let obj = {}
Object.defineProperty(obj, '_num', {
value: 1,
writable: true,
enumerable: false, // 不被枚举
configurable: false,
})
Object.defineProperty(obj, 'num', {
get() {
console.log('num属性被访问')
return this._num
},
set(value) {
console.log('num属性被修改')
this._num = value
},
enumerable: true,
configurable: true,
})
console.log(obj._num) // 1
console.log(obj.num) // num属性被访问 1
console.log(Object.keys(obj)) // ['num'] (_num属性未被枚举)
obj.num = 10 // num属性被修改
delete obj._num // (configurable为false,无法删除)
delete obj.num
console.log(obj.num) // undefined
console.log(obj._num) // 1
Proxy 用法
Proxy 是一个类,可以为对象创建一个代理对象,从而实现对基本操作的拦截和自定义。
语法:const p = new Proxy(target, handler)
- target:要代理的对象
- handler:拦截器对象
- 返回值:新的代理对象
该方法的核心在于拦截器对象,其包含许多的函数属性,用于定义代理行为。
Proxy 可以拦截对象的 14 种操作,不过我们一般拦截的只有读写操作,语法如下:
const hander = {
get(target, property, receiver) {},
set(target, property, value, receiver) {},
}
- target:被代理的原对象
- property:要访问的属性名或 Symbol
- value:要设置的新属性值
- receiver:代理对象
- 返回值:get 返回要获取的属性值,而 set 返回一个布尔值,表示赋值是否成功
用一个简单的示例展示其用法:
const obj = {
_num: 1,
}
const hander = {
get(target, property) {
if (property === 'num') return target['_num']
else return undefined
},
set(target, property, value) {
if (property === 'num') {
console.log('num属性赋值成功')
target['_num'] = value
return true
} else {
console.log(`${property}属性赋值失败`)
return false
}
},
}
const p = new Proxy(obj, hander)
console.log(p._num) // undefined
console.log(p.num) // 1
p.num = 2 // num属性赋值成功
p._num = 3 // _num属性赋值失败
console.log(p.num) // 2
console.log(obj._num) // 2
两者区别
看完了 Obejct.defineProperty 与 Proxy 的用法,接下来结合 Vue 讲解一下它们的区别
属性值的存储位置
在 Vue2 中,是通过闭包来存储属性值的
// 属性值存储在 val 变量中
const def = (obj, prop, val = obj[prop]) => {
Object.defineProperty(obj, prop, {
get() {
console.log(`${prop}属性被访问`)
return val
},
set(value) {
console.log(`${prop}属性被修改为${value}`)
val = value
},
enumerable: true,
configurable: true,
})
}
而在 Vue3 中,属性值一致存储在原对象中
const hander = {
get(target, prop) {
console.log(`${prop}属性被访问`)
return target[prop]
},
set(target, prop, value) {
console.log(`${prop}属性被修改为${value}`)
target[prop] = value
return true
},
}
拦截多个属性
当使用 Obejct.defineProperty 拦截对象的多个属性时,需要遍历对象的所有属性名,依次设置描述符
而使用 Proxy 拦截对象的多个属性时,拦截器会直接作用于所有属性
Obejct.defineProperty
const def = (obj, prop) => {...}
const observe = (obj) => {
// 循环所有属性名,Vue2为了兼容 IE,源码中使用的是 for in
for (const prop of Object.keys(obj)) {
def(obj, prop)
}
return obj
}
const obj = observe({
a: 1,
b: 2,
c: 3,
})
obj.a // a属性被访问
obj.b // b属性被访问
Proxy
const hander = {...}
const reactive = (obj) => {
return new Proxy(obj, hander)
}
const obj = reactive({
a: 1,
b: 2,
c: 3,
})
obj.a // a属性被访问
obj.b // b属性被访问
多层对象
二者在拦截多层对象的操作,也就是深度监听时,都需要递归地操作对象,区别在于 Vue2 在定义时就要完成所有层属性的拦截,而 Vue3 则是到真正使用时,才生成对应的 Proxy 对象。
Vue2
const def = (obj, prop, val = obj[prop]) => {
// 值是对象,递归监听
if (typeof val === 'object') observe(val)
Object.defineProperty(obj, prop, {
get() {
console.log(`${prop}属性被访问`)
return val
},
set(value) {
console.log(`${prop}属性被修改为${value}`)
val = value
},
enumerable: true,
configurable: true,
})
}
const observe = (obj) => {
for (const prop of Object.keys(obj)) {
def(obj, prop)
}
return obj
}
let obj = observe({
a: {
b: 1,
},
})
obj.a.b
// a属性被访问
// b属性被访问
Vue3
const hander = {
get(target, key) {
let val = target[key]
// 值是对象,则进行包装后返回
return typeof val === 'object' ? reactive(val) : val
}