前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

ES6系列之Reflect的设计意图与API全览

首页2018-12-21 11:10:31Front-End
ES6JavaScriptReflect元编程

第一次看 Vue 3 的响应式源码,很多人会卡在同一个地方:get trap 里明明写 return target[key] 就够了,为什么源码非要写成 Reflect.get(target, key, receiver)?多传一个参数到底有什么用?

这个问题的答案,正好就是 Reflect 存在的全部理由。它不是给 Object 换了个马甲,而是给「对象上的基础操作」提供了一套和 Proxy trap 严格一一对应的函数入口,让你在拦截之后能把默认行为原样补回去,还不丢失 this 指向。这篇把 Reflect 的四条设计理由讲清楚,逐个过一遍它的 API,重点落在 receiver 参数上。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • Reflect 出现的四条设计理由,每条解决什么实际问题
  • 报错改返回布尔值,对代码结构的影响
  • 操作符变函数带来的可组合性
  • Reflect.set 的 receiver 参数为什么会触发 defineProperty 拦截
  • 13 个 Reflect 方法逐个过一遍,附对应的旧写法
  • 什么时候该用 Reflect,什么时候 Object 就够了

# 一、简介

# 1.1 什么是 Reflect

Reflect 是为操作对象而提供的一套新 API。

它有两个和别的内置对象不一样的地方,值得先说清楚。第一,Reflect 不是构造函数,不能 new,它就是一个普通对象,上面挂了 13 个静态方法。第二,它的每一个方法都和 Proxy 的 trap 一一对应,名字和参数完全对得上。这个「一一对应」不是巧合,是设计出来的。

# 1.2 为什么要设计 Reflect

第一,将 Object 对象的属于语言内部的方法放到 Reflect 对象上。

Object 这个构造函数身上现在挂了一堆东西,有些是给日常业务用的(Object.assign、Object.keys),有些其实是语言内部的元操作(Object.defineProperty、Object.getOwnPropertyDescriptor)。这两类东西混在一起,职责很不清楚。Reflect 的第一个作用就是给后一类找个归宿,以后新增的语言内部方法只往 Reflect 上加,Object 保持干净。

第二,将用老 Object 方法报错的情况,改为返回 false。

// 旧写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}
@前端进阶之旅: 代码已经复制到剪贴板
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}
@前端进阶之旅: 代码已经复制到剪贴板

这个改动看着小,实际影响不小。try...catch 是为「异常」准备的控制流,而「这个属性不可配置所以定义失败」根本算不上异常,它是一个很正常的、可预期的结果。用布尔返回值表达可预期的失败,用异常表达真正的意外,代码读起来清楚多了。

第三,让 Object 操作变成函数行为。

// 旧写法
'name' in Object // true
@前端进阶之旅: 代码已经复制到剪贴板
// 新写法
Reflect.has(Object, 'name') // true
@前端进阶之旅: 代码已经复制到剪贴板

in、delete 这些是操作符,不是函数,没法传给别的函数当参数,也没法放进数组里遍历。变成函数之后就能组合了:

const keys = ['a', 'b', 'c'];
const missing = keys.filter(k => !Reflect.has(obj, k));
// 用操作符写不出这么直白的一行
@前端进阶之旅: 代码已经复制到剪贴板

第四,Reflect 与 Proxy 是相辅相成的,在 Proxy 上有的方法,在 Reflect 上就一定有。

let target = {}
let handler = {
  set(target, proName, proValue, receiver){
    // 确认对象的属性赋值成功
    let isSuccess = Reflect.set(target, proName, proValue, receiver)
    if(isSuccess){
      console.log("成功")
    }
    return isSuccess
  }
}
let proxy = new Proxy(target, handler)
@前端进阶之旅: 代码已经复制到剪贴板

这条是四条里最重要的。你拦截了一个操作,做完自己的事情之后,总得把默认行为执行掉,不然代理对象就废了。而「默认行为」是什么?就是 Reflect 上同名的那个方法。参数列表和 trap 完全一致,返回值也刚好是 trap 要求的类型,抄过去就能用。

确保对象的属性能正确赋值,广义上讲,即确保对象的原生行为能够正常进行,这就是 Reflect 的作用。

不用 Reflect 行不行?简单场景确实可以写 target[key] = value,但有两个问题。一是它没有返回值,你没法告诉 trap 赋值到底成没成功,而严格模式下 set trap 必须返回 true,你只能硬编码一个 return true,撒谎。二是丢了 receiver,原型链上的 getter/setter 里 this 会指错,这个下面 2.3 节展开。

# 二、Reflect 的 API

这 13 个方法和 Proxy 的 trap 一一对应,名字都能对上,学一遍等于学两个东西。

# 2.1 Reflect.get(target, property, receiver)

查找并返回 target 对象的 property 属性。

let obj = {
  name: "poetries",
}
let result = Reflect.get(obj, "name")
console.log(result) // poetries
@前端进阶之旅: 代码已经复制到剪贴板

第三个参数 receiver 在有 getter 的时候才会显出作用:

let obj = {
  // 属性 yu 部署了 getter 读取函数
  get yu(){
    // this 返回的是 Reflect.get 的 receiver 参数对象
    return this.name + this.age
  }
}

let receiver = {
  name: "shen",
  age: "18",
}

let result = Reflect.get(obj, "yu", receiver)
console.log(result) // shen18
@前端进阶之旅: 代码已经复制到剪贴板

yu 这个 getter 定义在 obj 上,但执行的时候 this 被换成了 receiver,所以拿到的是 shen 和 18,而不是 obj 上的(obj 上压根没有 name 和 age,不传 receiver 的话结果是 undefinedundefined)。

一句话概括:receiver 决定了 getter/setter 执行时 this 指向谁。

注意:如果 Reflect.get() 的第一个参数不是对象,会抛 TypeError。

# 2.2 Reflect.set(target, propName, propValue, receiver)

设置 target 对象的 propName 属性为 propValue,返回一个布尔值表示是否成功。

let obj = {
  name: "poetries"
}

let result = Reflect.set(obj, "name", "静观流叶")
console.log(result) // true
console.log(obj.name) // 静观流叶
@前端进阶之旅: 代码已经复制到剪贴板

# 2.3 Reflect.set 与 Proxy.set 的配合

Reflect.set 与 Proxy 的 set 联合使用,并且传入 receiver,会额外触发一次定义属性的操作。

let obj = {
  name: "chen"
}

let handler = {
  set(target, key, value, receiver){
    console.log("Proxy 拦截赋值操作")
    // Reflect 完成赋值操作
    return Reflect.set(target, key, value, receiver)
  },
  defineProperty(target, key, attribute){
    console.log("Proxy 拦截定义属性操作")
    // Reflect 完成定义属性操作
    return Reflect.defineProperty(target, key, attribute)
  }
}

let proxy = new Proxy(obj, handler)
proxy.name = "ya"
// Proxy 拦截赋值操作
// Proxy 拦截定义属性操作
@前端进阶之旅: 代码已经复制到剪贴板

原文这两个 trap 都没有 return,严格模式下会直接抛错,上面补上了。

那为什么 Reflect.set() 传入 receiver 参数,就会触发定义属性的操作?

原文的解释是「因为 Proxy.set() 中的 receiver 是 Proxy 的实例,即 obj」,这句有个事实错误,receiver 是 proxy 对象本身,不是 obj,两者是不同的对象。

正确的链路是这样的。set trap 收到的第四个参数 receiver 就是那个 proxy 对象。你把它原样传给 Reflect.set,Reflect.set 的规则是「把属性赋到 receiver 身上」,而 receiver 是 proxy,于是这次赋值又走了一遍代理。赋值到底怎么落地?内部会调 [[DefineOwnProperty]],也就是触发 defineProperty trap。所以你看到了两条日志。

这个行为不是 bug,但确实容易写出无限递归。如果你在 defineProperty trap 里又往 receiver 上写,那就转不出来了。上面这段之所以安全,是因为 Reflect.defineProperty 的第一个参数是 target 而不是 proxy,落到了原对象上,链条终止。

那到底该不该传 receiver?我的判断是:涉及原型链继承和 getter/setter 的场景必须传,否则 this 会指错,Vue 3 的 reactive 就是这么做的;纯粹给自己对象读写、没有继承关系的简单场景,不传也不会出问题。Proxy 那边完整的 trap 清单和实战用法我写在 ES6系列之 Proxy 的拦截机制与实战场景 里,两篇是配套的。

# 2.4 Reflect.has(obj, name)

对应 in 操作符,也对应 Proxy 的 has trap。

var obj = {
  name: "poetries",
};
@前端进阶之旅: 代码已经复制到剪贴板
// 旧写法
'name' in obj // true
@前端进阶之旅: 代码已经复制到剪贴板
// 新写法
Reflect.has(obj, 'name') // true
@前端进阶之旅: 代码已经复制到剪贴板

注意 has 会沿着原型链查找,和 in 的行为一致。只想查自身属性得用 Object.prototype.hasOwnProperty.call(obj, 'name') 或者更现代的 Object.hasOwn(obj, 'name')。

fe
  • 一、简介
    • 1.1 什么是 Reflect
    • 1.2 为什么要设计 Reflect
  • 二、Reflect 的 API
    • 2.1 Reflect.get(target, property, receiver)
    • 2.2 Reflect.set(target, propName, propValue, receiver)
    • 2.3 Reflect.set 与 Proxy.set 的配合
    • 2.4 Reflect.has(obj, name)
    • 2.5 Reflect.deleteProperty(obj, name)
    • 2.6 Reflect.construct(target, args)
    • 2.7 Reflect.getPrototypeOf(obj)
    • 2.8 Reflect.setPrototypeOf(obj, newProto)
    • 2.9 Reflect.apply(func, thisArg, args)
    • 2.10 Reflect.defineProperty(target, propertyKey, attributes)
    • 2.11 Reflect.getOwnPropertyDescriptor(target, propertyKey)
    • 2.12 Reflect.isExtensible(target)
    • 2.13 Reflect.preventExtensions(target)
    • 2.14 Reflect.ownKeys(target)
  • 三、什么时候用 Reflect
  • 总结
  • 参考

← ES6系列之装饰器的原理与实战用法ES6系列之Proxy的拦截机制与实战场景 →