「重温JS」- Object.defineProperty与Proxy总结

85 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

Object.definePropertyproxy都可以用来劫持对于对象的操作,也是我们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'。 它的configureableenumberablewriteable属性全部默认为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函数,读取属性值时调用,默认为undefined
  • set 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:18configurable: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

总的来说 proxyObject.defineProperty有以下区别:

  • Proxy代理整个对象,Object.defineProperty只代理对象上的某个属性。
  • vue中,Proxy在调用时递归,Object.defineProperty在一开始就全部递归,Proxy性能优于Object.defineProperty
  • 对象上定义新属性,Proxy可以监听到,Object.defineProperty监听不到。
  • 数组新增删除修改时,Proxy可以监听到, Object.defineproperty监听不到
  • Proxy不兼容IE,Object.defineproperty不兼容IE8及以下