ES6 Proxy与Reflect:拦截并重构JavaScript内置行为

作者:闭包旅人

时间:2026-10-22

浏览:403

本文详解ES6中Proxy代理与Reflect反射API的核心机制。通过拦截get、set、apply等底层操作,开发者可自定义对象行为。文章涵盖属性验证、函数参数检查及类构造器重写等实战案例,展示如何精确控制JavaScript引擎的内置行为。

文章目录

代理与反射的基本概念

Proxy(代理)能够拦截并修改 JavaScript 引擎的底层操作,涵盖数据读取、属性定义及函数构造等多个环节。ES6 借助这些底层内置对象的代理陷阱(Trap)与反射函数,赋予开发者更贴近引擎核心能力的控制力。

一、代理与反射的基本概念

如何理解代理与反射?

Proxy 用于替代另一个目标对象(target)。通过 new Proxy() 创建的代理,在表面上可与目标对象视为同一实体进行交互。

当对目标对象执行特定的底层操作时,代理允许拦截这些操作并进行覆写,这原本是 JavaScript 引擎的内部权限。

若对代理与反射的概念感到困惑,可直接参考后续的应用示例,阅读完后再回看定义,理解会更加清晰。

拦截行为依赖于响应特定操作的函数(称为陷阱),每个代理陷阱都对应一个 Reflect 方法。

ES6 的反射 API 以 Reflect 对象形式存在,其每个方法名与对应的陷阱函数一致,且接收参数也相同。以下是 Reflect 对象的部分方法:

代理陷阱 覆写的特性 方法
get 读取一个属性的值 Reflect.get()
set 写入一个属性 Reflect.set()
has in 运算符 Reflect.has()
deleteProperty delete 运算符 Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
defineProperty Object.defineProperty() Reflect.defineProperty
apply 调用一个函数 Reflect.apply()
construct 使用 new 调用一个函数 Reflect.construct()

每个陷阱函数均可重写 JavaScript 对象的特定内置行为,从而实现拦截与修改。

综上,若要控制或改变 JavaScript 的底层操作,需先创建代理对象并挂载陷阱函数,陷阱函数内部调用反射方法。结合后续应用示例,可更直观地理解代理过程。

创建简单的代理示例

二、开始一个简单的代理

使用 Proxy 构造器创建代理时,需传递两个参数:目标对象(target)与处理器(handler)。

以下示例创建一个仅进行透传的代理:


// 目标对象
let target = {}; 
// 代理对象
let proxy = new Proxy(target, {});

proxy.name = "hello";
console.log(proxy.name); // "hello"
console.log(target.name); // "hello"

target.name = "world";
console.log(proxy.name); // "world"
console.log(target.name); // "world

上述代码中,proxy 代理对象将所有操作直接传递给 target 目标对象。代理自身不存储属性,仅将值传递给 target,因此 proxy.name 与 target.name 的值始终相等,因为它们均指向 target.name。

此时代理陷阱的处理器为空对象,但处理器可定义一个或多个陷阱函数。

set 验证对象属性存储

2.1 set 验证对象属性的存储

假设需创建一个对象,要求其属性值仅限数值类型,这意味着该对象的每个新增属性

均需经过验证,若属性值非数值类型,则应抛出错误。

此时需使用 set 陷阱函数拦截传入的 value,该陷阱函数接受四个参数:

  • trapTarget:接收属性的对象(即代理的目标对象)
  • key:需写入的属性的键(字符串类型或符号类型)
  • value:将被写入属性的值;
  • receiver:操作发生的对象(通常为代理对象)

set 陷阱对应的反射方法及默认特性为 Reflect.set(),与陷阱函数一样接收这四个参数,并根据操作是否成功返回相应结果:


let targetObj = {};
let proxyObj = new Proxy(targetObj, {
 set: set
});

/* 定义 set 陷阱函数 */
function set (trapTarget, key, value, receiver) {
 if (isNaN(value)) {
   throw new TypeError("Property " + key + " must be a number.");
 }
 return Reflect.set(trapTarget, key, value, receiver);
}

/* 测试 */
proxyObj.count = 123;
console.log(proxyObj.count); // 123
console.log(targetObj.count); // 123

proxyObj.anotherName = "proxy" // TypeError: Property anotherName must be a number.

示例中 set 陷阱函数成功拦截传入的 value 值,读者可尝试注释或不 return Reflect.set() 的情况,答案是拦截陷阱将无反射响应。

需注意,直接给 targetObj 目标对象赋值时不会触发 set 代理陷阱,仅通过给代理对象赋值才会触发 set 代理陷阱与反射。

get 验证对象属性读取

2.2 get 验证对象属性的读取

JavaScript 的一个有趣特性是,读取不存在的属性时不会抛出错误,而是将 undefined 视为该属性的值。

在大型代码库中,属性名称书写错误(不抛错)会导致严重问题。此时使用 get 代理陷阱验证对象结构(Object Shape),访问不存在的属性时抛出错误,使对象结构验证变得简单。

get 陷阱函数在读取属性时被调用,即使该属性在对象中不存在,它接受三个参数:

  • trapTarget:将被读取属性的对象(即代理的目标对象)
  • key:需读取的属性的键(字符串类型或符号类型)
  • receiver:操作发生的对象(通常为代理对象)

Reflect.get() 方法接受相同参数,并返回默认属性的默认值。


let proxyObj = new Proxy(targetObj, {
 set: set,
 get: get
});

/* 定义 get 陷阱函数 */
function get(trapTarget, key, receiver) {
 if (!(key in receiver)) {
  throw new TypeError("Property " + key + " doesn't exist.");
 }
 return Reflect.get(trapTarget, key, receiver);
}

console.log(proxyObj.count); // 123
console.log(proxyObj.newcount) // TypeError: Property newcount doesn't exist.

这段代码允许添加新属性,且此后可正常读取该属性的值,但当读取的属性不

存在时,程序抛出错误,而非将其默认为 undefined。

还可使用 has 陷阱验证 in 运算符,使用 deleteProperty 陷阱函数防止属性被 delete 删除。 注:in 运算符用于判断对象中是否存在某个属性,若自有属性或原型属性匹配该名称字符串或 Symbol,则 in 运算符返回 true。


targetObj = {
 name: 'targetObject'
};
console.log("name" in targetObj); // true
console.log("toString" in targetObj); // true

其中 name 是对象自身的属性,而 toString 则是原型属性(从 Object 对象继承而来),因此检测结果均为 true。

has 陷阱函数在使用 in 运算符时被调用,并传入两个参数(同名反射 Reflect.has() 方法亦然):

  • trapTarget:需读取属性的对象(代理的目标对象)
  • key:需检查的属性的键(字符串类型或 Symbol 符号类型)

deleteProperty 陷阱函数在使用 delete 运算符删除对象属性时被调用,并传入两个参数(Reflect.deleteProperty() 方法也接受这两个参数):

  • trapTarget:需删除属性的对象(即代理的目标对象);
  • key:需删除的属性的键(字符串类型或符号类型)。

一些思考:分析过 Vue 源码者应了解,Vue 实例中挂载的 data 是通过 Object.defineProperty 代理 vm._data 中的对象属性,实现双向绑定……同理,可考虑使用 ES6 的 Proxy 的 get 和 set 陷阱实现此代理。

对象属性陷阱详解

三、对象属性陷阱

3.1 数据属性与访问器属性

ES5 最重要的特征之一是引入 Object.defineProperty() 方法定义属性的特性。属性特性是为实现 JavaScript 引擎所用,属内部值,因此无法直接访问。

属性分为数据属性和访问器属性。使用 Object.defineProperty() 方法修改数据属性特性值的示例如下:


let obj1 = {
 name: 'myobj',
}
/* 数据属性*/
Object.defineProperty(obj1,'name',{
 configurable: false, // default true
 writable: false,   // default true
 enumerable: true,  // default true
 value: 'jenny'    // default undefined
})
console.log(obj1.name) // 'jenny'

其中 [[Configurable]] 表示能否通过 delete 删除属性从而重新定义为访问器属性;[[Enumerable]] 表示能否通过 for-in 循环返回属性;[[Writable]] 表示能否修改属性的值;[[Value]] 包含该属性的数据值。

对于访问器属性,该属性不包含数据值,包含一对 getter 和 setter 函数,定义访问器属性必须使用 Object.defineProperty() 方法:


let obj2 = {
 age: 18
}
/* 访问器属性 */
Object.defineProperty(obj2,'_age',{
 configurable: false, // default true
 enumerable: false,  // default true
 get () {       // default undefined
  return this.age
 },
 set (num) {     // default undefined
  this.age = num
 }
})
/* 修改访问器属性调用 getter */
obj2._age = 20 
console.log(obj2.age) // 20

/* 输出访问器属性 */
console.log(Object.getOwnPropertyDescriptor(obj2,'_age')) 
// { get: [Function: get],
//  set: [Function: set],
//  enumerable: false,
//  configurable: false }

[[Get]] 在读取属性时调用的函数,[[Set]] 在写入属性时调用的函数。使用访问器属性的常用方式,是设置一个属性的值导致其他属性发生变化。

3.2 检查属性的修改

代理允许使用 defineProperty 同名函数陷阱函数拦截 Object.defineProperty() 的调用,defineProperty 陷阱函数接受下列三个参数:

  • trapTarget:需被定义属性的对象(即代理的目标对象);
  • key:属性的键(字符串类型或符号类型);
  • descriptor:为该属性准备的描述符对象。

defineProperty 陷阱函数要求在操作后返回一个布尔值以判断操作是否成功,若返回 false 则抛出错误,故可使用该功能来限制哪些属性可被 Object.defineProperty() 方法定义。

例如,若想阻止定义 Symbol 符号类型的属性,可检查传入的属性值,若是则返回 false:


/* 定义代理 */
let proxy = new Proxy({}, {
 defineProperty(trapTarget, key, descriptor) {
  if (typeof key === "symbol") {
   return false;
  }
  return Reflect.defineProperty(trapTarget, key, descriptor);
 }
});

Object.defineProperty(proxy, "name", {
 value: "proxy"
});
console.log(proxy.name); // "proxy"

let nameSymbol = Symbol("name");
// 抛出错误
Object.defineProperty(proxy, nameSymbol, {
 value: "proxy"
})

四、函数代理

函数代理的应用

4.1 构造函数 & 立即执行

函数的两个内部方法:[[Call]] 与 [[Construct]] 会在函数被调用时执行,通过代理函数为这两个内部方法设置陷阱,从而控制函数的行为。

[[Construct]] 会在函数被使用 new 运算符调用时执行,代理触发 construct() 陷阱函数,并与 Reflect.construct() 一样接收下列两个参数:

  • trapTarget:被执行的函数(即代理的目标对象);
  • argumentsList:被传递给函数的参数数组。

[[Call]] 会在函数被直接调用时执行,代理触发 apply() 陷阱函数,它与 Reflect.apply() 均接收三个参数:

  • trapTarget:被执行的函数(代理的目标函数);
  • thisArg:调用过程中函数内部的 this 值;
  • argumentsList:被传递给函数的参数数组。

每个函数都包含 call() 和 apply() 方法,用于重置函数运行的作用域即 this 指向,区别仅在于接收参数的方式不同:call() 的参数需逐个列举,apply() 是参数数组。

显然,apply 与 construct 要求代理目标对象必须是一个函数,这两个代理陷阱在函数的执行方式上开启了许多可能性,结合使用即可完全控制任意代理目标函数的行为。

验证函数参数

4.2 验证函数的参数

观察到 apply() 和 construct() 陷阱的参数中均有被传递给函数的参数数组 argumentsList,因此可用于验证函数的参数。

例如需保证所有参数均为特定类型,且不能通过 new 构造使用,示例如下:


/* 定义 sum 目标函数 */
function sum(...values) {
 return values.reduce((previous, current) => previous + current, 0);
}
/* 定义 apply 陷阱函数 */
function applyRef (trapTarget, thisArg, argumentList) {
 argumentList.forEach((arg) => {
  if (typeof arg !== "number") {
   throw new TypeError("All arguments must be numbers.");
  }
 });
 return Reflect.apply(trapTarget, thisArg, argumentList);
}
/* 定义 construct 陷阱函数 */
function constructRef () {
 throw new TypeError("This function can't be called with new.");
}
/* 定义 sumProxy 代理函数 */
let sumProxy = new Proxy(sum, {
 apply: applyRef,
 construct: constructRef
});

console.log(sumProxy(1, 2, 3, 4)); // 10

// console.log(sumProxy(1, "2", 3, 4)); // TypeError: All arguments must be numbers.
// let result = new sumProxy() // TypeError: This function can't be called with new.

sum() 函数会将所有传递进来的参数值相加,此代码通过将 sum() 函数封装在 sumProxy() 代理中,若传入参数的值非数值类型,该函数仍会尝试加法操作,但在函数运行之前拦截了函数调用,触发 apply 陷阱函数以保证每个参数均为数值。

出于安全考虑,这段代码使用 construct 陷阱抛出错误,以确保该函数不会被使用 new 运算符调用

实例对象 instance 对象会被同时判定为 proxy 与 target 对象的实例,是因为 instanceof 运算符使用了原型链来进行推断,而原型链查找并未受此代理影响,因此 proxy 对象与 target 对象对于 JavaScript 引擎而言具有相同的原型。

调用类的构造函数

4.3 调用类的构造函数

ES6 中新引入了 class 类的概念,类使用 constructor 构造函数封装数据,并规定必须始终使用 new 来调用,原因是类构造器的内部方法 [[Call]] 被明确要求抛出错误。

代理可拦截对于 [[Call]] 方法的调用,你可借助代理调用的类构造器。例如在缺少 new 的情况下创建一个新实例,就使用 apply 陷阱函数实现:


class Person {
 constructor(name) {
  this.name = name;
 }
}
let PersonProxy = new Proxy(Person, {
 apply: function(trapTarget, thisArg, argumentList) {
  return new trapTarget(...argumentList);
 }
});
let me = PersonProxy("Jenny");
console.log(me.name); // "Jenny"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true

类构造器即类的构造函数,使用代理时其行为如同函数,apply 陷阱函数重写了默认的构造行为。

关于类的更多有趣用法,可参考 【ES6】更易于继承的类语法

总结而言,代理的用途极为广泛,因其提供了修改 JavaScript 内置对象所有行为的入口。上述示例仅为简单应用入门,还有更多复杂示例,推荐阅读《深入理解 ES6》。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章