一、面试题:什么是 Reflect?它解决了什么问题?
核心思路(一句话)
Reflect 是一个提供对象底层操作标准化 API 的内置对象,让我们可以更直接地执行 JavaScript 对象的内部操作,并特别适合与 Proxy 配合。
1. 结构化逻辑思维
可以把 JavaScript 对象操作理解成三层:
text
JavaScript 代码
↓
对象语法 / Object 方法 / Reflect 方法
↓
ECMAScript 对象内部方法
↓
真正执行对象操作
例如:
js
obj.name = '张三';
表面上是属性赋值。
底层对应的是对象的:
text
[[Set]]
再比如:
js
obj.name
底层对应:
text
[[Get]]
再比如:
js
delete obj.name
底层对应:
text
[[Delete]]
而:
js
Reflect.get(obj, 'name')
Reflect.set(obj, 'name', '张三')
Reflect.deleteProperty(obj, 'name')
则是以 JavaScript API 的形式显式调用对应的对象操作。
ECMAScript 规范确实定义了大量对象内部方法,例如 [[Get]]、[[Set]]、[[Delete]]、[[GetPrototypeOf]]、[[SetPrototypeOf]]、[[OwnPropertyKeys]] 等。Reflect 对象提供了与这些底层操作对应的一组标准 API。(TC39)
二、对象的"基本操作"到底是什么?
核心思路(一句话)
所谓对象基本操作,就是
ECMAScript 规定对象必须能够执行的一组底层内部操作。
典型的内部方法包括:
text
对象内部方法
│
├── [[GetPrototypeOf]]
├── [[SetPrototypeOf]]
├── [[IsExtensible]]
├── [[PreventExtensions]]
├── [[GetOwnProperty]]
├── [[DefineOwnProperty]]
├── [[HasProperty]]
├── [[Get]]
├── [[Set]]
├── [[Delete]]
├── [[OwnPropertyKeys]]
│
└── 函数对象额外拥有
├── [[Call]]
└── [[Construct]]
例如:
js
obj.name
对应:
text
obj
↓
[[Get]]("name")
js
obj.name = '张三';
对应:
text
obj
↓
[[Set]]("name", "张三", receiver)
js
delete obj.name;
对应:
text
obj
↓
[[Delete]]("name")
js
Object.getPrototypeOf(obj);
对应:
text
obj
↓
[[GetPrototypeOf]]
这些内部方法不是普通 JavaScript 函数,开发者通常不能直接写:
js
obj.[[Get]](...)
而是通过语言语法、Object 方法、Reflect 方法、Proxy 等间接触发。
三、Reflect 和 Object 有什么区别?
这是这道题非常容易被问到的追问。
核心思路
Object 更偏向"对象工具方法",Reflect 更偏向"把对象底层操作以函数形式暴露出来"。
例如:
js
Object.defineProperty(obj, 'name', {
value: '张三'
});
对应:
js
Reflect.defineProperty(obj, 'name', {
value: '张三'
});
但二者并不完全等价。
一个非常重要的区别是:
js
Object.defineProperty(...)
失败时通常会抛出异常。
而:
js
Reflect.defineProperty(...)
返回:
js
true / false
例如:
js
const obj = {};
Object.preventExtensions(obj);
// Object.defineProperty 会抛异常
try {
Object.defineProperty(obj, 'name', {
value: '张三'
});
} catch (error) {
console.log('Object.defineProperty 失败');
}
// Reflect.defineProperty 返回 false
const result = Reflect.defineProperty(obj, 'age', {
value: 18
});
console.log(result); // false
所以不要简单记成:
Reflect = Object 的另一个版本
更准确的是:
Reflect 提供了一组与对象内部操作对应的统一函数式 API。
四、为什么需要 Reflect?
核心思路(一句话)
Reflect 的重要价值之一,是让开发者能够以更接近对象内部操作的方式进行操作,并且
特别适合 Proxy 中保持默认行为。
流程图
text
传统 JavaScript
│
├── obj.foo
├── obj.foo = value
├── delete obj.foo
└── Object.xxx()
│
↓
间接触发内部方法
│
↓
[[Get]]
[[Set]]
[[Delete]]
...
Reflect
│
├── Reflect.get()
├── Reflect.set()
├── Reflect.deleteProperty()
└── Reflect.defineProperty()
│
↓
更直接地对应底层对象操作
五、Object.keys 和 Reflect.ownKeys 有什么区别?
核心思路(一句话)
Object.keys()只要"自身 + 可枚举 + 字符串键",Reflect.ownKeys()要"自身 + 字符串键和 Symbol 键 + 不管是否可枚举"。
对比
| API | 自身属性 | 原型属性 | 不可枚举 | Symbol |
|---|---|---|---|---|
Object.keys() |
✅ | ❌ | ❌ | ❌ |
Object.getOwnPropertyNames() |
✅ | ❌ | ✅ | ❌ |
Object.getOwnPropertySymbols() |
✅ | ❌ | ✅ | ✅ |
Reflect.ownKeys() |
✅ | ❌ | ✅ | ✅ |
for...in |
❌只限字符串 | ✅ | ❌ | ❌ |
例如:
js
const symbolKey = Symbol('id');
const obj = {
name: '张三'
};
Object.defineProperty(obj, 'age', {
value: 18,
enumerable: false
});
obj[symbolKey] = 100;
console.log(Object.keys(obj));
// ['name']
console.log(Object.getOwnPropertyNames(obj));
// ['name', 'age']
console.log(Object.getOwnPropertySymbols(obj));
// [Symbol(id)]
console.log(Reflect.ownKeys(obj));
// ['name', 'age', Symbol(id)]
Reflect.ownKeys()可以拿到对象自身的全部属性键,包括不可枚举属性和 Symbol 属性,但不包括原型链属性。
六、Reflect.get 为什么有第三个参数 receiver?
这是整个内容中最重要的知识点。
核心思路(一句话)
Reflect.get(target, key, receiver)的第三个参数决定访问器属性中的this指向,因此在 Proxy 中通常应该把receiver传成 Proxy 本身。
1. 普通情况
js
const obj = {
a: 10,
b: 20,
get total() {
return this.a + this.b;
}
};
console.log(obj.total); // 30
访问:
js
obj.total
实际上 getter 内部:
js
this.a
this.b
这里的 this 是:
js
obj
2. Reflect.get 可以显式指定 receiver
js
const obj = {
a: 10,
b: 20,
get total() {
return this.a + this.b;
}
};
const receiver = {
a: 100,
b: 200
};
console.log(
Reflect.get(obj, 'total', receiver)
);
// 300
为什么?
因为:
text
Reflect.get(
target = obj,
key = 'total',
receiver = receiver
)
↓
找到 getter
↓
getter 中 this = receiver
↓
this.a + this.b
↓
100 + 200
↓
300
这就是 Reflect.get() 第三个参数真正重要的地方。
七、为什么 Proxy 中推荐 Reflect.get(target, key, receiver)?
这是 Vue 3 响应式源码中非常重要的设计。
核心思路(一句话)
Proxy 的 get 拦截器中使用
Reflect.get(target, key, receiver),可以让 getter 内部继续通过 Proxy 访问依赖,从而保证深层访问也能被拦截。
1. 错误写法
js
const target = {
a: 10,
b: 20,
get total() {
return this.a + this.b;
}
};
const proxy = new Proxy(target, {
get(target, key) {
console.log('拦截:', key);
return target[key];
}
});
console.log(proxy.total);
执行过程:
text
proxy.total
↓
Proxy.get
↓
target.total
↓
getter 执行
↓
this = target
↓
this.a
this.b
↓
直接访问 target.a / target.b
↓
绕过 Proxy
所以你可能看到:
text
拦截: total
但是看不到:
text
拦截: a
拦截: b
八、正确写法:Reflect.get + receiver
js
const target = {
a: 10,
b: 20,
get total() {
return this.a + this.b;
}
};
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log('拦截:', key);
return Reflect.get(target, key, receiver);
}
});
console.log(proxy.total);
执行流程:
text
proxy.total
↓
Proxy.get
↓
Reflect.get(
target,
"total",
proxy
)
↓
找到 getter
↓
getter 中 this = proxy
↓
this.a
↓
proxy.a
↓
再次进入 Proxy.get
↓
Reflect.get(...)
↓
得到 10
this.b
↓
proxy.b
↓
再次进入 Proxy.get
↓
得到 20
最终:
text
拦截: total
拦截: a
拦截: b
这才是 Proxy + Reflect 的关键组合。
ECMAScript 对 Proxy 的 [[Get]] 明确定义了 Receiver 参数,并且 Proxy 的 get 陷阱会收到 target、属性键和 receiver 三个参数。(TC39)
九、为什么 Vue 3 源码经常使用 Reflect?
核心思路
不是因为 Vue "必须使用 Reflect 才能实现 Proxy",而是因为 Reflect 能正确保持 Proxy 语义,尤其是访问器属性和 receiver 的传递。
典型逻辑可以抽象成:
text
用户访问 proxy.foo
↓
Proxy get trap
↓
记录依赖 / 执行响应式逻辑
↓
Reflect.get(target, key, receiver)
↓
真正读取属性
↓
如果属性内部又访问 this.xxx
↓
this = proxy
↓
再次进入 Proxy
↓
继续完成依赖追踪
这对于响应式系统尤其重要。
例如:
js
const state = {
firstName: '张',
lastName: '三',
get fullName() {
return this.firstName + this.lastName;
}
};
const proxy = new Proxy(state, {
get(target, key, receiver) {
console.log('track:', key);
return Reflect.get(target, key, receiver);
}
});
console.log(proxy.fullName);
能够得到:
text
track: fullName
track: firstName
track: lastName
这正是响应式系统需要的效果。
十、Reflect.get 的三个参数必须记住
js
Reflect.get(target, propertyKey, receiver)
分别是:
text
target
↓
真正被读取的对象
propertyKey
↓
要读取的属性
receiver
↓
属性访问过程中作为 this 的对象
面试中最容易犯的错误:
js
Reflect.get(target, key, handler); // ❌
不是 handler。
正确的是:
js
Reflect.get(target, key, receiver); // ✅
在 Proxy 的 get 陷阱中:
js
const proxy = new Proxy(target, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
}
});
这里:
text
target → 原始对象
key → 属性名
receiver → 当前代理对象 proxy
十一、Reflect 和 Proxy 的关系是什么?
核心思路(一句话)
Proxy 负责"拦截",Reflect 负责"按照标准对象语义继续执行"。
可以把它们理解成:
text
用户操作
│
↓
Proxy 代理
│
┌─────┴─────┐
↓ ↓
拦截 其他逻辑
│
↓
Reflect
│
↓
执行对象默认操作
典型写法:
js
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 1. 自己做额外逻辑
console.log('读取:', key);
// 2. 使用 Reflect 保持默认行为
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 1. 自己做额外逻辑
console.log('修改:', key, value);
// 2. 使用 Reflect 保持默认行为
return Reflect.set(target, key, value, receiver);
}
});
这也是 Proxy 中非常经典的模式:
text
拦截
↓
自定义逻辑
↓
Reflect 执行默认行为
十二、常见 Reflect API 应该怎么记?
不要死背所有 API,按照"对象基本操作"分类。
| Reflect 方法 | 对应能力 |
|---|---|
Reflect.get() |
读取属性 |
Reflect.set() |
设置属性 |
Reflect.has() |
判断属性是否存在 |
Reflect.deleteProperty() |
删除属性 |
Reflect.defineProperty() |
定义属性 |
Reflect.getOwnPropertyDescriptor() |
获取属性描述符 |
Reflect.getPrototypeOf() |
获取原型 |
Reflect.setPrototypeOf() |
设置原型 |
Reflect.isExtensible() |
判断是否可扩展 |
Reflect.preventExtensions() |
禁止扩展 |
Reflect.ownKeys() |
获取自身全部属性键 |
Reflect.apply() |
调用函数 |
Reflect.construct() |
构造函数调用 |
其中:
js
Reflect.apply()
Reflect.construct()
也很重要,因为函数本身也是对象,只是函数对象拥有额外的 [[Call]] 和 [[Construct]] 内部方法 。Reflect 规范中也明确提供了对应操作。(TC39)
十三、使用场景
场景一:Proxy 默认转发
js
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log('读取属性');
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('修改属性');
return Reflect.set(target, key, value, receiver);
}
});
这是最常见的场景。
场景二:正确处理 getter 的 this
js
const target = {
value: 10,
get double() {
return this.value * 2;
}
};
const receiver = {
value: 100
};
console.log(
Reflect.get(target, 'double', receiver)
);
// 200
场景三:需要获取对象全部自身属性键
js
const symbolKey = Symbol('id');
const obj = {};
Object.defineProperty(obj, 'hidden', {
value: 1,
enumerable: false
});
obj.visible = 2;
obj[symbolKey] = 3;
console.log(Reflect.ownKeys(obj));
// ['hidden', 'visible', Symbol(id)]
十四、边界场景与易错点
1. Reflect.ownKeys 不包含原型属性
js
const parent = {
parentName: '父对象'
};
const child = Object.create(parent);
child.name = '子对象';
console.log(Reflect.ownKeys(child));
// ['name']
如果需要原型链上的属性,需要继续遍历原型:
text
当前对象
↓
Reflect.ownKeys()
↓
Object.getPrototypeOf()
↓
继续 Reflect.ownKeys()
↓
直到 null
2. Reflect.get() 不等于简单的 target[key]
尤其是:
js
Reflect.get(target, key, receiver)
第三个参数会影响 getter 中的 this。
因此:
js
target[key]
和:
js
Reflect.get(target, key, proxy)
在访问器属性场景下可能产生完全不同的行为。
3. Proxy 中不要随便写 target[key]
推荐:
js
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
}
而不是:
js
get(target, key) {
return target[key];
}
核心原因:
text
target[key]
↓
getter 中 this 往往指向 target
↓
内部访问绕过 Proxy
Reflect.get(target, key, receiver)
↓
receiver 通常是 proxy
↓
getter 中 this 指向 proxy
↓
内部访问继续经过 Proxy
十五、这道题的主要矛盾和次要矛盾
主要矛盾
理解 Reflect 的本质:
text
Reflect
↓
以函数形式暴露对象底层操作
↓
尤其用于 Proxy 中保持默认对象语义
次要矛盾
Object.keys()和Reflect.ownKeys()的区别- 属性描述符与可枚举性
Reflect.get()第三个参数receiver- getter 中
this的绑定 - Proxy 为什么经常配合 Reflect
- Vue 3 响应式为什么需要正确传递
receiver
面试时间有限时,优先把主要矛盾讲清楚,不要一上来背一堆 Reflect API。
十六、完整示例:Proxy + Reflect 的正确用法
js
const target = {
firstName: '张',
lastName: '三',
// 访问器属性。
// getter 中通过 this 读取 firstName 和 lastName。
get fullName() {
return this.firstName + this.lastName;
}
};
const proxy = new Proxy(target, {
// 读取属性时触发。
get(target, key, receiver) {
console.log('读取属性:', key);
// 不直接写 target[key]。
// 把 receiver 传进去,确保 getter 中的 this 指向 proxy。
return Reflect.get(target, key, receiver);
},
// 修改属性时触发。
set(target, key, value, receiver) {
console.log('修改属性:', key, value);
// 使用 Reflect.set 保持标准的属性设置行为。
// receiver 通常就是当前 Proxy。
return Reflect.set(target, key, value, receiver);
}
});
// 第一次触发 get。
console.log(proxy.fullName);
// getter 内部执行:
// this.firstName
// this.lastName
//
// 因为 this 是 proxy,
// 所以这两个读取也会再次触发 Proxy 的 get。
proxy.firstName = '李';
核心执行链:
text
proxy.fullName
↓
Proxy.get
↓
Reflect.get(target, "fullName", proxy)
↓
执行 getter
↓
this = proxy
↓
this.firstName
↓
Proxy.get
↓
this.lastName
↓
Proxy.get
这就是 Reflect 在 Proxy 中最值得掌握的底层原理。
十七、面试中不要这样回答
❌:
Reflect 就是用来做反射的,可以操作对象属性,Vue 里面也用了。
问题:
text
太泛
↓
没有说清楚什么叫反射
↓
没有说清楚对象基本操作
↓
没有说清楚 Proxy + Reflect
❌:
Reflect 和 Object 差不多。
问题:
过度简化,而且无法解释
Reflect.get()的receiver。
❌:
Reflect.ownKeys 可以获取对象所有属性。
问题:
"所有属性"容易让面试官追问原型链。准确说法应该是所有自身属性键。
❌:
Vue 3 使用 Reflect 是因为 Reflect 可以解决 Proxy。
问题:
Proxy 本身并不依赖 Reflect;Reflect 主要是帮助 Proxy handler 正确执行默认对象语义,尤其是
receiver。
十八、满分答案
核心思路:Reflect 本质上是把 JavaScript 对象的底层操作以函数形式暴露出来,最典型的应用是与 Proxy 配合,使用 Reflect 保持对象原本的行为,尤其是正确传递 receiver。
第一,对象本身有一组 ECMAScript 规定的内部操作,例如 [[Get]]、[[Set]]、[[Delete]]、[[OwnPropertyKeys]] 等。我们平时写 obj.foo、obj.foo = value,本质上都是在间接触发这些内部操作。Reflect 就是把其中很多操作以 API 的形式提供出来。
第二,Reflect 和 Object 不能简单认为是一样的。比如 Object.keys() 只返回自身、可枚举的字符串属性,而 Reflect.ownKeys() 返回对象自身的全部属性键,包括不可枚举属性和 Symbol 属性。
第三,Reflect 在 Proxy 中特别重要。比如:
js
const proxy = new Proxy(target, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
}
});
这里第三个参数 receiver 很关键。它决定访问器属性中 this 的指向。传入 Proxy 后,getter 内部再访问 this.xxx 时,会继续经过 Proxy,从而保证依赖追踪、拦截等行为能够继续生效。Vue 3 的响应式系统中就大量采用这种模式。
所以面试时可以总结为:
text
Reflect
↓
提供对象底层操作的函数式 API
↓
Proxy 拦截
↓
自定义逻辑
↓
Reflect 执行默认对象行为
↓
保持正确的 this / receiver / Proxy 语义
一句话记忆:Proxy 负责拦截,Reflect 负责按照标准对象语义继续执行。
这版基本就是这道题面试时应该掌握的主线。规范层面上,Proxy 的 [[Get]] 明确携带 Receiver,而 Reflect API 对应执行对象内部操作,因此把两者结合起来理解,比单纯背 Reflect API 更重要。(TC39)