Proxy 学习
朋友们,大家好!本期内容是《你应该掌握的 JavaScript 高阶技能》的第三期,内容主要是
Proxy 代理
,关于第一和第二期遗留内容typeScript 实战项目贪吃蛇和手写 Promise
会在后续继续更新!本期参考 JavaScript 高级程序设计(第四版) 和 阮一峰老师的《ECMAScript 6 标准入门》,特别鸣谢❤️!
ES6
新增的反射内容将于新一期讲解,本文不多赘述。
文章目录
- Proxy 学习
- 1. Proxy 概述
- 2. Proxy 使用
- 3. Proxy 支持的拦截方法操作
- 3.1 get(target, propKey, receiver)
- 3.2 set(target, propKey, value, receiver)
- 3.3 has(target, propKey)
- 3.4 deleteProperty(target, propKey)
- 3.5 ownKeys(target)
- 3.6 getOwnPropertyDescriptor(target, propKey)
- 3.7 defineProperty(target, propKey, descriptor)
- 3.8 preventExtensions(target)
- 3.9 getPrototypeOf(target)
- 3.10 isExtensible(target)
- 3.11 setPrototypeOf(target, proto)
- 3.12 apply()
- 3.13 construct(target, args)
- 4. 可撤销代理
- 5. 代理中的 this
- 6. 小结
1. Proxy 概述
-
Proxy (代理)可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。
-
Proxy 是一个构造函数,可以通过它生成一个 Proxy 实例。
-
跟 JavaScript 中的
Object.defineProperty
类似(也就是访问器属性,关注vue2.x
的朋友都知道,vue2.x 的底层响应式原理部分就是使用它来实现的,vue3.x
使用的是 Proxy + Reflect 来实现的数据代理) -
Proxy (代理) 用于创建一个对象的代理,可以在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
2. Proxy 使用
- 语法
new Proxy(target,handler)
- target : 目标对象,待操作对象(可以是普通对象,数组,函数,proxy 实例对象)
- handler : 一个通常以函数作为属性的对象,也可以是空对象,各属性中的函数分别定义了在执行各种操作时代理 Proxy 对象的行为。
- 生成的实例对象是针对 target 对象的“拦截器”。
let obj = {
name: 'zhangsan',
age: 20
}
let hander = {};
const p = new Proxy(obj, hander);
console.log(p);
- 上图示可知 proxy 对象中,包含了
Handler
属性和Target
属性和IsRevoked
属性,值分别是传入的 handler 以及 obj 和 false。 - isRevoked 表示是否可撤销,生成可撤销的 proxy 对象用
Proxy.revocable()
方法。 Proxy
还有一种使用方式:Proxy.revocable
的静态方法,具体细节在后续说明。
const target = {};
const {proxy, revoke} = Proxy.revocable(target, {
get (target, key, receiver) {
return 123;
}
})
console.log(proxy.getData); // 123
2.1 通过 Proxy 对象操作原对象
- 最简单就是空代理,也就是 handler 是个空对象,那么操作拦截器相当于直接操作目标对象 target。
- 要创建空代理,可以传入一个简单的对象字面量作为处理程序对象。
- 在代理对象 proxy 上执行的任何操作都会应用到目标对象 target 上。
//定义目标对象
let obj = {
name: 'zhangsan',
age: 20
}
//定义拦截器
let hander = {};
const p = new Proxy(obj, hander);
//读取属性 修改属性
console.log(p.name);//zhangsan
p.age = 18;
console.log(p.age);//18
console.log(obj.age);//18
p.address = 'xxx';
console.log(p.address);
console.log(p);
delete p.age;
console.log(p.age);//undefined
console.log(obj.age);//undefined
- 操作 proxy 实例对象致使原对象的属性也被修改
let o = {
name: 'lisi',
age: 20
};
let temp = o['name'];
Object.defineProperty(o,'name',{
get(){
console.log('name被读取了');
return temp;
},
set(newValue){
temp = newValue;
console.log('name被修改为' + newValue);
}
});
- 同一个拦截器函数,可以设置拦截多个操作。
let o = {
name: 'lisi',
age: 20
};
const proxy = new Proxy(o,{
get: function(target,propKey){
console.log(`${propKey}属性被读取`);
return target[propKey];
},
set: function(target,propKey,newValue){
console.log(`${propKey}属性被修改`);
target[propKey] = newValue;
}
});
//通过代理对象执行 get() / set() 操作时,就会触发定义的 get() / set() 拦截器,该操作可以通过多种形式触发并被 get() / set() 拦截器拦截,后续会讲。
//这些操作只要发生在代理对象上 proxy,就会触发 get() / set() s拦截器,注意在代理对象上执行相应的形式操作才会触发拦截器,在目标对象上执行仍会产生相应的行为。
- 上面代码中,handler 对象中有一个
get
方法,用来拦截对目标对象属性的访问请求。get
方法的两个参数分别是目标对象和所要访问的属性,set
方法的三个参数分别是目标对象、所要访问的属性和所设置的新值,用来拦截目标对象属性的设置。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 1;
}
});
let obj = Object.create(proxy);
/*
1.Object.create() 创建的新对象的原型指向接收的参数本身,
2.new Object() 创建的新对象的原型指向的是 Object 的prototype。
3.可以通过 Object.create(null) 创建一个没有原型的对象
4.new Object() 创建的对象是 Object 的实例,原型永远指向Object 的 prototype。
*/
console.log(obj);
console.log(obj.male);//1
- 上面代码中,proxy 对象是 obj 对象的原型,obj 对象本身并没有 male 属性,所以根据原型链,会在 proxy 对象上读取该属性,会被拦截。
3. Proxy 支持的拦截方法操作
- 拦截对象属性的读取。
- 拦截器参数
- target:目标对象
- propKey:引用的目标对象上的字符串键属性或符号键
- receiver:代理对象或继承代理对象的对象
- 拦截操作:
proxy.foo
proxy['foo']
Object.create(proxy)['foo']
target.propKey
是不可配置(configurable
:表示能否通过 delete 删除属性、能否修改属性的特性)且不可写;(writable
:表示能否修改属性的值,即值是可写的还是只读。),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
- 拦截器参数
- 利用 Proxy,可以将读取属性的操作(
get
),转变为执行某个函数,从而实现属性的链式操作
let pipe = function(value){
//pipe 类型是 proxy
let funcArr = [];
let p = new Proxy({},{
//拦截 pipe.foo 操作
get: function(target,propKey){
//拦截属性为 get
if(propKey === 'get'){
//循环执行 funArr 里面的方法
return funcArr.reduce(function (val,fn){
return fn(val);
},value);
}
//拦截属性不为 get 存储 [window] 下对应方法名的函数
funcArr.push(window[fnName]);
return p;
}
});
return p;
}
//不能是 let const
var double = n => n*2;
//double(3) //6
var pow = n => n*n;
//pow(5) //25
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
//reverseInt(1234) //4321
//把数字转换为字符串;把字符串分割成字符串数组;翻转数组;将数组作为字符串返回
console.log(pipe(4).double.pow.reverseInt.get);//46
- 下面是一个
get
方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。
const proxy = new Proxy({}, {
get: function(target, key, receiver) {
return receiver;
}
});
proxy.getReceiver === proxy // true
- 上面代码中,proxy 对象的
getReceiver
属性是由 proxy 对象提供的,所以receiver
指向 proxy 对象。
- 拦截对象属性的设置。
- 返回 true 表示成功,返回 false 表示失败,严格模式下会抛出 TypeError。
- 拦截器参数
- target:目标对象
- propKey:引用的目标对象上的字符串键属性或符号键
- value: 要赋给属性的值
- receiver:接收最初赋值的对象(一般情况下是 proxy 实例对象本身)
- 拦截操作:
proxy.foo = value
proxy['foo'] = value
Object.create(proxy)['foo'] = value
- 如果目标对象自身的某个属性
target.propKey
不可写,那么set
方法将不起作用。
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
return true;
}
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);
myObj.foo = 'bar';
myObj.foo === myObj // true
- 上面代码中,设置
myObj.foo
属性的值时,myObj
并没有foo
属性,因此引擎会到 myObj 的原型链去找 foo 属性。myObj 的原型对象 proxy 是一个 Proxy 实例,设置它的 foo 属性会触发 set 方法。这时,第四个参数receiver
就指向原始赋值行为所在的对象 myObj。
- 返回必须是布尔类型,非布尔类型会被转型为布尔类型。
- 拦截操作:
propKey in proxy
propKey in Object.create(proxy)
- 判断对象是否具有某个属性时,这个方法会生效。典型的操作就是
in
运算符- has 方法接收两个参数:target:目标对象,propKey:需要查询的属性名.
- has 方法拦截的是
HasProperty
操作而不是HasOwnProperty
操作,也就是说 has 方法不判断一个属性是本身的属性还是继承来的属性 - 如果原对象不可配置或者禁止扩展(
Object.preventExtensions()
),has 方法拦截会报错。
- 隐藏对象的某些属性不被 in 发现,比如以 $ 符号开头的属性
const target = { _data: 'bar', $data: 'baz', _el: '<div></div>', $el: '#app' };
const handler = {
has(target,propKey){
if (propKey[0] === '$') {
return false;
}
console.log(target[propKey]);
return propKey in target;
}
}
const p = new Proxy(target,handler);
for...in
循环也用到了in
运算符,但是 has 方法拦截对其不生效。
let stu1 = {name: '张三', gender: '男'};
let stu2 = {name: '李四', gender: '女'};
const handler = {
has(target, propKey) {
if (propKey === 'gender' && target[propKey] === '男') {
return false;
}
console.log(target[propKey]);
return propKey in target;
}
}
const p1 = new Proxy(stu1, handler);
const p2 = new Proxy(stu2, handler);
for (let k in p1) {
console.log(p1[k]);
}
for (let k in p2) {
console.log(p2[k]);
}
-
3.4 deleteProperty(target, propKey)
- 必须返回布尔值,表示属性是否被删除成功。如果这个方法抛出错误或者返回 false,当前属性就无法被
delete
命令删除。返回非布尔值会被转型为布尔值。 - 拦截
delete
操作delete proxy[propKey]
delete proxy.propKey
- 目标对象自身的不可配置(configurable)的属性,不能被
deleteProperty
方法删除,否则报错。
//invariant 恒常,不可变
function invariant(key,action){
if(key[0] === '$'){
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const handler = {
deleteProperty(target,propKey){
invariant(propKey,'delete');
delete target[propKey];
return true;
}
};
//deleteProperty 方法拦截了 delete 操作符
const target = {
$el:'#app',
_el:'<div></div>',
};
const p = new Proxy(target,handler);
- 用来拦截对象自身属性的读取操作
- 返回一个数组,数组成员只能是字符串或 Symbol 值。该方法返回目标对象所有自身的属性的属性名,而
Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - 拦截操作:
Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
Object.keys(proxy)
for...in
循环
// ownKeys() 方法拦截 Object.getOwnPropertyNames()
let p = new Proxy({}, {
ownKeys: function(target) {
return ['a', 'b', 'c'];
}
});
Object.getOwnPropertyNames(p);
// ['a','b','c']
let target = {
a: 1,
b: 2,
c: 3
};
let handler = {
ownKeys(target) {
return ['a'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy);// [ 'a' ]
- 上面代码拦截了对于 target 对象的
Object.keys()
操作,只返回a
、b
、c
三个属性之中的a
属性。
-
注意,使用
Object.keys()
方法时,有三类属性会被ownKeys()
方法自动过滤,不会返回。- 目标对象上不存在的属性
- 属性名为 Symbol 值
- 不可遍历(enumerable)的属性
-
ownKeys()
方法返回除字符串或 Symbol 其他类型的值,或返回的表示数组,会报错
let obj = {}
let p = new Proxy(obj,{
ownKeys: function(target) {
return [123,true,undefined,null,{},[]];
}
});
Object.getOwnPropertyNames(p);
// Uncaught TypeError: 123 is not a valid property name...
let target = {
a: 1,
b: 2,
c: 3,
[Symbol.for('secret')]: '4',
//Symbol.for(key) 根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。
};
Object.defineProperty(target, 'key', {
enumerable: false,
configurable: true,
writable: true,
value: 'static'
});
let handler = {
ownKeys(target) {
return ['a', 'd', Symbol.for('secret'), 'key'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// ['a']
-
上面代码中,ownKeys() 方法之中,显式返回不存在的属性(
d
)、Symbol 值(Symbol.for('secret')
)、不可枚举的属性(key
),结果都被自动过滤掉。 -
如果目标对象自身包含不可配置的属性,则该属性必须被 ownKeys() 方法返回,否则报错。
var obj = {};
Object.defineProperty(obj, 'a', {
configurable: false,//不可配置
enumerable: true,
value: 10 }
);
var p = new Proxy(obj, {
ownKeys: function(target) {
return ['b'];
}
});
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
-
上面代码中,
obj
对象的a
属性是不可配置的,这时 ownKeys() 方法返回的数组之中,必须包含a
,否则会报错 -
如果目标对象是不可扩展的(non-extensible),这时 ownKeys() 方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。
var obj = {
a: 1
};
Object.preventExtensions(obj);
var p = new Proxy(obj, {
ownKeys: function(target) {
return ['a', 'b'];
}
});
Object.getOwnPropertyNames(p);
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
- 上面代码中,
obj
对象是不可扩展的,这时 ownKeys() 方法返回的数组之中,包含了obj
对象的多余属性b
,所以导致了报错。
-
拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回必须时属性的描述对象,或者在属性不存在时返回 undefined。 -
Object.getOwnPropertyDescriptor(obj, prop)
获取指定对象指定的自有属性的属性描述符。 -
-
注意:
target.propKey
【存在/不存在】 和 【可配置/不可配置】 的情况。
const target = {
_el: '<div></div>',
$el: '#app'
}
const handler = {
getOwnPropertyDescriptor(target, PropKey) {
if (PropKey[0] === '$') {
return false;
}
return Object.getOwnPropertyDescriptor
(target, PropKey);
}
}
上面代码中,handler.getOwnPropertyDescriptor()
方法对于第一个字符为 $
的属性名会返回 undefined
。
- 拦截
Object.defineProperty(proxy, propKey, descriptor)
,必须返回一个布尔值,返回非布尔值会被转型为布尔值。 - 拦截器参数
- target:目标对象
- propKey:引用的目标对象上的字符串键属性或符号键
- descriptor: 包括可选的
enumerable、configurable、writable、value、get 和 set
定义的对象。
- 注意:
- 如果目标对象不可扩展,则无法定义属性。
- 如果目标对象的某个属性不可写或不可配置的,则
defineProperty()
方法不得改变这两个设置。
const handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
const target = {};
const p = new Proxy(target, handler);
- 上面代码中,
defineProperty()
方法内部没有任何操作,只返回false
,导致添加新属性总是无效。注意,这里的false
只是用来提示操作失败,本身并不能阻止添加新属性。
- 拦截
Object.preventExtensions(proxy)
,必须返回布尔值,表示 target 是否已经不可扩展,返回非布尔值会被转型成布尔值。 - 如果目标对象不可扩展时(即
Object.isExtensible(proxy)
为false
),proxy.preventExtensions
才能返回true
,否则会报错。
const p = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(p);
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
- 为了防止出现这个问题,通常要在
proxy.preventExtensions()
方法里面,调用一次Object.preventExtensions()
。
const p = new Proxy({}, {
preventExtensions: function(target) {
Object.preventExtensions(target);
//此时 Object.isExtensible(p) 为 false
return true;
}
});
Object.preventExtensions(p);
- 返回一个对象或 null。
- 拦截操作
Object.prototype.__proto__
Object.prototype.isPrototypeOf(proxy)
Object.getPrototypeOf(proxy)
Reflect.getPrototypeOf(proxy)
proxy instanceof Object
- 如果目标对象不可扩展,
Object.getPrototypeOf(proxy)
方法必须返回目标对象的原型对象(Object.getPrototypeof(target)
)。
const proto = {};
const p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true
- 拦截
Object.isExtensible(proxy)
,必须返回一个布尔值,表示 target 是否可扩展,返回非布尔值会被转型成布尔值。
const p = new Proxy({}, {
isExtensible: function(target) {
return true;
}
});
Object.isExtensible(p)
// true
target
可扩展,则处理程序必须返回true
,反之不可扩展,返回false
,也就是说它的返回值必须与目标对象的isExtensible
属性保持一致,否则就会抛出错误
// Object.isExtensible(proxy) === Object.isExtensible(target)
const p = new Proxy({}, {
isExtensible: function(target) {
return false;
}
});
Object.isExtensible(p)
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
- 拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值,表示原型赋值是否成功,返回非布尔值会被转型成布尔值。 - 参数:
- target:目标对象
- proto:target 的替代原型,如果是顶级原型则为 null
- 如果目标对象不可扩展,
setPrototypeOf()
方法不得改变目标对象的原型,则唯一有效的 prototype 参数就是Object.getPrototypeOf(target)
的返回值。
const target = {};
const handler = {
setPrototypeOf(target, prototype) {
console.log(`target: ${target},prototype:
${prototype}`);
console.log(`setPrototypeOf()`);
return true;
}
}
const p = new Proxy(target, handler);
- 拦截 Proxy 实例作为函数调用的操作。
- 接受三个参数,分别是目标对象(
target
)、目标对象的上下文对象(this
)和目标对象的参数数组(args
)。 - 拦截操作
proxy(...args)
Function.prototype.call(thisArg, ...args)
Function.prototype.apply(target,thisArg,args)
const target = (a, b) => a + b;
const handler = {
apply(target, thisArg, args) {
console.log('apply()');
console.log(`target: ${target}`);
console.log(`thisArg: ${thisArg}`);
console.log(`args: ${args}`);
return target(...args);
}
}
const p = new Proxy(target, handler);
- 拦截 Proxy 实例作为构造函数调用的操作。
- 返回值必须是一个对象,否则会报错。
- 拦截
new
命令new proxy(...args)
。
construct()
方法可以接受三个参数。target
:目标构造函数。args
:构造函数的参数数组。newTarget
:创造实例对象时,new
命令作用的构造函数
const target = function () { };
const handler = {
construct(target, args) {
console.log(this === handler);
return new target(...args);
}
}
const p = new Proxy(target, handler);
const target = function () { };
const sum = function (total, sum) {
return total + sum;
}
const handler = {
construct(target, args) {
console.log(args);
return {
value: args.reduce(sum)
}
}
}
const p = new Proxy(target, handler);
4. 可撤销代理
- 有时候可能需要中断代理对象与目标对象之间的联系。对于使用
new Proxy()
创建的普通代理来说,这种联系会在代理对象的生命周期一直存在。 Proxy.revocable()
方法,返回一个可取消的 Proxy 实例。支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。
let target = {
foo: 'bar'
};
let handler = {};
let { proxy, revoke } = Proxy.revocable(target, handler);
//解构
Proxy.revocable()
方法返回一个对象,该对象的 proxy
属性是 Proxy
实例,revoke
属性是一个函数,可以取消 Proxy
实例。上面代码中,当执行 revoke
函数之后,再访问 Proxy
实例,就会抛出一个错误。
5. 代理中的 this
- 一般来说,方法中的 this 通常指向调用这个方法的对象
- Proxy 拦截函数内部的
this
,指向的是handler
对象。
const target = {
this() {
return this === proxy
}
};
const handler = {};
const proxy = new Proxy(target,handler);
console.log(proxy.this);//true
console.log(target.this);//false
- 上面代码中,一旦 proxy 代理 target,
target.this()
内部的 this就是指向 proxy ,而不是target。所以,虽然 proxy 没有做任何拦截,target.this()
和proxy.this()
返回不一样的结果。 - 但是如果目标对象依赖于对象标识,可能会出现一些问题。
const wm = new WeakMap();
class User {
constructor(userId) {
wm.set(this, userId);
}
get id() {
return wm.get(this);
}
set id(userId) {
wm.set(this, userId);
}
}
const user = new User(123);
console.log(user.id) // 123
const proxy = new Proxy(user, {});
console.log(proxy.id) // undefined
-
上面代码中,目标对象
user
的userId
属性,实际保存在外部WeakMap
对象wm
上面,通过this
键区分。由于通过proxy.id
访问时,this
指向proxy
,导致无法取到值,所以返回undefined
。 -
此外,有些原生对象的内部属性,只有通过正确的
this
才能拿到,所以 Proxy 也无法代理这些原生对象的属性。
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();// TypeError: this is not a Date object.
const target = {
getDate: new Date('2022-11-20'),
a: 1,
b: 2,
c: 3
};
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target[prop].getDate.bind(target
[prop]);
}
return target[prop];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.getDate()) // 20
- 上面代码中,
getDate()
方法只能在Date
对象实例中使用,如果this
不是Date
对象实例就会报错。这时,this
绑定到target[prop]
,就可以解决这个问题。
6. 小结
Proxy 代理应用场景是非常丰富的,比如跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定和可观察对象等等,细心的朋友可能发现,枚举的部分应用场景在本文中实现了,未实现的举例大家可以尝试着去完成哦!
不得不说代理是 ES6 新增的令人兴奋和动态十足的新特性,尽管不支持向后兼容,但其开辟出了一片前所未有的 JavaScript 元编程及抽象的新天地!