标题同样出入某大佬入职鹅厂的面试题:谈谈 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的代理实例dobuleTargetvar 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 = 2console.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 // falsedelete 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 方便多。
评论