谈: Object.defineProperty 和 Proxy

672 阅读4分钟

前言

接触到这两个概念还是偶然的机会, 因为平常业务开发主要是是基于vue.js,碰到一些问题会忽然僵住,感觉难以入手. 比如:

vue.js的响应式是如何实现的, 为什么它可以做到双向绑定?

对象拦截的话, 如果属性值还是对象呢?

为什么在vue中数组可以检测到改变?

为了解决这些问题, 就引出了Object.defineProperty().

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性, 或者修改一个对象的现有属性,并返回此对象.

简单的说就是, 对象调用这个方法, 可以在对象上加一个拦截器, 可以对对象属性进行修改.

const obj = {}
Object.defineProperty(obj,'name', {
  value: 'aixiao',
  writable: false
})
obj.name = 'other' // 严格模式下会报错
console.log(obj.name) // 'aixiao'

value

对象属性对应的值. 默认为undefined

writable

由Object.defineProperty定义的属性的value,默认是不可修改的. 如需要修改则设置 writable为true. 上文的例子中有表现出这一属性.

enumerable

对象属性的可枚举性,enumable默认为false,只有为true时才可被枚举.

const obj = {
  name: 'aixiao'
}
Object.defineProperty(obj, 'age', {
  value: '18',
  enumerable: false
})
const keys = Object.keys(obj)
console.log(keys); // ["name"]
console.log(obj); // {name: "aixiao", age: "18"}

configurable

configurable为true时,对象属性能从对象上删除,被改变.

const obj = {
  name: 'aixiao'
}
Object.defineProperty(obj, 'age', {
  value: '18',
  configurable: false
})
const keys = Object.keys(obj)
delete obj.age;
console.log(obj); // {name: "aixiao", age: "18"}

get

当访问对象的属性时会调用get,如果没有get那么就是undefined;

const obj = {
  name: 'aixiao'
}
Object.defineProperty(obj, 'age', {
  get() {
    console.log('调用属性了')
    return 18;
  }
})
console.log(obj.age); // 18

set

当修改对象的属性时会调用set,如果没有set那么就是undefined;

const obj = {
  name: 'aixiao',
  age: 18
}
Object.defineProperty(obj, 'newAge', { // 这里用一个新的属性来做数据同步
  get() {
    return this.age;
  },
  set(newVal) {
    this.age = newVal
  }
})
console.log(obj.age); // 18
obj.age = 17
console.log(obj.age); // 17

了解了这些概念之后, 那么回过头来看上面的问题.毋庸置疑, vue2数据的双向绑定就是通过Object.defineProperty()来劫持对象实现的, 对每个对象的每个属性进行遍历.

那如果属性值还是对象就需要深度遍历,直到每个对象(属性)都调用了Object.defineProperty().相应的问题,在使用vue开发过程中,直接通过this.xx去声明并赋值一个属性, 那它肯定不是响应式的,需要通过Vue.set() 这个方法来更新视图.

为什么数组可以直接通过数组方法去操作来达到数据更新呢?

因为 vue把push();pop();shift();unshift();splice();sort();reverse()进行了重写从而实现数组的劫持.像filter();concat();slice()这些都不会改变原数组, 会返回一个新数组.


proxy

接下来看看vue3中使用的proxy, proxy是ES6的一个新特性(这个时间点应该大家都融会贯通了...),也可以用来做到Object.defineProperty()所做的事情,对属性做获取修改操作.

先看一个例子:

const obj = {
  name: 'aixiao',
  age: 18
}
function observe(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('获取属性值')
      return target[key]
    },
    set(target, key, value) {
      console.log('设置属性值')
      target[key] = value
    }
  })
}
const result = observe(obj)
console.log(result.age); // 获取属性值  // 18
result.age = 17 // 设置属性值
console.log(result.age); // 获取属性值 // 17
     

Proxy充当了一个拦截器的角色,在读取/修改对象属性,获取对象属性的时候去拦截对象, 然后此时自定义这些行为.

Proxy有两个参数

target:要兼容(拦截的对象), 可以是对象,数组,函数等等

handler: 它是个对象,里面包含了自定义要拦截对象的行为函数,上面例子中的 get 与 set就是.它会返回一个新的对象proxy, 要触发handler里的函数必须要通过返回值.

handler的方法

handler.get() :读取对象属性操作的函数.

handler.set() :设置对象属性操作的函数.

handler.has() :in操作符会触发的函数.

handler.deleteProperty() :delete 操作符会触发的函数.

handler.ownKeys() :当通过Object.getOwnPropertyNames(), Object.getownPropertySymbols(),Object.keys()方法去操作对象的时候会触发的函数

handler.apply() :当拦截的是一个函数的时候会触发的函数

handler.construct() :使用new操作符的时候触发的函数

handler.getPrototypeOf() :当读取对象prototype的时候会触发的函数

handler.setPrototypeOf() :当设置对象prototype的时候会触发的函数

handler.isExtensible() :当调用Object.isExtensible()判断对象可否添加新书行的时候会触发的函数

handler.preventExtensions() :当调用Object.preventExtensions()设置不可修改属性的时候会触发的函数

handler.getOwnPropertyDescriptor() :当调用Object.getOwnPropertyDescriptor()获取对象description的时候会触发的函数

handler.defineProperty() :当调用Object.defineProperty()的时候会触发的函数

Proxy的这些handler 解决了vue2中setset delete所做的事情, 因为Object.defineProperty无法监听新的属性,所以要用set.相应的无法监听到属性被删除,所以要用set.相应的无法监听到属性被删除,所以要用delete. 那再Proxy中可以通过set监听新增属性, 通过deleteProperty监听到删除 所以setset跟delete就没用了.

总结

这篇文章以我自己使用vue.js踩到的坑为例子,一步步发掘Object.defineProperty(),再到Proxy().用自己的理解诠释了一些问题以及问题的原因.Proxy中handler的方法没有一一举例, 可以在MDN上找到相对完整的讲义.旨在不断学习,加深印象.