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

4,300 阅读5分钟

这是我参与更文挑战的第14天,活动详情查看:更文挑战

[重学JavaScript系列文章连载中...]

标题同样出入某大佬入职鹅厂的面试题:谈谈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写入一个值
hasin操作符
deletePropertyObject.getPrototypeOf()
getPrototypeOfObject.getPrototypeOf()
setPrototypeOfObject.setPrototypeOf()
isExtensibleObject.isExtensible()
preventExtensionsObject.preventExtensions()
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()
definePropertyObject.defineProperty
ownKeysObject.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)

描述符

对象里存在的描述符有两种形式:数据描述符和存取描述符,一个描述符只能是这两者中的一个,不能同时是两者,且两种描述符都是对象。

数据描述符:有值的属性,该值是否可写。

存取描述符:由gettersetter函数所描述的属性。

描述符共享以下属性:

  1. configurable(默认false) 是否可配置,为true时,属性描述符才能够被改变,同时属性可以从对应对象上被删除。

  2. enumerable(默认false) 是否可枚举,为true时,属性才会出现在对象的枚举属性中

数据描述符

数据描述符可选键值:

  1. value(默认undefined) 属性对应的值,可以是任何有效的JavaScript值。
  2. writable(默认false) 键值为true时,value才能被赋值运算符改变。

存取描述符可选键值:

  1. get(默认undefined) 属性的getter()函数,当访问该属性时,会调用此函数。返回的返回值会被用作属性的值。

  2. 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方便多。