携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
Object.defineProperty与proxy都可以用来劫持对于对象的操作,也是我们Vue中实现数据响应式的核心。
Object.defineProperty
MDN的官方解释是:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
也就是说,我们可以为 对象上的key配置一些特殊属性来描述该key,用来表示我们能对它做什么,不能做什么,以及如何做。这些特殊属性可以分为: 数据属性 和 访问器属性
数据属性:
configurable:
-
- 表示是否可以通过
delete删除并重新定义 - 是否可以修改它的特性
- 默认为
true
- 表示是否可以通过
enumberable:
-
- 可否通过
for..in遍历到 - 默认为
true
- 可否通过
writeable:
-
- 能否被修改
- 默认为
true
value:
-
- 就是该属性的值
- 默认为
undefined
比如:我们使用字面量创建一个对象:
let person = {
name: "Luffy"
}
此时,我们创建了一个名为'name'的属性,并将它的数据属性value设置为了'Luffy'。 它的configureable、enumberable、writeable属性全部默认为true
若想修改对象的默认特性,就需要使用Object.defineProperty(),尝试对'name'属性进行以下修改:
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Luffy"
});
console.log(person.name); // "Luffy"
person.name = "Zero";
console.log(person.name); // "Luffy"
将'name'属性配置为 writeable:false,表示不可修改。非严格模式下,赋值操作会被忽略;严格模式下会抛出错误
访问器属性:
访问器属性不包含数据值。它们包含一个获取(getter)函 数和一个设置(setter)函数。
当我们在读取属性值时,会触发getter函数,getter函数返回什么我们就能读到什么。
当我们设置属性值时,就会触发setter函数,会将新设置的值作为参数传入,由setter函数来决定对数据做出什么修改
get: getter函数,读取属性值时调用,默认为undefinedset: setter函数,设置属性值时调用,默认为undefined
访问器属性也必须使用Object.defineproperty定义。
代码示例:
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year", {
get() { return this.year_; },
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
console.log(book.year) // 触发getter -> 2017
book.year = 2018; //触发setter -> 执行setter函数中的操作
console.log(book.edition); // 2
定义多个属性
可以使用Object.defineProperties()来为对象同时定义多个属性
该方法接收两个参数:
- 要添加或修改属性的对象
- 描述符对象,里面的属性要与添加或修改的属性一一对应
let obj = {}
Object.defineProperties(obj, {
name:{
get(){
return 'Zero'
}
},
age:{
value:18,
configurable:false,
writable:false,
enumable:false
},
title:{
get(){...},
set(newV){...}
}
})
获取属性的特性:
使用Object.getOwnPropertyDescriptor()可以获取到指定属性的属性描述符
该方法接收两个参数:
- 对象名
- 键名
返回值是一个对象,里面包含key所有的属性描述
以上面的obj为例:
const descriptor = Object.getOwnPropertyDescriptor(obj, 'age')
console.log(descriptor)
可以看到,Object.getOwnPropertyDescriptor()会返回一个对象,里面包含关于这个key的属性描述符
应用
- Vue2.x响应式处理,遍历options中传入的data, 为他们分别设置getter和setter,模板编译的过程中,会触发getter,从而进行依赖收集。重新设置值,触发setter,执行依赖的update方法,达到更新界面的效果
a==1&&a==2&&a==3同时成立
//通过劫持对a的访问,就可以达到 a==1&&a==2&&a==3的效果
let _a = 1
Object.defineProperty(window, 'a', {
get(){
return _a++
}
})
if(a==1&&a==2&&a==3){
console.log(123)
}
proxy
ES6新增了代理对象proxy。也就是说,我们可以给目标对象定义一个关联的 代理对象。在对目 标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作 加以控制
创建代理对象
可以通过 new Proxy来创建一个代理对象,外界所有对原对象的访问都要先经过代理对象。因此也就提供了一种机制,使我们可以对外界对于原对象的访问进行拦截,进而达到过滤和改写的目的。它接收两个参数:
- 原对象target
- 处理对象,在里面定义捕获器,定义了执行各种操作时代理对象的行为,用于实现对原对象的拦截行为。如果传入一个空对象,就会返回一个空的代理对象,不会劫持对于target的操作
const target = {
id:'target'
}
const proxy = new Proxy(target, {})
// 给目标属性赋值会反映在两个对象上 // 因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(pro3y.id); // foo
// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(pro3y.id); // bar
// hasOwnProperty()方法在两个地方 // 都会应用到目标对象
console.log(target.hasOwnProperty('id')); //true
console.log(proxy.hasOwnProperty('id')); //true
// Proxy.prototype是undefined
// 因此不能使用instanceof操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype
使用捕获器
捕获器接收3个参数: target:原对象、property:属性、receiver:代理对象,可选,用于绑定this
捕获器可捕获以下行为:
get(target, property, receiver) // 读取属性
get(target, property,value, receiver) // 设置属性
has(target, propKey) // 拦截propKey in target 的操作, 返回一个布尔值
deleteProperty(target,propKey) // 拦截delete 操作, 返回一个布尔值
enumerate(target) // 拦截 for...in 操作, 返回一个遍历器
hasOwn(target,propKey) // 拦截proxy.hasOwnProperty(propKey)操作,返回一个布尔值
ownKeys(target) //拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnProeprtyNames(proxy) 、 Object.keys(proxy)
getPropertyOf(target) // 拦截Object.getPropertyOf(proxy)
setPropertyOf(target) // 拦截Object.setPropertyOf(proxy)
apply(target, ctx, args) // 拦截函数的调用 call/apply
constructor(target) // 拦截new 调用
proxy代理对象
let target = {
name:'Luffy',
age:18
}
let proxy = new Proxy(target, {
get(target, property, receiver){
console.log(`get ${property}`)
return Reflect.get(...arguments)
},
set(target,property,newV, receiver){
console.log(`set ${property}`)
return Reflect.set(...arguments)
}
})
proxy.age = 19
// set age
proxy.age++
//get age
//set age
target.age = 28
//不会触发set
console.log(proxy.age)
//get age
// 28
注意:要想Proxy起效果,必须操作代理对象,而不是操作原对象
Proxy递归代理嵌套对象
let target={a:1,b:{c:2}};
let handler={
get(target,property, receiver){
console.log(`get -- ${property}`)
const v = Reflect.get(...arguments);
if(v !== null && typeof v === 'object'){
return new Proxy(v,handler);//递归代理
}else{
return v; // 返回obj[prop]
}
},
set(obj,property,value){
console.log(`set -- ${property}`)
return Reflect.set(...arguments);//设置成功返回true
}
};
let proxy=new Proxy(target,handler);
proxy.a//会触发get方法
proxy.b.c//会先触发get方法获取proxy.b,然后触发返回的新代理对象的.c的get。
proxy代理数组
参数与代理对象时一致, 第二个参数表示下标
let target = [1,2,3,4,5]
let proxy = new Proxy(target, {
get(target, index, receiver){
console.log(`get -- ${property}`)
return Reflect.get(...arguments)
},
set(target, index, receiver){
console.log(`set -- ${property}`)
return Reflect.set(...arguments)
}
})
console.log(proxy[0])
//get -- 0
// 1
proxy[0] = 18
//set -- 0
proxy代理函数
apply(target, ctx, args) 方法拦截代理对象作为函数的调用、call、apply的操作
let handler = {
get(target, property, receiver){
return `Hello ${property}`
},
apply(target, ctx, args){
return Reflect.apply(...arguments) * 2
}
}
function sum(num1, num2){
return num1 + num2
}
let proxy = new Proxy(sum, handler)
console.log(proxy.name) // Hello name
proxy(1,3) // 8
proxy.call(null, 1,2) // 6
proxy.apply(null, [1,5]) // 12
上面的代码中,将proxy作为函数执行就会被apply方法拦截。进而可以在回调中进行我们想要的操作
撤销代理
使用new Proxy创建的代理对象,知道页面销毁会一直存在。
Proxy.revocable() 可以用于撤销代理,且该操作是不可逆的,撤销后再次调用代理会抛出TypeError
const target = {
name : 'Luffy'
}
const handler = {
get(){
return 'Zero'
}
}
const {proxy, revoke} = Proxy.revocable(target, handler)
console.log(proxy.name) // 'Zero'
console.log(target.name) // 'Luffy'
revoke()
console.log(proxy.name) // TypeError
撤销之后,再次调用代理对象,会抛出以下错误:
Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
at
总结:
Object.definePrtoerty与Proxy兼容性不同,使用时注意区分。提供给大家一个查兼容性网站:can i use
总的来说 , proxy与Object.defineProperty有以下区别:
- Proxy代理整个对象,Object.defineProperty只代理对象上的某个属性。
- vue中,Proxy在调用时递归,Object.defineProperty在一开始就全部递归,Proxy性能优于Object.defineProperty
- 对象上定义新属性,Proxy可以监听到,Object.defineProperty监听不到。
- 数组新增删除修改时,Proxy可以监听到, Object.defineproperty监听不到
- Proxy不兼容IE,Object.defineproperty不兼容IE8及以下