Object.defineProperty
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
有一个对象,我们希望监听这个对象中的属性被设置或获取的过程可以使用Object.defineProperty 的存储属性描述符来对属性的操作进行监听
const user = {
name: 'Klaus',
age: 23
}
Object.keys(user).forEach(key => {
let value = user[key]
Object.defineProperty(user, key, {
get() {
console.log(`${key}被设置了`)
return value
},
set(v) {
console.log(`${key}被获取了`)
// 虽然是赋值给了基本数据类型
// 但是Object.defineProperty内部会自动将对象上对应的属性值进行修改操作
value = v
}
})
})
// 获取值
console.log(user.name)
console.log(user.age)
user.age = 18
console.log(user.age) // => 18
但是这种形式监听属性的改变是存在缺陷的
- Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的,而是定义对象的属性的属性描述符的
- 如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty是无能为力的
- 使用Object.defineProperty实际上是使用修改了描述符的属性去覆盖原本的同名属性,也就是使用Object.defineProperty进行属性值的改变会修改原对象
Proxy
在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的
也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象)
之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作
我们之后的操作都是直接对Proxy的操作,而不是原有的对象
const user = {
name: 'Klaus',
age: 23
}
// 创建对user对象的代理对象
/*
参数1:需要被代理的对象
参数2:捕获器对象(handler) ---> 对应的回调函数对象
--> 在执行某些操作时候的回调
--> Proxy提供了13种捕获器(trap)
--> 如果不进行任何的设置,会使用默认的捕获器
*/
const proxy = new Proxy(user, {})
// 使用默认的get捕获器
console.log(user.name)
// 使用默认的set捕获器
user.age = 18
console.log(user.age)
捕获器
const user = {
name: 'Klaus',
age: 23
}
const proxy = new Proxy(user, {
// 重写get捕获器
/*
参数1: target 被代理的原对象
参数2: key 被获取的属性key
参数3: receiver:调用的代理对象
*/
get(target, key) {
return target[key]
},
// 重写set捕获器
/*
参数1: target 被代理的原对象
参数2: key 被获取的属性key
参数3: value 新属性值
参数4: receiver:调用的代理对象
*/
set(target, key, value) {
target[key] = value
},
// in操作符 对应的捕获器
has(target, key) {
console.log('执行了in操作符')
return key in target
},
// 删除属性 对应的捕获器
defineProperty(target, key) {
console.log('执行了delete操作')
// 返回值是boolean值 代表对应的属性是否被成功移除
return delete target[key]
}
})
// 所有的操作都需要对代理对象进行操作
console.log(proxy.name)
proxy.age = 18
console.log(proxy.age)
console.log('age' in proxy)
console.log(delete proxy.age)
// 2个特别的捕获器 --- 专门针对于函数对象进行代理操作
function foo() {}
const proxy = new Proxy(foo, {
construct(target, args) {
console.log('执行了new操作')
return new target(...args)
},
// 注意: 只有apply捕获器,没有call捕获器
// 注意: 第二个参数是apply调用的时候传入的this对象
// --- 形参的名称不要和关键字同名,所以建议命名为thisArg
apply(proxy, thisArg, args) {
console.log('执行了apply操作')
return proxy.apply(thisArg, args)
}
})
proxy.apply({})
new proxy()
Reflect
Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射, 映射。
那么这个Reflect有什么用呢?
- 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法
- 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf()
- 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()
- 在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面
- 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适
- 另外还包含一些类似于 in、delete操作符也可以对对象进行操作,这让JS看起来是会有一些奇怪
- 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上
Object和Reflect对象之间的API关系,可以参考MDN文档
基本使用
在使用Proxy的时候,我们需要重写一系列的捕获器,因为要完成原本的功能
所以我们在重写捕获器的时候,依旧需要操作原对象(target)来完成我们对应的功能
但是proxy的本意是使用代理对象来替换原始对象,也就是所有的操作都在代理对象上进行操作,不要去主动操作原对象
此时在语义上就会存在问题
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value
}
所以ES6+中,我们可以使用Reflect对象来替代我们原本对于原对象的操作
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, value) {
Reflect.set(target, key, value)
}
所以,其实我们可以看到,Reflect中的方法其实和Proxy中的捕获器是一一对应的
总结:
-
在
Reflect对象上定义了可以操作对象的静态方法,这些方法和Proxy的捕获器是一一对应的 -
Reflect对象是对原本Object上操作对象方法和操作符的统一和规范
-
Reflect对象上的静态方法基本可以划分为两类:
- 一类是原来存在
Object上的方法,将它转义到了Reflect上,并作了小改动,让方法更加合理, 如defineProperty,getPrototypeOf,apply等 - 另一类是将原来操作符的功能,变成函数行为,如
in ---> has,delete ---> deleteProperty等
- 一类是原来存在
const user = {}
Object.defineProperty(user, 'name', {
value: 'Klaus',
configurable: false,
enumerable: true
})
// 传统写法 --- 静默错误
console.log(user.name = 'Alex') // => Alex
// 使用Reflect --- 会返回一个boolean值, 表示操作是否成功
const isSuccess = Reflect.set(user, 'name', 'Klaus')
console.log(isSuccess) // => false
Receiver
const user = {
_name: 'Klaus',
get name() {
return this._name
},
set name(v) {
this._name = v
}
}
const proxy = new Proxy(user, {
get(target, key) {
console.log('set -----', key)
return Reflect.get(target, key)
},
set(target, key, newV) {
console.log('get ----', key)
Reflect.set(target, key, newV)
}
})
proxy.name = 'Alex'
console.log(proxy.name)
/*
=>
get ---- name
set ----- name
Alex
*/
可以看到get捕获器和set捕获器只被执行了一次,也就是对于属性_name的存取操作并没有被代理对象监听到
这是因为默认情况下,在执行对象的访问器属性的时候,内部的this是原始对象,并不是代理对象
const user = {
_name: 'Klaus',
get name() {
console.log(this === user) // => true
return this._name
},
set name(v) {
this._name = v
}
}
const proxy = new Proxy(user, {
get(target, key) {
// 实际操作等价于 target[key]
return Reflect.get(target, key)
},
set(target, key, newV) {
// 实际操作等价于 target[key] = newV
Reflect.set(target, key, newV)
}
})
proxy.name = 'Alex'
console.log(proxy.name)
为此,Proxy对于set和get捕获器设置了一个特殊的参数Reciver,这个参数可以用来设置在使用对象的访问器属性的时候,其内部this的值
const user = {
_name: 'Klaus',
get name() {
return this._name
},
set name(v) {
this._name = v
}
}
const proxy = new Proxy(user, {
get(target, key) {
// 执行user.name的get访问器的时候
// 内部的this指向的是 { _name: 'Alex' }
return Reflect.get(target, key, {
_name: 'Alex'
})
},
set(target, key, newV) {
Reflect.set(target, key, newV)
}
})
console.log(proxy.name) // => Alex
所以我们可以使用receiver参数,来对一些私有属性进行监听
const user = {
_name: 'Klaus',
get name() {
return this._name
},
set name(v) {
this._name = v
}
}
const proxy = new Proxy(user, {
get(target, key, receiver) {
console.log(proxy === receiver) // => true
console.log('get ----', key)
return Reflect.get(target, key, receiver)
},
set(target, key, receiver) {
console.log('set ----', key)
Reflect.set(target, key, receiver)
}
})
proxy.name = 'Alex'
console.log(proxy.name)
此时代理对象中的get和set捕获器就被执行了两次,而对于_name这个私有属性的修改也被正常的监听到了
Reflect.construct
function Student(name, age) {
this.name = name
this.age = age
}
function Teacher() {}
// 借用Student构造方法,创建Teacher的实例对象
const teacher = Reflect.construct(Student, ['Klaus', 23], Teacher)
console.log(teacher) // => { name: 'Klaus', age: 23 }
console.log(teacher instanceof Teacher) // => true
console.log(teacher instanceof Student) // => false
上述操作等价于
function Student(name, age) {
this.name = name
this.age = age
}
function Teacher() {}
const teacher = new Student('Klaus', 23)
Object.setPrototypeOf(teacher, Teacher.prototype)
console.log(teacher) // => { name: 'Klaus', age: 23 }
console.log(teacher instanceof Teacher) // => true
console.log(teacher instanceof Student) // => false