JavaScript Proxy 实战:从基础语法到高级应用场景

作者:代码拾荒

时间:2026-10-23

浏览:309

本文深入解析 ES6 提供的 Proxy API,介绍其作为对象代理的核心概念与基础语法。详细阐述 get、set、apply 等常用 Trap 的行为机制,并通过解决深层对象属性访问、兼容构造函数调用、封装 Fetch 请求及实现断言工具等实际案例,展示 Proxy 在前端开发中的强大功能与灵活应用。

文章目录

Proxy 概念与基础语法

Proxy是什么

首先,我们需要明确 Proxy 的含义,该词在中文里意为“代理”。

可以将其类比为某位知名明星开设的活跃微博账号,该账号频繁回复粉丝、点赞互动,但实际运营者并非明星本人。

幕后由其他人员或团队负责运营,他们发布的微博内容即代表明星本人的意愿,因此这些人可被称为“代理人”。

P.S. 此处仅为强行举例,因作者本人不追星,仅基于猜测可能存在此类运营团队。

将此概念映射到 JavaScript 中,Proxy 即用于对对象或函数进行代理操作的机制。

JavaScript中的Proxy

Proxy 是 ES6 标准引入的新 API,用于定义对象基本操作的自定义行为(文档中称为 traps,可理解为针对对象各类行为的钩子)。利用它可以实现许多有趣的功能,在需要控制对象行为时尤为有效。

Proxy的语法

创建 Proxy 实例需传入两个参数。

1. target:被代理的对象,可以是 object 或 function。

2. handlers:处理对该代理对象的各种操作行为。


let target = {}
let handlers = {} // do nothing
let proxy = new Proxy(target, handlers)
proxy.a = 123
console.log(target.a) // 123

当第二个参数为空对象时,基本可视为对第一个参数执行了一次浅拷贝(注:Proxy 必须是浅拷贝,若为深拷贝则失去代理意义)。

Traps(各种行为的代理)

如上述示例代码所示,若未定义对应的 trap,则不会产生任何作用,相当于直接操作 target。

当定义了特定 trap 后,执行对应动作时会触发回调函数,由我们控制被代理对象的行为。

最常用的两个 trap 应为 get 和 set。

早年 JavaScript 允许在定义对象时为特定属性设置 getter、setter:


let obj = {
_age: 18,
get age () {
return `I'm ${this._age} years old`
},
set age (val) {
this._age = Number(val)
}
}
console.log(obj.age) // I'm 18 years old
obj.age = 19
console.log(obj.age) // I'm 19 years old

如这段代码所示,我们设置了属性 _age,并定义了 get age 和 set age。

随后可直接调用 obj.age 获取返回值,或对其进行赋值。

这种做法存在几个缺点:

1. 需为每个要代理的属性单独编写对应的 getter、setter。

2. 必须存在一个存储真实值的 key(若直接在 getter 中调用 this.age 会导致堆栈溢出,因为每次调用 this.age 取值都会触发 getter)。

Proxy 很好地解决了这两个问题:


let target = { age: 18, name: 'Niko Bellic' }
let handlers = {
get (target, property) {
return `${property}: ${target[property]}`
},
set (target, property, value) {
target[property] = value
}
}
let proxy = new Proxy(target, handlers)
proxy.age = 19
console.log(target.age, proxy.age) // 19, age : 19
console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic

我们通过创建 get、set 两个 trap 来统一管理所有操作。可见,修改 proxy 时 target 内容同步修改,且我们对 proxy 的行为进行了特殊处理。

此外,无需额外使用 key 存储真实值,因为 trap 内部操作的是 target 对象而非 proxy 对象。

Proxy 的实际应用场景

拿Proxy来做些什么

由于使用 Proxy 后对象行为基本可控,因此能实现之前较复杂的功能。

下文列出几个简单的适用场景。

解决对象属性为undefined的问题

在获取层级较深的对象属性时,处理 undefined 一直较为繁琐,利用 Proxy 可很好地兼容此情况。


(() => {
let target = {}
let handlers = {
get: (target, property) => {
target[property] = (property in target) ? target[property] : {}
if (typeof target[property] === 'object') {
return new Proxy(target[property], handlers)
}
return target[property]
}
}
let proxy = new Proxy(target, handlers)
console.log('z' in proxy.x.y) // false (其实这一步已经针对`target`创建了一个x.y的属性)
proxy.x.y.z = 'hello'
console.log('z' in proxy.x.y) // true
console.log(target.x.y.z) // hello
})()

我们代理了 get,并在其中进行逻辑处理:若获取的值来自不存在的 key,则在 target 中创建对应 key,并返回针对该 key 的代理对象。

这样可保证取值操作不会抛出 can not get xxx from undefined 错误。但这有一个小缺点:若需判断 key 是否存在,只能通过 in 操作符判断,而不能直接通过 get 判断。

普通函数与构造函数的兼容处理

若向他人提供 Class 对象或 ES5 版本的构造函数,未使用 new 关键字调用时,Class 对象会直接抛出异常,而 ES5 构造函数 this 指向则变为调用函数时的作用域。我们可以使用 apply 这个 trap 来兼容这种情况:


class Test {
constructor (a, b) {
console.log('constructor', a, b)
}
}
// Test(1, 2) // throw an error
let proxyClass = new Proxy(Test, {
apply (target, thisArg, argumentsList) {
// 如果想要禁止使用非new的方式来调用函数,直接抛出异常即可
// throw new Error(`Function ${target.name} cannot be invoked without 'new'`)
return new (target.bind(thisArg, ...argumentsList))()
}
})
proxyClass(1, 2) // constructor 1 2

我们使用 apply 代理部分行为,函数调用时触发。因明确代理的是 Class 或构造函数,故直接在 apply 中使用 new 关键字调用被代理函数。

若需限制函数禁止使用 new 关键字调用,可使用另一个 trap: construct。


function add (a, b) {
return a + b
}
let proxy = new Proxy(add, {
construct (target, argumentsList, newTarget) {
throw new Error(`Function ${target.name} cannot be invoked with 'new'`)
}
})
proxy(1, 2) // 3
new proxy(1, 2) // throw an error

用Proxy来包装fetch

前端发送请求时,目前常用的是 fetch,一个原生提供的 API。我们可以用 Proxy 包装它,使其更易用。


let handlers = {
get (target, property) {
if (!target.init) {
// 初始化对象
['GET', 'POST'].forEach(method => {
target[method] = (url, params = {}) => {
return fetch(url, {
headers: {
'content-type': 'application/json'
},
mode: 'cors',
credentials: 'same-origin',
method,
...params
}).then(response => response.json())
}
})
}
return target[property]
}
}
let API = new Proxy({}, handlers)
await API.GET('XXX')
await API.POST('XXX', {
body: JSON.stringify({name: 1})
})

对 GET、POST 进行了一层封装,可直接通过 .GET 方式调用,并设置一些通用参数。

实现一个简易的断言工具

写过测试的开发者应都了解断言概念。console.assert 是一个断言工具,接受两个参数,若第一个为 false,则将第二个参数作为 Error message 抛出。我们可以使用 Proxy 做一个直接赋值即可实现断言的工具。


let assert = new Proxy({}, {
set (target, message, value) {
if (!value) console.error(message)
}
})
assert['Isn\'t true'] = false // Error: Isn't true
assert['Less than 18'] = 18 >= 19 // Error: Less than 18

统计函数调用次数。在服务端开发中,可用 Proxy 代理某些函数,以统计一段时间内的调用次数,后期做性能分析时或许能派上用场:


function orginFunction () {}
let proxyFunction = new Proxy(orginFunction, {
apply (target, thisArg. argumentsList) {
log(XXX)
return target.apply(thisArg, argumentsList)
}
})

Proxy 完整 Trap 列表

全部的traps

这里列出了 handlers 所有可定义的行为 (traps):

具体细节可查看 MDN-Proxy。

其中同样包含一些示例。

traps description
get 获取某个key值
set 设置某个key值
has 使用in操作符判断某个key是否存在
apply 函数调用,仅在代理对象为function时有效
ownKeys 获取目标对象所有的key
construct 函数通过实例化调用,仅在代理对象为function时有效
isExtensible 判断对象是否可扩展,Object.isExtensible的代理
deleteProperty 删除一个property
defineProperty 定义一个新的property
getPrototypeOf 获取原型对象
setPrototypeOf 设置原型对象
preventExtensions 设置对象为不可扩展
getOwnPropertyDescriptor 获取一个自有属性 (不会去原型链查找) 的属性描述

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

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章