JavaScript 中的 Reflect 反射

7,312 阅读3分钟

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

Reflect 是一个内置对象,提供了一系列操作对象的方法,这些方法都是静态的(类似 Math),使用 Reflect 的方法可以做一些对象的基本操作。

Reflect 意思是反射,反射是在程序运行中获取和动态操作自身内容的一项技术。其实对于 JavaScript 来说在出现 Reflect 之前就已经有反射的能力了,我们可以使用 Object.keys() 获取对象属性,可以使用 Object.defineProperty 定义属性,这些实际上都是反射的能力。既然已经可以实现反射功能,那为什么还需要有 Reflect 呢?

为什么要有 Reflect

来看一个真实的使用场景,ext 是一个普通对象,它的全部属性信息,使用 Object 应该怎样做呢?

看起来很简单是吧?可以这样:

Object.keys(ext);

或者这样:

Object.getOwnPropertyNames(ext);

然而这样获取到的只是字符串属性,对象的属性只能是字符串吗?并不是,除了字符串,Symbol 类型也可以作为对象的属性 key,因此上面的代码应该改成这样:

Object.getOwnPropertyNames(ext)
        .concat(Object.getOwnPropertySymbols(ext));

这是一个在开源框架中的常见用法: github.com/eggjs/egg-c…

如果有了 Reflect,上面的代码只需要这样:

Reflect.ownKeys(ext);

效果是完全相同的,但是语义更加明确。

上面这个例子可能不够直观,你可以说这不是就是增加了一个新的 API 吗?那我们再看一个例子:

使用 Object.defineProperty 为对象添加属性,如果失败了如何处理呢?

由于 Object.defineProperty 失败是会直接抛一个异常,因此代码应该是这样的:

try {
    Object.defineProperty(obj, prop, attr);
    // success
} catch (e) {
    // fail
}

使用 Reflect 代码就变成下面这样:

if (Reflect.defineProperty(obj, prop, attr)) {
  // success
} else {
  // fail
}

看起来区别不大,但是哪一种更合理呢?反射一种编程语言的行为,我执行反射做什么我只需要知道是否成功就可以了,我可能希望核心流程被异常打断。反过来说如果我想要异常我可以自己判断在失败时抛出异常,而不是你直接给我一个异常我原本处理失败的逻辑现在都要写 catch 块中。因此 Reflect 这种实现和 Object 相比提供了更完整的能力。

再比如删除属性,在不使用 Reflect 时需要使用 delete 关键字,而使用 Reflect 可以调用上面的 deleteProperty 方法来实现功能,其中的区别在实际开发中可以有更明显的感受。

Reflect 提供的是一整套反射能力 API,它们的调用方式,参数和返回值都是统一风格的,我们可以使用 Reflect 写出更优雅的反射代码。在 Reflect 库出现之前我们只是利用分散的 API 完成一些动态编程的事情,Reflect 使得反射真正成为一种标准化能力。

Reflect 相关方法

Reflect 上面的方法:

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

这些方法其实都很简单,通过名字也很容易知道方法的定义,大部分也都在 Object 上面见到过,Reflect 与 Object 部分重复方法详细的区别可以查看 MDN

Reflect 的应用

proxy

Reflect 对象上面的方法和 Proxy 上面都是一一对应的,参数也是完全相同的,因此在 Proxy 中可以使用 Reflect 来操作对象:

new Proxy(obj, {
  set(...args) {
    return Reflect.set(...args)
  },
  get(...args) {
    return Reflect.get(...args);
  },
  deleteProperty(...args) {
    return Reflect.deleteProperty(...args);
  },
  has(...args) {
    return Reflect.has(...args);
  }
}

metadata

Reflect 更强大之处在于它的元编程能力,有一个 metadata 的提案可以通过 Reflect 添加元数据信息,使用元数据编程能够更方便进行框架开发,metadata 的提案目前还在 stage 阶段,目前可以通过 github.com/rbuckton/re… 库来提前使用相关功能,在 typescript 中还可以使用 emitDecoratorMetadata 的增强功能,感兴趣可以阅读。