本文深入解析 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 | 获取一个自有属性 (不会去原型链查找) 的属性描述 |
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。