JS Advance --- Proxy 和 Reflect

606 阅读7分钟

Object.defineProperty

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

有一个对象,我们希望监听这个对象中的属性被设置或获取的过程可以使用Object.defineProperty 的存储属性描述符来对属性的操作进行监听

const user = {
  name: 'Klaus',
  age: 23
}

Object.keys(user).forEach(key => {
  let value = user[key]

  Object.defineProperty(user, key, {
    get() {
      console.log(`${key}被设置了`)
      return value
    },

    set(v) {
      console.log(`${key}被获取了`)
      // 虽然是赋值给了基本数据类型
      // 但是Object.defineProperty内部会自动将对象上对应的属性值进行修改操作
      value = v
    }
  })
})

// 获取值
console.log(user.name)
console.log(user.age)

user.age = 18
console.log(user.age) // => 18

但是这种形式监听属性的改变是存在缺陷的

  1. Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的,而是定义对象的属性的属性描述符的
  2. 如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty是无能为力的
  3. 使用Object.defineProperty实际上是使用修改了描述符的属性去覆盖原本的同名属性,也就是使用Object.defineProperty进行属性值的改变会修改原对象

Proxy

在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理

也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象)

之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作

我们之后的操作都是直接对Proxy的操作,而不是原有的对象

const user = {
  name: 'Klaus',
  age: 23
}


// 创建对user对象的代理对象
/*
  参数1:需要被代理的对象
  参数2:捕获器对象(handler) ---> 对应的回调函数对象
    --> 在执行某些操作时候的回调
    --> Proxy提供了13种捕获器(trap)
    --> 如果不进行任何的设置,会使用默认的捕获器
*/
const proxy = new Proxy(user, {})

// 使用默认的get捕获器
console.log(user.name)

// 使用默认的set捕获器
user.age = 18
console.log(user.age)

捕获器

IB4kxf.png

const user = {
  name: 'Klaus',
  age: 23
}


const proxy = new Proxy(user, {
  // 重写get捕获器

  /*
    参数1: target 被代理的原对象
    参数2: key 被获取的属性key
    参数3: receiver:调用的代理对象
  */
  get(target, key) {
    return target[key]
  },

  // 重写set捕获器

  /*
    参数1: target 被代理的原对象
    参数2: key 被获取的属性key
    参数3: value 新属性值
    参数4: receiver:调用的代理对象
  */
  set(target, key, value) {
    target[key] = value
  },

  // in操作符 对应的捕获器
  has(target, key) {
    console.log('执行了in操作符')
    return key in target
  },

  // 删除属性 对应的捕获器
  defineProperty(target, key) {
    console.log('执行了delete操作')

    // 返回值是boolean值 代表对应的属性是否被成功移除
    return delete target[key]
  }
})

// 所有的操作都需要对代理对象进行操作
console.log(proxy.name)

proxy.age = 18
console.log(proxy.age)

console.log('age' in proxy)

console.log(delete proxy.age)
// 2个特别的捕获器 --- 专门针对于函数对象进行代理操作
function foo() {}

const proxy = new Proxy(foo, {
  construct(target, args) {
    console.log('执行了new操作')
    return new target(...args)
  },

  // 注意: 只有apply捕获器,没有call捕获器
  // 注意: 第二个参数是apply调用的时候传入的this对象
  //   --- 形参的名称不要和关键字同名,所以建议命名为thisArg
  apply(proxy, thisArg, args) {
    console.log('执行了apply操作')
    return proxy.apply(thisArg, args)
  }
})

proxy.apply({})
new proxy()

Reflect

Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射, 映射

那么这个Reflect有什么用呢?

  • 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法
    • 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf()
    • 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()
  • 在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面
  • 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适
  • 另外还包含一些类似于 in、delete操作符也可以对对象进行操作,这让JS看起来是会有一些奇怪
  • 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上

Object和Reflect对象之间的API关系,可以参考MDN文档

基本使用

在使用Proxy的时候,我们需要重写一系列的捕获器,因为要完成原本的功能

所以我们在重写捕获器的时候,依旧需要操作原对象(target)来完成我们对应的功能

但是proxy的本意是使用代理对象来替换原始对象,也就是所有的操作都在代理对象上进行操作,不要去主动操作原对象

此时在语义上就会存在问题

get(target, key) {
  return target[key]
},

set(target, key, value) {
 target[key] = value
}

所以ES6+中,我们可以使用Reflect对象来替代我们原本对于原对象的操作

get(target, key) {
  return Reflect.get(target, key)
},

set(target, key, value) {
  Reflect.set(target, key, value)
}

所以,其实我们可以看到,Reflect中的方法其实和Proxy中的捕获器是一一对应的

IB4p6U.png

总结:

  1. Reflect对象上定义了可以操作对象的静态方法,这些方法和Proxy的捕获器是一一对应的

  2. Reflect对象是对原本Object上操作对象方法和操作符的统一和规范

  3. Reflect对象上的静态方法基本可以划分为两类:

    • 一类是原来存在Object上的方法,将它转义到了Reflect上,并作了小改动,让方法更加合理, 如defineProperty,getPrototypeOf,apply
    • 另一类是将原来操作符的功能,变成函数行为,如in ---> has,delete ---> deleteProperty
const user = {}

Object.defineProperty(user, 'name', {
  value: 'Klaus',
  configurable: false,
  enumerable: true
})

// 传统写法 --- 静默错误
console.log(user.name = 'Alex') // => Alex

// 使用Reflect --- 会返回一个boolean值, 表示操作是否成功
const isSuccess = Reflect.set(user, 'name', 'Klaus')

console.log(isSuccess) // => false

Receiver

const user = {
  _name: 'Klaus',

  get name() {
    return this._name
  },

  set name(v) {
    this._name = v
  }
}

const proxy = new Proxy(user, {
  get(target, key) {
    console.log('set -----', key)
    return Reflect.get(target, key)
  },

  set(target, key, newV) {
    console.log('get ----', key)
    Reflect.set(target, key, newV)
  }
})

proxy.name = 'Alex'
console.log(proxy.name)

/* 
  =>
  get ---- name
  set ----- name
  Alex
*/

可以看到get捕获器和set捕获器只被执行了一次,也就是对于属性_name的存取操作并没有被代理对象监听到

这是因为默认情况下,在执行对象的访问器属性的时候,内部的this是原始对象,并不是代理对象

const user = {
  _name: 'Klaus',

  get name() {
    console.log(this === user) // => true
    
    return this._name
  },

  set name(v) {
    this._name = v
  }
}

const proxy = new Proxy(user, {
  get(target, key) {
    // 实际操作等价于 target[key]
    return Reflect.get(target, key)
  },

  set(target, key, newV) {
    // 实际操作等价于 target[key] = newV
    Reflect.set(target, key, newV)
  }
})

proxy.name = 'Alex'
console.log(proxy.name)

为此,Proxy对于set和get捕获器设置了一个特殊的参数Reciver,这个参数可以用来设置在使用对象的访问器属性的时候,其内部this的值

const user = {
  _name: 'Klaus',

  get name() {
    return this._name
  },

  set name(v) {
    this._name = v
  }
}

const proxy = new Proxy(user, {
  get(target, key) {
    // 执行user.name的get访问器的时候
    // 内部的this指向的是 { _name: 'Alex' }
    return Reflect.get(target, key, {
      _name: 'Alex'
    })
  },

  set(target, key, newV) {
    Reflect.set(target, key, newV)
  }
})

console.log(proxy.name) // => Alex

所以我们可以使用receiver参数,来对一些私有属性进行监听

const user = {
  _name: 'Klaus',

  get name() {
    return this._name
  },

  set name(v) {
    this._name = v
  }
}

const proxy = new Proxy(user, {
  get(target, key, receiver) {
    console.log(proxy === receiver) // => true
    console.log('get ----', key)

    return Reflect.get(target, key, receiver)
  },

  set(target, key, receiver) {
    console.log('set ----', key)
    Reflect.set(target, key, receiver)
  }
})

proxy.name = 'Alex'
console.log(proxy.name)

此时代理对象中的get和set捕获器就被执行了两次,而对于_name这个私有属性的修改也被正常的监听到了

Reflect.construct

function Student(name, age) {
  this.name = name
  this.age = age
}

function Teacher() {}

// 借用Student构造方法,创建Teacher的实例对象
const teacher = Reflect.construct(Student, ['Klaus', 23], Teacher)

console.log(teacher) // => { name: 'Klaus', age: 23 }
console.log(teacher instanceof Teacher) // => true
console.log(teacher instanceof Student) // => false

上述操作等价于

function Student(name, age) {
  this.name = name
  this.age = age
}

function Teacher() {}

const teacher = new Student('Klaus', 23)
Object.setPrototypeOf(teacher, Teacher.prototype)

console.log(teacher) // => { name: 'Klaus', age: 23 }
console.log(teacher instanceof Teacher) // => true
console.log(teacher instanceof Student) // => false