写点什么

重学 JS | Proxy 与 Object.defineProperty 的用法与区别

用户头像
梁龙先森
关注
发布于: 2021 年 01 月 17 日
重学JS | Proxy与Object.defineProperty的用法与区别

标题同样出入某大佬入职鹅厂的面试题:谈谈 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'}]
复制代码

总结

  1. Proxy 是对整个对象的代理,而 Object.defineProperty 只能代理某个属性。

  2. 对象上新增属性,Proxy 可以监听到,Object.defineProperty 不能。

  3. 数组新增修改,Proxy 可以监听到,Object.defineProperty 不能。

  4. 若对象内部属性要全部递归代理,Proxy 可以只在调用的时候递归,而 Object.definePropery 需要一次完成所有递归,性能比 Proxy 差。

  5. Proxy 不兼容 IE,Object.defineProperty 不兼容 IE8 及以下

  6. Proxy 使用上比 Object.defineProperty 方便多。


发布于: 2021 年 01 月 17 日阅读数: 42
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
重学JS | Proxy与Object.defineProperty的用法与区别