Web 前端开发日志(一):Proxy 与 Reflect

2,301 阅读6分钟

文章为在下以前开发时的一些记录与当时的思考, 学习之初的内容总会有所考虑不周, 如果出错还请多多指教.

TL;DR

ProxyReflect 是用于实现元编程的 API,是应对复杂应用程序与工程管理的得力助手.

Proxy 一般用于拦截 JS 的默认行为,Reflect 一般用于对被拦截的对象进行修改操作.

Proxy

Proxy 提供拦截 JS 默认行为的能力,比如从一个对象的属性取值、赋值时,或者 new 一个 Constructor 时,可以使用 Proxy 把这个行为给拦住,然后给个机会加入自己的逻辑,去达到自己想要的目标.

基本操作是指类似属性访问、赋值、遍历、函数调用等行为.

好像 ES5 的访问器也有类似的效果?

实际上 ES5 中 Object.defineProperty 的访问器确实能达到一部分这样的效果,但只能拦截属性的访问与赋值操作;ES6 提供的 Proxy 能够拦截的操作类型要多不少,这样才能满足元编程的需求.

基础语法

// 创建一个代理.
const proxy = new Proxy(target, handler)
  • target 是想要修改的目标对象.
  • handler 是一个包含一堆“定义代理行为的函数”的对象,这堆函数有个比较酷炫的名词,叫陷阱.

那么再来一个更详细一点的例子:

class Student {
  constructor (
    public name: string,
	score: number
  ) {}
}

const student = new Proxy(new Student('LancerComet', 59), {
  get (target, property) {
    // 当访问不存在的属性时, 打印一行提示.
    if (typeof target[property] !== 'undefined') {
	  return target[property]
	} else {
	  console.log('Wow, what are you looking ♂ for?')
	}
  }
})

student.name  // 'LancerComet'
student.score // 59
student.age   // Wow, what are you looking ♂ for?

这个例子的意思是,当访问一个对象的不存在的属性时,将打印一行文字.

所以利用 Proxy,好像能做不少事情?

Handler 的 API

Handler 提供了很多可以拦截 JS 中默认行为的方法:

调用行为拦截

  • handler.apply(targetFunc, thisContext, args) - 拦截函数调用行为,使得函数调用时按照自定义的逻辑执行.
  • handler.construct(targetConstructor, args, proxyConstructor) - 拦截 new 操作符行为,可以对 new 操作进行加工,有点类装饰器的意思.

属性访问拦截

  • handler.get(target, property, receiver?) - 拦截属性读取操作,在访问目标对象属性时将触发此拦截陷阱.
  • handler.getPrototypeOf() - 拦截对原型的访问操作,当使用 Object.getPrototypeOf()Reflect.getPrototypeOf()__proto__Object.prototype.isPrototypeOf()instanceof 任一操作时将触发此拦截陷阱.
  • handler.has() - 拦截属性检查操作符,当使用 inReflect.has(proxy)with(proxy) 时将触发此拦截陷阱.
  • handler.ownKeys(target) - 拦截 Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys() 操作.
  • handler.set(target, property, receiver?) - 拦截属性赋值操作,在对目标属性赋值时将触发此拦截陷阱.

Object 静态方法拦截

以下陷阱均拦截 Object 对象中对应的静态方法:

  • handler.defineProperty(target, property, descriptor)
  • handler.deleteProperty(target, property)
  • handler.getOwnPropertyDescriptor(target, property, descriptor)
  • handler.isExtensible(target)
  • handler.preventExtensions(target)
  • handler.setPrototypeOf(target, prototype)

由于篇幅问题每个 API 不再详细举例,不过您已经知道了 Proxy 的作用,查一查 API 应该没什么问题 🍺🐸

Reflect

Reflect 提供了一组操作与修改对象的 API,以便在 Proxy 的陷阱中对目标进行操作.

那么这样一来关系就很明了了,Proxy 提供拦截操作,Reflect 提供修改操作.

Reflect 的 API

Reflect 的 API 和 Proxy 的 Handler 的 API 非常相似,所以可以很容易的在编写 Proxy 逻辑时从 Reflect 找到对应 API,保持思路清晰.

调用行为操作

  • Reflect.apply(function, this, args) - 传入上下文与参数列表对目标函数进行调用,目的和 Function.prototype.apply 是一致的.
  • Reflect.construct(Constructor, args) - 目的同 new Constructor(args).

属性访问操作

  • Reflect.get(target, property, receiver?) - 从目标对象中获取目标属性值.
  • Reflect.has(target, property) - 检测目标对象是否有目标属性.
  • Reflect.set(target, property, value, receiver?) - 对目标对象的目标属性进行赋值.
  • Reflect.ownKeys(target) - ownKeys 是 Reflect 的新方法,作用相当于 Object.getOwnPropertyNames() + Object.getOwnPropertySymbols(),获取当前对象的

Object 静态方法替代与补充

以下方法为 Object 对应方法的替代方法,就不过多解释:

  • Reflect.defineProperty(target, property, attributes)
  • Reflect.deleteProperty(target, property)
  • Reflect.getOwnPropertyDescriptor(target, property)
  • Reflect.getPrototypeOf(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.setPrototypeOf(target, prototype)

API 和 Object 那么像,为啥叫 Reflect,咋不起个 ObjectV2 ?

我们看一下反射的定义(摘自 Wikipedia):

在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。

那么很显然,Reflect 提供的这些 API 的目的就是在运行时可以去访问和修改 JS 代码数据和行为的能力,所以中央就决定叫 Reflect 了.

说的更俗气一点,如果把新加入的 API 全部都扔到 Object 上的话不就会非常乱嘛,这些 API 的职责实际上也并不属于 Object 对象的设计管辖范围,如果强行加入到 Object 中,那体验是不是就非常糟糕.

另外尽管和 Object 已有的 API 比较相似,实际上行为上略有细微调整,更加方便使用:

// Object 中的 defineProperty 不返回操作状态,需要在 try / catch 代码块中获取状态.
try {
  Object.defineProperty({}, 'name', {...})
  // Done!
} catch (e) {
  // Boom!
}

// Reflect 中直接返回操作状态.
if (Reflect.defineProperty({}, 'name', {...})) {
  // Done!
} else {
  // Boom.
}

有些方法和已有的好像没差别,比如 apply,干嘛还单独搞一个?

因为以前的方法都可以被 Proxy 拦截掉,所以一个对象原型链上的诸如 call()apply() 等方法并不保证其行为是默认行为,所以需要一个能够提供默认操作行为的 API 集合,这就是 Reflect.

使用案例:创建运行时的类型安全对象

就算使用 TypeScript,实际上在运行时依然会出现因为类型安全问题而引起的关键业务错误,对于这种严格场景,可以使用 Proxy 与 Reflect 确保目标业务的数据的类型安全与严格.

// Utils 类提供创建类型安全对象的静态方法.

class Utils {
  static createTypeSafetyInstance <T> (Constructor: new (...args) => any, ...args): T {
    const obj = new Constructor(...args)
    return new Proxy(obj, {
      set (target, keyName, value, proxy) {
        const newType = getType(value)
        const correctType = getType(target[keyName])
        if (newType === correctType) {
          Reflect.set(target, keyName, value)
        } else {
          console.warn(
            `[Warn] Incorrect data type was given to property "${keyName}" on "${Constructor.name}":\n` +
            `       "${value}" (${getTypeText(newType)}) was given, but should be a ${getTypeText(correctType)}.`
          )
        }
        // 永远返回 true 防止出现运行时报错.
        return true
      }
    })
  }
}

function getType (target: any) {
  return Object.prototype.toString.call(target)
}

function getTypeText (fullTypeString: string) {
  return fullTypeString.replace(/\[object |\]/g, '')
}

export {
  Utils
}
// 一个测试用例.

import { Utils } from './utils'

class Student {
  static create (param?: IStudent): Student {
    return Utils.createTypeSafetyInstance(Student, param)
  }

  name: string = ''
  age: number = 0

  constructor (param?: IStudent) {
    if (param) {
      this.name = param.name
      this.age = param.age
    }
  }
}

interface IStudent {
  name: string
  age: number
}

test('It should be a type-safety instance.', () => {
  const johnSmith = Student.create({
    name: 'John Smith', age: 20
  })

  expect(johnSmith.name).toEqual('John Smith')
  expect(johnSmith.age).toEqual(20)

  johnSmith.name = 'John'
  expect(johnSmith.name).toEqual('John')

  johnSmith.age = 'Wrong type' as any // age 修改为错误的类型.
  expect(johnSmith.age).toEqual(20)   // 当遇到错误类型时保持为上一个正确数据.
})