ES6中 Proxy 的?使用场景?
Proxy 是 js 中的原生对象,用来创建一个对象的代理,可以实现基本操作的拦截和自定义。
const p = new Proxy(target, handler)
- target 是要代理的对象
- handler 中定义了基本操作的逻辑
基本操作:
handler.has 对 in 操作符进行拦截
var obj = {
count: 1,
}
var p = new Proxy(obj, {
has: function (target, prop) {
var res = Reflect.has(target, prop)
console.log('Proxy.has', prop, target === obj)
return res
},
})
'count' in p // Proxy.has count true
var subP = Object.create(p)
'count' in subP // Proxy.has count true
Reflect.has(p, 'count') // Proxy.has count true
with (p) {
count // // Proxy.has count true
}
handler.get 对属性读取操作的拦截
var obj = {
count: 1,
}
var p = new Proxy(obj, {
get: function (target, property, receiver) {
if (property == 'count') {
var res = Reflect.get(target, property, receiver)
console.log('Proxy.get', target, property, receiver)
return res
}
return Reflect.get(target, property, receiver)
},
})
p.count // Proxy.get obj count p
p['count'] // Proxy.get obj count p
Reflect.get(p, 'count') // Proxy.get obj count p
var subP = Object.create(p)
subP['count'] // Proxy.get obj count subP
handler.set 对属性设置操作的拦截
var obj = {
count: 1,
}
var p = new Proxy(obj, {
set: function (target, property, value, receiver) {
if (property == 'count') {
console.log('Proxy.set', target, property, value, receiver)
var res = Reflect.set(target, property, value, receiver)
return res
}
return Reflect.set(target, property, receiver)
},
})
p.count = 2 // Proxy.set obj count 2 p
p['count'] = 3 // Proxy.set obj count 3 p
Reflect.set(p, 'count', 4) // Proxy.set obj count 4 p
var subP = Object.create(p)
subP['count'] = 5 // Proxy.set obj count 5 subP
subP['count'] = 6 // <无输出>
比较特殊的是 subP['count'] = 6 会无输出,对一个普通对象 subP 的未定义属性 count 赋值时,会执行到原型链上的 Proxy 的 set 拦截器,同时 subP 会有一个新增的属性 count。第二次向 subP 赋值时,由于已经存在属性 count,就不会访问到原型链上的 Proxy,也就不会执行 Proxy 中的 set 逻辑。
handler.apply 对函数调用的拦截
function fn() {}
var pfn = new Proxy(fn, {
apply: function (target, thisArg, argumentsList) {
console.log('Proxy.apply', target, thisArg, argumentsList)
var res = Reflect.apply(target, thisArg, argumentsList)
return res
},
})
pfn(1) // Proxy.apply fn undefined [1]
pfn.apply({}, [2]) // Proxy.apply fn {} [2]
pfn.call({}, 3) // Proxy.apply fn {} [3]
执行 pfn.apply({},[2]);时,会先执行.操作获取 apply 函数,所以 Proxy 上的 get 拦截器会先执行 handler.
handler.deleteProperty 对 delete 操作的拦截
handler.construct 对 new 操作的拦截
vue3 的响应性就是基于 Proxy 实现,vue3 响应性的特点是
- 当一个值被读取时进行追踪
- 当某个值改变时进行检测
- 重新运行代码来读取原始值
一、介绍
定义: 用于定义基本操作的自定义行为
本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)
元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
一段代码来理解
#!/bin/bash
# metaprogram
echo '#!/bin/bash' >program
for ((I=1; I<=1024; I++)) do
echo "echo $I" >>program
done
chmod +x program
这段程序每执行一次能帮我们生成一个名为program
的文件,文件内容为 1024 行echo
,如果我们手动来写 1024 行代码,效率显然低效
元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译
Proxy
亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
二、用法
Proxy
为 构造函数,用来生成 Proxy
实例
var proxy = new Proxy(target, handler)
参数
target
表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))handler
通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p
的行为
handler 解析
关于handler
拦截属性,有如下:
- get(target,propKey,receiver):拦截对象属性的读取
- set(target,propKey,value,receiver):拦截对象属性的设置
- has(target,propKey):拦截
propKey in proxy
的操作,返回一个布尔值 - deleteProperty(target,propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值 - ownKeys(target):拦截
Object.keys(proxy)
、for...in
等循环,返回一个数组 - getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象 - defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
,返回一个布尔值 - preventExtensions(target):拦截
Object.preventExtensions(proxy)
,返回一个布尔值 - getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy)
,返回一个对象 - isExtensible(target):拦截
Object.isExtensible(proxy)
,返回一个布尔值 - setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值 - apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
- construct(target, args):拦截 Proxy 实例作为构造函数调用的操作
Reflect
若需要在Proxy
内部调用对象的默认行为,建议使用Reflect
,其是ES6
中操作对象而提供的新 API
基本特点:
- 只要
Proxy
对象具有的代理方法,Reflect
对象全部具有,以静态方法的形式存在 - 修改某些
Object
方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false
) - 让
Object
操作都变成函数行为
下面我们介绍proxy
几种用法:
get()
get
接受三个参数,依次为目标对象、属性名和 proxy
实例本身,最后一个参数可选
var person = {
name: '张三',
}
var proxy = new Proxy(person, {
get: function (target, propKey) {
return Reflect.get(target, propKey)
},
})
proxy.name // "张三"
get
能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey)
if (index < 0) {
propKey = String(target.length + index)
}
return Reflect.get(target, propKey, receiver)
},
}
let target = []
target.push(...elements)
return new Proxy(target, handler)
}
let arr = createArray('a', 'b', 'c')
arr[-1] // c
注意:如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则会报错
const target = Object.defineProperties(
{},
{
foo: {
value: 123,
writable: false,
configurable: false,
},
}
)
const handler = {
get(target, propKey) {
return 'abc'
},
}
const proxy = new Proxy(target, handler)
proxy.foo
// TypeError: Invariant check failed
set()
set
方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy
实例本身
假定Person
对象有一个age
属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy
保证age
的属性值符合要求
let validator = {
set: function (obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer')
}
if (value > 200) {
throw new RangeError('The age seems invalid')
}
}
// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value
},
}
let person = new Proxy({}, validator)
person.age = 100
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错
如果目标对象自身的某个属性,不可写且不可配置,那么set
方法将不起作用
const obj = {}
Object.defineProperty(obj, 'foo', {
value: 'bar',
writable: false,
})
const handler = {
set: function (obj, prop, value, receiver) {
obj[prop] = 'baz'
},
}
const proxy = new Proxy(obj, handler)
proxy.foo = 'baz'
proxy.foo // "bar"
注意,严格模式下,set
代理如果没有返回true
,就会报错
'use strict'
const handler = {
set: function (obj, prop, value, receiver) {
obj[prop] = receiver
// 无论有没有下面这一行,都会报错
return false
},
}
const proxy = new Proxy({}, handler)
proxy.foo = 'bar'
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'
deleteProperty()
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除
var handler = {
deleteProperty(target, key) {
invariant(key, 'delete')
Reflect.deleteProperty(target, key)
return true
},
}
function invariant(key, action) {
if (key[0] === '_') {
throw new Error(`无法删除私有属性`)
}
}
var target = { _prop: 'foo' }
var proxy = new Proxy(target, handler)
delete proxy._prop
// Error: 无法删除私有属性
注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty
方法删除,否则报错
取消代理
Proxy.revocable(target, handler);
# 三、使用场景
Proxy
其功能非常类似于设计模式中的代理模式,常用功能如下:
- 拦截和监视外部对对象的访问
- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
使用 Proxy
保障数据类型的准确性
let numericDataStore = { count: 0, amount: 1234, total: 14 }
numericDataStore = new Proxy(numericDataStore, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error('属性只能是number类型')
}
return Reflect.set(target, key, value, proxy)
},
})
numericDataStore.count = 'foo'
// Error: 属性只能是number类型
numericDataStore.count = 333
// 赋值成功
声明了一个私有的 apiKey
,便于 api
这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey
let api = {
_apiKey: '123abc456def',
getUsers: function () {},
getUser: function (userId) {},
setUser: function (userId, config) {},
}
const RESTRICTED = ['_apiKey']
api = new Proxy(api, {
get(target, key, proxy) {
if (RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可访问.`)
}
return Reflect.get(target, key, proxy)
},
set(target, key, value, proxy) {
if (RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可修改`)
}
return Reflect.get(target, key, value, proxy)
},
})
console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都抛出错误
还能通过使用Proxy
实现观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行
observable
函数返回一个原始对象的 Proxy
代理,拦截赋值操作,触发充当观察者的各个函数
const queuedObservers = new Set()
const observe = (fn) => queuedObservers.add(fn)
const observable = (obj) => new Proxy(obj, { set })
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
queuedObservers.forEach((observer) => observer())
return result
}
观察者函数都放进Set
集合,当修改obj
的值,在会set
函数中拦截,自动执行Set
所有的观察者