此方法可以直接在对象上定义一个新属性,也可以修改对象的现有属性。
语法:Object.defineProperty(obj, prop, descriptor)
该方法的核心在于属性描述符(descriptor 参数),存在两种写法,分别为数据描述符与存取描述符
这两种描述符都是对象。它们共享以下键值:
configurable 表示该属性的描述符能否被改变以及该属性能否从对象上删除,默认为 false
enumerable 表示该属性是否会出现在对象的枚举属性中,默认为 false
数据描述符还具有以下键值:
value 表示该属性的值,默认为 undefined
writable 表示该属性是否可写,默认为 false
存取描述符还具有以下键值:
两种描述符不可同时配置,如果一个描述符同时拥有 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 是一个类,可以为对象创建一个代理对象,从而实现对基本操作的拦截和自定义。
语法:const p = new Proxy(target, handler)
该方法的核心在于拦截器对象,其包含许多的函数属性,用于定义代理行为。
Proxy 可以拦截对象的 14 种操作,不过我们一般拦截的只有读写操作,语法如下:
const hander = {
get(target, property, receiver) {},
set(target, property, value, receiver) {},
}
用一个简单的示例展示其用法:
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
},
set(target, key, value) {
target[key] = value
return true
},
}
// 对象与代理的映射表,weakMap是为了不影响垃圾回收
const weakMap = new WeakMap()
const reactive = (obj) => {
// 已经包装过了,返回现成的代理
if (weakMap.has(obj)) return weakMap.get(obj)
const p = new Proxy(obj, hander)
weakMap.set(obj, p)
return p
}
const obj = reactive({
a: {
b: 1,
},
})
obj.a.b
// a属性被访问
// b属性被访问
Obejct.defineProperty 只能通过设置原型,改写数组方法来实现响应式,较为复杂,且无法实现通过数组索引的访问
// 设置响应式数组的原型
const arrayProto = Object.create(Array.prototype)
// 要被改写的7个数组方法
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
for (const methodName of methods) {
// 备份原来的方法
const original = [][methodName]
// 定义新的方法
arrayProto[methodName] = function (...args) {
// 恢复原来的功能
const result = original.apply(this, args)
// 数组可能插入新项,也需要变为observe
let inserted = []
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
// splice格式是splice(下标,数量,插入的新项)
inserted = args.slice(2)
break
}
// 将新插入的项也变为响应式
for (const newItem of inserted) {
observe(newItem)
}
console.log(`调用数组的${methodName}方法`)
return result
}
}
const def = (obj, prop, val = obj[prop]) => {……}
const observe = (obj) => {
// 如果是数组,改变原型
if (Array.isArray(obj)) Object.setPrototypeOf(obj, arrayProto)
else
for (const prop of Object.keys(obj)) {
def(obj, prop)
}
return obj
}
const obj = observe({
a: [0, 1, 2],
})
obj.a.push({ b: 3 })
// a属性被访问
// 调用数组的push方法
obj.a[3].b
// a属性被访问
// b属性被访问
而 Proxy 能直接拦截对数组的所有操作,包括数字索引的访问
const hander = {……}
// 对象与代理的映射表,weakMap是为了不影响垃圾回收
const weakMap = new WeakMap()
const reactive = (obj) => {……}
const obj = reactive({
a: [1, 2, 3],
})
obj.a[0] = 2
// a属性被访问
// 0属性被修改为2
obj.a.push(4)
// push属性被访问
// length属性被访问
// 3属性被修改为4
// length属性被修改为4