Proxy 代理
一、介绍
定义: 用于定义基本操作的自定义行为
本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(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)
// 例子
const proxy = new Proxy({}, {
get(target, proper(Key)) {
console.log('你的访问被我拦截到了')
return 1
},
set(target, properKey, properValue) {
console.log('你修改这个属性被我拦截到了')
}
})
参数
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
所有的观察者
参考文献
TODO: 待整理
ES6 Proxy拦截器详解
Proxy的原意是“拦截”,可以理解为对目标对象的访问和操作之前进行一次拦截。提供了这种机制,所以可以对目标对象进行修改和过滤的操作。
const proxy = new Proxy({}, {
get(target, proper(Key)) {
console.log('你的访问被我拦截到了')
return 1
},
set(target, properKey, properValue) {
console.log('你修改这个属性被我拦截到了')
}
})
Proxy 实际上重载了点运算符,即用自己的定义覆盖了语言的原始定义。 语法:
const proxy = new Proxy(target, hanlder)
new Proxy生成一个 proxy的实例, target表示要拦截的目标,可以是对象或函数等。凡是对目标对象的一些操作都会经过拦截器的拦截处理。 hanlder 参数也是一个对象,它表示拦截配置,就如上例所示。Proxy 实例也可以作为其他对象的原型对象。对这个目标对象进行操作,如果它自身没有设置这个属性,就会去它的原型对象上面寻找,从而出发拦截行为。如下例:
const proxy = new Proxy({}, {
get(target, key) {
consoloe.log(`你访问的属性是${key}`)
}
})
const newObject = Object.create(proxy)
newObject.a // 你访问的属性是a
提醒 同一个拦截器可以拦截多个操作,只需要在第二个参数(hanlder)配置添加 如果对这个目标对象没有设置拦截行为,则直接落在目标对象上。
Proxy 支持的拦截操作 get(target, propKey, receiver) 拦截对象属性读取 set(target, propKey, value, receiver) 拦截对象的属性设置 has(target, propKey) 拦截propkey in proxy deleteProperty(target, propKey) 拦截delete proxy[propKey] ownKeys(target) getOwnPropertyDescriptor(target, propKey) 返回对象属性的描述对象拦截 defineProperty(target, propKey, propDesc) proventExtensions(target) getPrototypeOf(target) isExtensible(target) setPrototypeOf(target, proto) apply(target, object, args) construct(target, args) 拦截 proxy 实例作为构造函数调用的操作
Proxy 实例的方法 get(target, key): 当访问目标对象属性的时候,会被拦截。 target: 目标对象 key: 访问的key值
const proxy = new Proxy({a:1,b:2}, {
get(target, key) {
console.log('called')
return target[key]
}
})
proxy.a // 1
// called 会被打印出来
上面的代码中,当读取代理对象属性的时候,会被get方法拦截。所以可以在拦截前做一些事情,比如必须访问这个对象存在的属性,如果访问对象不存在的属性就抛出错误! 如下例:
const obj = {
name: 'qiqingfu',
age: 21
}
const proxy = new Proxy(obj, {
get(target, key) {
if (key in target) {
return target[key]
} else {
throw Error(`${key}属性不存在`)
}
}
})
以上代码读取代理对象的属性,如果存在就正常读取,负责提示错误访问的key值不存在。 如果一个属性不可配置(configurable), 或者不可写(writeble),则该属性不能被代理
const obj = Object.defineProperties({}, {
foo: {
value: 'a',
writeble: false, // 不可写
configurable: false, //不可配置
}
})
const proxy = new Proxy(obj, {
get(target, key) {
return 'qiqingfu'
}
})
proxy.value // 报错
场景例子: 通过get()方法可以实现一个函数的链式操作
const pipe = (function(){
return function (value) {
const funcStack = []; // 存放函数的数组
const proxy = new Proxy({}, {
get(target, fnName) {
if (fnName === 'get') {
return funcStack.reduce((val, nextfn) => {
return fn(val)
}, value)
}
funcStack.push(window[fnName])
return proxy //返回一个proxy对象,以便链式操作
}
})
return proxy
}
}())
var add = x => x * 2;
var math = y => y + 10;
pipe(3).add.math.get // 16
set(target, key, value)方法用于拦截某个属性的赋值操作 target: 目标对象 key: 要设置的key值 value: 设置的value值 返回值: Boolean 假如有一个prosen对象,要设置的值不能小于100,那么就可以使用 set方法拦截。
const prosen = {
a: 101,
b: 46,
c: 200
}
const proxy = new Proxy(prosen, {
set(target, key, value) {
if (value < 100) {
throw Error(`${value}值不能小于100`)
}
target[key] = value
}
})
上面代码对prosen对象赋值,我们可以拦截判断它赋值如果小于100就给它提示错误。 使用场景 可以实现数据绑定,即数据发生变化时,我们可以拦截到,实时的更新DOM元素。 还可以设置对象的内部数据不可被修改,表示这些属性不能被外部访问和修改,这是可以使用get和set, 如下例 规定对象的内部属性以_开头的属性不能进行读写操作。
const obj = {
name: 'qiqingfu',
age: 21,
_money: -100000,
_father: 'xxx'
}
function isSeal(key) {
if (key.charAl(0) === '_') {
return true
}
return false
}
const proxy = new Proxy(obj, {
get(target, key) {
if (isSeal(key)) {
throw Error(`${key},为内部属性,不可以读取`)
}
return target[key]
},
set(target, key, value) {
if (isSeal(key)) {
throw Error(`${key},为内部属性,不可以修改`)
}
target[key] = value
return true
}
})
以上代码obj对象设置了内部属性,以_开头的不支持读写。那么可以使用Proxy对其进行拦截判断。get和set中的key属性如果是以_开头的属性就提示错误。 set方法修改完值后,返回的是一个布尔值。 true成功,反则false为修改失败
apply(target, context, args) 方法可以拦截函数的调用,call()、apply() target: 目标对象, context: 目标对象的上下文对象 args: 函数调用时的参数数组
const proxy = new Proxy(function(){}, {
apply(target, context, args) {
console.log(target, 'target')
console.log(context, 'context')
console.log(args, 'args')
}
})
const obj = {
a: 1
}
proxy.call(obj,1,2,3)
上面的代码是拦截一个函数的执行,分别打印: target -> function(){}: 目标对象 context -> {a: 1}: 目标对象的上下文对象,也就是函数的调用者,这里我们使用call,让obj对象来调用这个函数。 args -> [1,2,3]: 目标对象函数调用时我们传递的参数,这里会以数组的形式接受。 例子: 再说下面一个例子之前,先了解一下Reflect.apply(), 下面是 MDN 的解释 Reflect.apply() 通过指定的参数列表发起对目标(target)函数的调用。 语法: Reflect.apply(target, context, args) target: 目标函数 context: 目标函数执行的上下文 args: 函数调用时传入的实参列表,该列表应该是一个类数组的对象 该方法和ES5的 function.prototype.apply() 方法类似。 下面对 sum 函数的调用进行拦截,并且将函数的执行结果 *2
const sum = (num1, num2) => {
return num1 + num2
}
const proxy = new Proxy(sum, {
apply(target, context, args) {
// 我们可以通过 Reflect.apply()来调用目标函数
return Reflect.apply(...arguments)* 2
}
})
proxy(3,4) // 14
以上代码是对 sum函数进行代理,并且将其执行结果 * 2
has(target, key ) 方法即拦截 hasProperty操作, 判断对象是否具有某个属性时,这个方法会生效 target: 目标对象, key: 对象的属性 返回值是一个布尔值 如果原对象不可配置或者禁止扩展, 那么has拦截会报错。 for in循环虽然也有 in 操作符,但是has对 for in 循环不生效. has在什么情况下会进行拦截: 属性查询: 例如 foo in window 继承属性查询: foo in Object.create(proxy) with检查: with(proxy) {} Reflect.has() 例1: 使用 has方法隐藏属性,使其不被 in 操作符发现。 就比如说对象以_开头的属性不能被发现。
const prosen = {
name: 'qiqingfu',
_age: 21
}
const proxy = new Proxy(prosen, {
has(target, key) {
if (key.chatAt(0) === '_') {
return false
}
return key in target
}
})
例2: with检查 with的定义总结 在with语句块中,只是改变了对变量的遍历顺序,由原本的从执行环境开始变为从with语句的对象开始。当尝试在with语句块中修改变量时,会搜索with语句的对象是否有该变量,有就改变对象的值,没有就创建,但是创建的变量依然属于with语句块所在的执行环境,并不属于with对象。 离开with语句块后,遍历顺序就会再次变成从执行环境开始。 with语句接收的对象会添加到作用域链的前端并在代码执行完之后移除。 关于js with语句的一些理解
let a = 'global a'
const obj = {
a: 1,
b: 2
}
const fn = key => {
console.log(key)
}
const proxy = new Proxy(obj, {
has(target, key) {
console.log(target, 'target')
console.log(key, 'key')
}
})
with(proxy) {
fn('a')
}
//依此打印
// {a: 1, b: 2} target
// fn key
// a
以上代码是对obj对象进行代理, 通过with检查, 访问代理对象的 a 属性会被 has方法拦截。那么拦截的第一个target就是目标对象, 而第二个参数key是访问 a时的with语句块所在的执行环境
construct(target, args) 方法用于拦截 new 命令。 target: 目标函数, args: 构造函数的参数对象 返回值必须是一个 对象, 否则会报错。
const proxy = new Proxy(function() {}, {
construct(target, args) {
console.log(target, 'target')
console.log(args, 'args')
return new target(args)
}
})
new proxy(1,2)
// function() {} 'target'
// [1,2] 'args'
如果返回值不是对象会报错
deleteProperty(target, key) 拦截对象的 delete操作 target: 目标对象 key: 删除的哪个key值 返回值: 布尔值, true成功,false失败 目标对象不可配置(configurable)属性不能被deleteProperty删除, 否则会报错
const obj = Object.defineProperties({}, {
a: {
value: 1,
configurable: false,
},
b: {
value: 2,
configurable: true
}
})
const proxy = new Proxy(obj, {
deleteProperty(target, key) {
delete target[key]
return true;
}
})
delete proxy.a // 报错
delete proxy.b // true
以上代码拦截 obj对象, 当进行删除不可配置的属性a时,会报错。删除b属性时则成功。 应用场景: 我们可以指定内置属性不可被删除。如以_开头的属性不能被删除
const obj = {
_a: 'a',
_b: 'b',
c: 'c'
}
const proxy = new Proxy(obj, {
deleteProperty(target, key) {
if (key.charAt(0) === '_') {
throw Error(`${key}属性不可被删除`)
return false
}
delete target[key]
return true
}
})
defindeProperty(target, key, descriptor)方法拦截Object.defindProperty()操作 target: 目标对象, key: 目标对象的属性 descriptor: 要设置的描述对象 返回值: 布尔值, true添加属性成功, false则会报错v
const proxy = new Proxy({}, {
defineProperty(target, key, descriptor) {
console.log(target, 'target')
console.log(key, 'key')
console.log(descriptor, 'descriptor')
return true
}
})
Object.defineProperty(proxy, 'a', {
value: 1
})
以上代码是拦截一个对象的Object.defindProperty()添加属性的操作, 如果返回值为true,表示添加成功。返回值false则会报错。 以上代码的执行结果:
getPrototypeOf(target) 方法,用来拦截获取对象原型。 target: 代理对象 可以拦截一下获取原型的操作: Object.prototype. proto Object.prototype.isPrototypeOf() Object.getPrototypeOf() 获取一个对象的原型对象 instance 操作符 Object.prototype.isPrototypeOf() 方法 检测一个对象的原型链上有没有这个对象 语法: Objectactive.isPrototypeOf(object), 检测object对象的原型链上有没有Objectactive这个对象, 如果有返回true, 否则返回false
const Objectactive = {a: 1}
const object = Object.create(Objectactive)
Objectactive.isPrototypeOf(object) // true
以上代码 Objectactive作为 object的原型对象,然后通过 isPrototypeOf 检测object对象的原型链上有没有Objectactive这个对象。 理所当然返回 true 使用 getPrototypeOf()拦截
const Objectactive = {a: 1}
const object = Object.create(Objectactive)
const proxy = new Proxy(object, {
getPrototypeOf(target) {
console.log(target, 'target')
return Object.getPrototypeOf(target)
}
})
let bl = Objectactive.isPrototypeOf(proxy)
console.log(bl)
// 依此打印结果:
/*
{
__proto__:
a: 1,
__proto__: Object
} 'target'
true
*/
以上代码对 object对象进行代理,当访问原型对象时,通过getPrototypeOf()方法拦截,target就是代理对象 getPrototypeOf()方法的返回值必须是 null 或者对象,否则报错。
isExtensible(target) 方法拦截 Object.isExtensible()方法 Object.isExtensible() 方法返回一个布尔值,其检查一个对象是否可扩展。 target: 目标对象 isExtensible()方法有一个强限制,它的返回值必须与目标对象的 isExtensible属性保持一致。
const testObj = {
name: 'apy'
}
const proxy = new Proxy(testObj, {
isExtensible(target) {
console.log('拦截对象的isExtensible操作')
return true; // 这里要返回true, 因为目标对象现在是可扩展的,如果返回 false会报错
}
})
console.log(Object.isExtensible(proxy))
// 打印:
// 拦截对象的isExtensible操作
// true
以上代码通过Object.isExtensible()检测一个对象是否可扩展,会被配置选项中的 isExtensible方法拦截。 那么什么情况下可以 return false 呢 Object.preventExtensions(object): 将一个对象设置为不可扩展的
const testObj = {
name: 'apy'
}
Object.preventExtensions(testObj) // 将 testObj对象设置为不可扩展
const proxy = new Proxy(testObj, {
isExtensible(target) {
console.log('拦截对象的isExtensible操作')
return false // 因为testObj对象不可扩展,返回值要和目标对象的 Object.isExtensible一致。
}
})
Object.isExtensible(testObj)
以上代码通过 proxy拦截对象的 Object.isExtensible方法, 并且拦截的返回值与Object.isExtensible一致。否则报错
ownKeys(target)方法用于拦截对象自身的属性读取操作 target: 目标对象 返回值: Array<String, Symbol>, 返回值为数组,且数组中只能包含字符串或Symbol类型的 会被 ownKeys 拦截的读取操作 Object.getOwnPropertyNames() Object.getOwnPropertySymbols() Object.keys() 使用Object.keys 方法时,有三类属性会被 ownKeys 过滤掉,并不会返回. 目标对象 target 上压根不存在的属性 属性名为 Symbol 还有就是目标对象上不可遍历的属性
const obj = {
a: 1,
b: 2,
[Symbol.for('c')]: 3
}
Object.defineProperty(obj, 'd', {
value: 4,
enumerable: false
})
const proxy = new Proxy(obj, {
ownKeys(target) {
return ['a', 'b', [Symbol('c')], 'd', 'e']
}
})
Object.keys(obj).forEach(key => {
console.log(key)
})
// a
// b
以上代码定义了一个 obj对象, 有其属性a, b, [Symbol],d。并且d属性是不可扩展的。那么 ownKeys方法显式返回 不可遍历的属性(d)、Symbol和不存在的属性e都会被过滤掉,那么最终返回a和b 注意: 如果目标对象包含不可配置(configurable)的属性,那么该属性必须被 ownkeys方法返回。 如果目标对象是不可扩展(preventExtensions)的对象,那么 ownkeys返回必须返回这个对象的原有属性,不能包含额外的属性。
setPrototypeOf(target, proto) 方法拦截 Object.setPrototypeOf方法 target: 目标对象 proto: 要设置的原型对象 返回值 布尔值 设置一个对象的原型对象操作,会被 setPrototypeOf拦截。
const obj = {}
const proxy = new Proxy(obj, {
setPrototypeOf(target, proto) {
console.log('拦截设置原型操作')
// 内部手动设置原型,并且返回 boolean
return Object.setPrototype(target, proto)
}
})
Object.setPrototypeOf(proxy, {a: 1})
以上代码拦截Object.setPrototypeOf方法,所以会打印 拦截设置原型操作 使用场景, 禁止修改一个对象的原型,否则报错 如上例子,拦截一个修改对象原型的操作,抛出相应的错误就可以。
const foo = {}
const proxy = new Proxy(foo, {
setPrototypeOf(target, key) {
throw Error(`${target}不可以修改原型对象`)
}
})
Object.setPrototypeOf(proxy, {a: 1}) // 报错