标题同样出入某大佬入职鹅厂的面试题:谈谈 Vue 双向绑定的原理,引申出的 Proxy 和 Object.defineProperty 的区别。
代理(Proxy)是一种可以拦截并改变底层 JavaScript 引擎操作的包装器,在新语言中通过它暴露内部运作的对象。
Proxy
Proxy 主要用于改变对象的默认访问行为,实际上是在访问对象之前增加一层拦截,在任何对对象的访问行为都会通过这层拦截。在这层拦截中,我们可以增加自定义的行为。
基本语法如下:
/*
* target: 目标对象
* handler: 配置对象,用来定义拦截的行为
* proxy: Proxy构造器的实例
*/
var proxy = new Proxy(target,handler)
复制代码
基本用法
看个简单例子:
// 目标对象
var target = {
num:1
}
// 自定义访问拦截器
var handler = {
// receiver: 操作发生的对象,通常是代理
get:function(target,prop,receiver){
console.log(target,prop,receiver)
return target[prop]*2
},
set:function(trapTarget,key,value,receiver){
console.log(trapTarget.hasOwnProperty(key),isNaN(value))
if(!trapTarget.hasOwnProperty(key)){
if(typeof value !== 'number'){
throw new Error('入参必须为数字')
}
return Reflect.set(trapTarget,key,value,receiver)
}
}
}
// 创建target的代理实例dobuleTarget
var dobuleTarget = new Proxy(target,handler)
console.log(dobuleTarget.num) // 2
dobuleTarget.count = 2
// 代理对象新增属性,目标对象也跟着新增
console.log(dobuleTarget) // {num: 1, count: 2}
console.log(target) // {num: 1, count: 2}
// 目标对象新增属性,Proxy能监听到
target.c = 2
console.log(dobuleTarget.c) // 4 能监听到target新增的属性
复制代码
例子里,我们通过 Proxy 构造器创建了 target 的代理 dobuleTarget,即是代理了整个 target 对象,此时通过对 dobuleTarget 属性的访问都会转发到 target 身上,并且针对访问的行为配置了自定义 handler 对象。因此任何通过 dobuleTarget 访问 target 对象的属性,都会执行 handler 对象自定义的拦截操作。
这里面专业的描述是:
代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些操作被拦截后会触发响应特定操作的陷阱函数。例子里的陷阱函数就是 get 函数。
陷阱函数汇总
总结下 Proxy 的陷阱函数:
陷阱函数 : 覆写的特性
get: 读取一个值
set:写入一个值
has:in 操作符
deleteProperty:delete 操作符
getPrototypeOf:Object.getPrototypeOf()
setPrototypeOf:Object.setPrototypeOf()
isExtensible:Object.isExtensible()
preventExtensions:Object.preventExtensions()
getOwnPropertyDescriptor:Object.getOwnPropertyDescriptor()
defineProperty:Object.defineProperty
ownKeys:Object.keys()、Object.getOwnPropertyNames()和 Object.getOwnPropertySymbols()
apply:调用一个函数
construct:用 new 调用一个函数
陷阱函数应用
隐藏私有属性,以及不允许删除
var obj = {
// 以"_"下划线开头的为私有属性
_type:'obj',
name:'hello world'
}
var handler = {
// 判断的是hasProperty,不是hasOwnProperty,拦截的是in操作符
has:function(trapTarget,prop){
if(prop[0]=== '_'){
return false
}
return prop in trapTarget
},
// 拦截的是delete操作符
deleteProperty:function(trapTarget,prop){
if(prop[0]=== '_'){
throw new Error('私有属性不能删除')
}
return true
}
}
var proxy = new Proxy(obj,handler)
'_type' in proxy // false
delete proxy._type // 报错:私有属性不能删除
复制代码
Proxy 递归代理
Proxy 只代理对象的外层属性。例子如下:
var target = {
a:1,
b:{
c:2,
d:{e:3}
}
}
var handler = {
get:function(trapTarget,prop,receiver){
console.log('触发get:',prop)
return Reflect.get(trapTarget,prop)
},
set:function(trapTarget,key,value,receiver){
console.log('触发set:',key,value)
return Reflect.set(trapTarget,key,value,receiver)
}
}
var proxy = new Proxy(target,handler)
proxy.b.d.e = 4
// 输出 触发get:b , 由此可见Proxy仅代理了对象外层属性。
复制代码
如何解决呢?递归设置代理
var target = {
a:1,
b:{
c:2,
d:{e:3}
}
}
var handler = {
get:function(trapTarget,prop,receiver){
var val = Reflect.get(trapTarget,prop)
console.log('get',prop)
if(val !== null && typeof val==='object'){
return new Proxy(val,handler) // 代理内层
}
return Reflect.get(trapTarget,prop)
},
set:function(trapTarget,key,value,receiver){
console.log('触发set:',key,value)
return Reflect.set(trapTarget,key,value,receiver)
}
}
var proxy = new Proxy(target,handler)
proxy.b.d.e
// 输出: 均被代理
// get b
// get d
// get e
复制代码
从递归代理可以看出,如果对象内部要全部递归代理,Proxy 可以只在调用时递归设置代理。
Object.defineProperty
Object.defineProperty()直接在对象上定义新属性,或修改对象上的现有属性,然后返回该对象。
语法如下:
/*
* obj: 要在其上定义属性的对象
* prop: 要定义或修改的属性的名称或Symbol
* descriptor: 定义或修改的属性的描述符
*/
Object.defineProperty(obj, prop, descriptor)
复制代码
描述符
对象里存在的描述符有两种形式:数据描述符和存取描述符,一个描述符只能是这两者中的一个,不能同时是两者,且两种描述符都是对象。
数据描述符:有值的属性,该值是否可写。
存取描述符:由 getter 和 setter 函数所描述的属性。
描述符共享以下属性:
configurable(默认 false)
是否可配置,为 true 时,属性描述符才能够被改变,同时属性可以从对应对象上被删除。
enumerable(默认 false)
是否可枚举,为 true 时,属性才会出现在对象的枚举属性中
数据描述符
数据描述符可选键值:
value(默认 undefined)
属性对应的值,可以是任何有效的 JavaScript 值。
writable(默认 false)
键值为 true 时,value 才能被赋值运算符改变。
存取描述符可选键值:
get(默认 undefined)
属性的 getter()函数,当访问该属性时,会调用此函数。返回的返回值会被用作属性的值。
set(默认 undefined)
属性的 setter()函数,当属性被修改时,会调用此函数。
基础用法
var obj = {}
Object.defineProperty(obj,'name',{
value:'张三'
})
obj.name // '张三'
obj.name = '李四' // 给obj.name赋新值
console.log(obj.name) // 输出:张三 ,值还是没有改变,因为默认不可写
// 以上定义等同于
Object.defineProperty(obj,'name',{
value:'张三',
writable:false,
configurable: false,
enumerable: false
})
复制代码
Object.defineProperty 只能代理对象上的某个属性,因此存在对内部属性进行代理的时候,只能一次性递归完成对所有属性的代理。
自定义 setter 和 getter
function Archiver() {
var log = null;
var archive = [];
Object.defineProperty(this, 'log', {
get() {
console.log('get log!');
return log;
},
set(value) {
log = value;
archive.push({ val: log });
}
});
this.getArchive = function() { return archive; };
}
var arc = new Archiver();
arc.log; // 'get log!'
arc.log = 'log1';
arc.log = 'log2';
arc.getArchive(); // [{ val: 'log1' }, { val: 'log2'}]
复制代码
总结
Proxy 是对整个对象的代理,而 Object.defineProperty 只能代理某个属性。
对象上新增属性,Proxy 可以监听到,Object.defineProperty 不能。
数组新增修改,Proxy 可以监听到,Object.defineProperty 不能。
若对象内部属性要全部递归代理,Proxy 可以只在调用的时候递归,而 Object.definePropery 需要一次完成所有递归,性能比 Proxy 差。
Proxy 不兼容 IE,Object.defineProperty 不兼容 IE8 及以下
Proxy 使用上比 Object.defineProperty 方便多。
评论