前言
我们知道 Vue3 的数据响应式原理是基于Proxy实现的,那么到底什么是Proxy?今天我们就来学习一下。
1. 什么是Proxy
Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。表示在目标对象之前架设一层“拦截”,外界对该对象的访问(如属性查找、赋值、枚举、函数调用等),都必须先通过这层拦截;在拦截内部可以对外界的访问进行过滤和改写。
1.1 语法及参数
const p = new Proxy(target, handler)
const p = new Proxy(target, {
get() { /.../ },
set() { /.../ },
// ...
})
-
target要使用Proxy进行拦截的目标对象。 -
handler也是一个对象,通常属性中会定义拦截行为的函数(如:get/set)。
1.2 handle拦截器一览表
| 方法 | 描述 |
|---|---|
handler.apply() | 拦截函数的调用 |
handler.construct() | 拦截 new 操作符 |
handler.defineProperty() | 拦截对象的 Object.defineProperty() 操作 |
handler.deleteProperty() | 拦截对对象属性的 delete 操作 |
handler.get() | 拦截对象的读取属性操作 |
handler.getOwnPropertyDescriptor() | 拦截 Object.getOwnPropertyDescriptor() 操作 |
handler.getPrototypeOf() | 拦截代理对象的原型 |
handler.has() | 拦截 in 操作符 |
handler.isExtensible() | 拦截对对象的 Object.isExtensible() |
handler.ownKeys() | 拦截 Reflect.ownKeys() |
handler.preventExtensions() | 拦截 Object.preventExtensions() 的操作 |
handler.set() | 拦截设置属性值的操作 |
handler.setPrototypeOf() | 拦截 Object.setPrototypeOf() 的操作 |
2. 常用的拦截器及基本用法
2.1 get()
拦截属性值的读取操作
target目标对象prop被获取的属性名receiverProxy 或者继承 Proxy 的对象返回值可以是任何值
let obj = {
name: 'Proxy'
}
let p = new Proxy(obj, {
get: function(target, prop, receiver) {
console.log("target: ", target === obj);
console.log("called: ", prop);
console.log("receiver: ", receiver === p);
return 10;
}
});
console.log(p.name);
输出:
// target: true
// called: name
// receiver: true
// 10
以下场景,proxy 会抛出 TypeError:
- 如果目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同。
- 如果要访问的目标属性没有配置访问方法,即 get 方法是 undefined 的,则返回值必须为 undefined。
let obj = {};
Object.defineProperty(obj, "a", {
configurable: false,
enumerable: false,
value: 10,
writable: false
});
let p = new Proxy(obj, {
get: function(target, prop) {
return 11;
}
});
console.log(p.a)
输出:
// Uncaught TypeError: 'get' on proxy: property 'a' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '10' but got '11')
2.2 set()
拦截设置属性值的操作
target目标对象。property将被设置的属性名或 Symbol。value新属性值。receiver最初被调用的对象。通常是proxy本身,但set方法在原型链上,或以其他方式被间接地调用的时候则不是proxy本身。返回值为true时表示设置成功。
receiver 为 Proxy 本身的情况
let obj = {}
let p = new Proxy(obj, {
set: function(target, prop, value, receiver) {
console.log('target: ', target === obj)
console.log('prop: ', prop)
console.log('value: ', value)
// 这里指的是正常的 proxy 对象本身,也就是 p
console.log('receiver: ', receiver === p)
target[prop] = value;
return true;
}
})
p.name = 'Proxy';
console.log(p.name)
输出:
// target: true
// prop: name
// value: Proxy
// receiver: true
// Proxy
间接调用原型链的情况
let p = new Proxy({}, {
set: function(target, prop, value, receiver) {
console.log('target: ', target === obj)
console.log('prop: ', prop)
console.log('value: ', value)
// 这里需要注意,receiver 指的是被调用的对象,也就是 obj
console.log('receiver: ', receiver === obj)
target[prop] = value;
return true;
}
})
let obj = Object.create(p)
obj.name = 'Proxy'
输出:
// target: false
// prop: name
// value: Proxy
// receiver: true
以下场景,proxy 会抛出 TypeError:
- 若目标属性是一个不可写及不可配置的数据属性,则不能改变它的值。
- 如果目标属性没有
set,则不能设置它的值。 - 在严格模式下
set()方法返回 false。
// 无法改变值
let obj = {};
Object.defineProperty(obj, "name", {
configurable: false,
enumerable: false,
value: 'zhk',
writable: false
});
let p = new Proxy(obj, {
set: function(target, prop, value, receiver) {
target[prop] = value;
return true;
}
})
p.name = 'Proxy';
输出:
// 返回 true 修改成功时,与不可写,不可配置冲突,所以报错
// Uncaught TypeError: 'set' on proxy: trap returned truish for property 'name' which exists in the proxy target as a non-configurable and non-writable data property with a different value
// 严格模式下
'use strict'
let obj = {}
let p = new Proxy(obj, {
set: function(target, prop, value, receiver) {
target[prop] = value;
return false;
}
})
p.name = 'Proxy';
console.log(p.name)
输出:
// Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'name'
2.3 deleteProperty()
用于拦截对对象属性的delete操作
target目标对象。property待删除的属性名。返回值是一个Boolean类型的值,表示了该属性是否被成功删除
let obj = {
name: 'Proxy'
};
let p = new Proxy(obj, {
deleteProperty: function(target, prop) {
console.log("target: ", target === obj);
console.log("prop: ", prop);
return true;
}
})
delete p.name;
输出:
// target: true
// prop: name
以下场景,proxy 会抛出 TypeError:
- 如果目标对象的属性是不可配置的,那么该属性不能被删除。
let obj = {};
Object.defineProperty(obj, "name", {
configurable: false,
enumerable: false,
});
let p = new Proxy(obj, {
deleteProperty: function(target, prop) {
return true;
}
})
delete p.name;
输出:
// Uncaught TypeError: 'deleteProperty' on proxy: trap returned truish for property 'name' which is non-configurable in the proxy target
2.4 apply()
拦截函数的调用
target目标对象(函数)。thisArg被调用时的上下文对象。argumentsList被调用时的参数数组。返回值可以是任何值。
let func = function () {}
let p = new Proxy(func, {
apply: function(target, thisArg, argumentsList) {
console.log('target:', target === func);
console.log('thisArg:', thisArg);
console.log('argumentsList:', argumentsList);
return 'any'
}
});
// 普通调用
p(1, 2)
结果:
// target:true
// thisArg:undefined
// argumentsList:[1, 2]
// 传入当前this的调用
p.call(this, 1, 2)
结果:
// target:true
// thisArg:Window {window: Window, self: Window, document: document, name: '', location: Location, …}
// argumentsList:[1, 2]
以下场景,proxy 会抛出 TypeError:
- target 必须是一个函数对象。
let func = {}
let p = new Proxy(func, {
apply: function(target, thisArg, argumentsList) {
console.log('target:', target === func);
console.log('thisArg', thisArg);
console.log('argumentsList: ', argumentsList);
return 'any'
}
});
p(1, 2)
输出:
// Uncaught TypeError: p is not a function
2.5 construct()
拦截 new 操作符
target目标对象。argumentsList构造函数的参数列表。newTarget最初被调用的构造函数。返回值是一个对象。
const func = function() {}
let p = new Proxy(func, {
construct: function(target, argumentsList, newTarget) {
console.log('target:', target === func);
console.log('argumentsList:', argumentsList);
console.log('newTarget:', newTarget === p);
return { value: argumentsList[0] * 10 };
}
});
console.log(new p(1));
结果:
// target: true
// argumentsList: [1]
// newTarget: true
// {value: 10}
以下场景,proxy 会抛出 TypeError:
- 返回值不是一个对象。
target必须具有一个有效的constructor供new操作符调用。
const func = function() {}
let p = new Proxy(func, {
construct: function(target, argumentsList, newTarget) {
return 10;
}
});
console.log(new p(1));
结果:
// Uncaught TypeError: 'construct' on proxy: trap returned non-object ('10')
const func = {}
let p = new Proxy(func, {
construct: function(target, argumentsList, newTarget) {
return { value: argumentsList[0] * 10 };
}
});
console.log(new p(1));
结果:
// Uncaught TypeError: p is not a constructor
2.6 defineProperty()
拦截对象的 Object.defineProperty() 操作
target目标对象。property待检索其描述的属性名。descriptor待定义或修改的属性的描述符。返回值是一个Boolean类型的值,表示操作的成功与否。
let obj = {}
let desc = {
configurable: true,
enumerable: true,
value: 'Proxy'
};
let p = new Proxy(obj, {
defineProperty: function(target, prop, descriptor) {
console.log('target:', target === obj);
console.log('prop', prop);
console.log('descriptor:', descriptor);
return true;
}
});
Object.defineProperty(p, 'name', desc);
结果:
// target: true
// prop name
// descriptor: {value: 'Proxy', enumerable: true, configurable: true}
以下场景,proxy 会抛出 TypeError:
- 如果目标对象不可扩展,将不能添加属性。
- 不能添加或者修改一个不可配置的属性。
- 在严格模式下,返回值为
false。
// 1. 目标不可扩展
let obj = {}
Object.preventExtensions(obj)
let desc = {
configurable: true,
enumerable: true,
value: 'Proxy'
};
let p = new Proxy(obj, {
defineProperty: function(target, prop, descriptor) {
return true;
}
});
Object.defineProperty(p, 'name', desc);
结果:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned truish for adding property 'name' to the non-extensible proxy target
// 2. 设置或者修改属性为不可配置的
let obj = {}
let desc = {
configurable: false, // 不可配置
enumerable: true,
value: 'Proxy'
};
let p = new Proxy(obj, {
defineProperty: function(target, prop, descriptor) {
return true;
}
});
Object.defineProperty(p, 'name', desc);
结果:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned truish for defining non-configurable property 'name' which is either non-existent or configurable in the proxy target
// 3. 严格模式下返回 false
'use strict'
let obj = {}
let desc = {
configurable: true,
enumerable: true,
value: 'Proxy'
};
let p = new Proxy(obj, {
defineProperty: function(target, prop, descriptor) {
return false;
}
});
Object.defineProperty(p, 'name', desc);
结果:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property 'name'
2.7 getOwnPropertyDescriptor()
拦截 Object.getOwnPropertyDescriptor() 操作
target目标对象。prop返回属性名称的描述。返回值必须是一个object或undefined。
const obj = {
name: {
configurable: true,
enumerable: true,
value: 'Proxy'
}
}
let p = new Proxy(obj, {
getOwnPropertyDescriptor: function(target, prop) {
console.log('target: ', target === obj);
console.log('prop: ', prop);
return { configurable: true, enumerable: true, value: 'Proxy' };
}
});
console.log(Object.getOwnPropertyDescriptor(p, 'name').value);
结果:
// target: true
// prop: a
// Proxy
以下场景,proxy 会抛出 TypeError:
getOwnPropertyDescriptor必须返回一个 object 或 undefined。- 如果属性作为目标对象的不可配置的属性存在。
- 如果属性作为目标对象的属性存在,并且目标对象不可扩展。
// 1. 返回值问题
const obj = {}
let p = new Proxy(obj, {
getOwnPropertyDescriptor: function(target, prop) {
return 10;
}
});
Object.getOwnPropertyDescriptor(p, 'name')
结果:
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined for property 'name'
// 2. 如果属性作为目标对象的不可配置的属性存在
const obj = {}
Object.defineProperty(obj, 'name', {
configurable: false,
enumerable: true,
value: 'Proxy'
})
let p = new Proxy(obj, {
getOwnPropertyDescriptor: function(target, prop) {
return { configurable: true, enumerable: true, value: 'Proxy' };
}
});
Object.getOwnPropertyDescriptor(p, 'name')
结果:
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'name' that is incompatible with the existing property in the proxy target
// 3.对象为不可扩展
const obj = {}
Object.defineProperty(obj, 'name', {
configurable: false,
enumerable: true,
value: 'Proxy'
})
Object.preventExtensions(obj)
let p = new Proxy(obj, {
getOwnPropertyDescriptor: function(target, prop) {
return { configurable: true, enumerable: true, value: 'Proxy' };
}
});
Object.getOwnPropertyDescriptor(p, 'name')
结果:
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'name' that is incompatible with the existing property in the proxy target
2.8 getPrototypeOf()
代理(Proxy)方法,当读取代理对象的原型时,该方法就会被调用
target被代理的目标对象。返回值必须是一个对象或者 null。
let obj = {};
let proto = {};
let p = new Proxy(obj, {
getPrototypeOf(target) {
console.log(target === obj);
return proto;
}
});
console.log(Object.getPrototypeOf(p) === proto);
结果:
// true
// true
5 种触发 getPrototypeOf 代理方法的方式
let obj = {};
let p = new Proxy(obj, {
getPrototypeOf(target) {
return Array.prototype;
}
});
console.log(
Object.getPrototypeOf(p) === Array.prototype,
Reflect.getPrototypeOf(p) === Array.prototype,
p.__proto__ === Array.prototype,
Array.prototype.isPrototypeOf(p),
p instanceof Array
);
结果:
// true true true true true
以下场景,proxy 会抛出 TypeError:
getPrototypeOf()返回值不是对象也不是null。- 目标对象是不可扩展的,且
getPrototypeOf()返回的原型不是目标对象本身的原型。
// 1. 返回值不是对象
let obj = {};
let p = new Proxy(obj, {
getPrototypeOf(target) {
return "foo";
}
});
Object.getPrototypeOf(p);
结果:
// Uncaught TypeError: 'getPrototypeOf' on proxy: trap returned neither object nor null
// 2. 目标对象不可扩展
let obj = {};
Object.preventExtensions(obj)
let p = new Proxy(obj, {
getPrototypeOf(target) {
return {};
}
});
Object.getPrototypeOf(p);
结果:
// Uncaught TypeError: 'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype
2.9 has()
拦截 in 操作符的操作
target目标对象。prop需要检查是否存在的属性。返回值是一个boolean属性的值。
let obj = {}
let p = new Proxy(obj, {
has: function(target, prop) {
console.log('target:', target === obj);
console.log('prop:', prop);
return true;
}
});
console.log('name' in p);
结果:
// target: true
// prop: name
// true
以下场景,proxy 会抛出 TypeError:
- 目标对象的某一属性本身不可被配置。
- 目标对象不可扩展。
let obj = {}
Object.defineProperty(obj, 'name', {
configurable: false, // 不可配置
enumerable: false,
value: 'init-Value',
})
Object.preventExtensions(obj) // 不可扩展
let p = new Proxy(obj, {
has: function(target, prop) {
return false; // 代理隐藏
}
});
console.log('name' in p);
结果:
// Uncaught TypeError: 'has' on proxy: trap returned falsish for property 'name' which exists in the proxy target as non-configurable
2.10 isExtensible()
拦截 Object.isExtensible() 操作
target目标对象。返回值一个Boolean值或可转换成Boolean的值。
let obj = {}
let p = new Proxy(obj, {
isExtensible: function(target) {
console.log('target:', target === obj);
return true;
}
});
console.log(Object.isExtensible(p));
结果:
// target: true
// true
以下场景,proxy 会抛出 TypeError:
Object.isExtensible(proxy)必须和Object.isExtensible(target)返回相同值。
let obj = {}
let p = new Proxy(obj, {
isExtensible: function(target) {
// 这里返回 true 不会出现报错
return false;
}
});
console.log(Object.isExtensible(p) === Object.isExtensible(obj));
console.log(Object.isExtensible(p));
结果:
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
2.11 ownKeys()
可以拦截以下操作:
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- Reflect.ownKeys()
target目标对象。返回值可枚举对象
let obj = {}
let p = new Proxy(obj, {
ownKeys: function(target) {
console.log('target:', target === obj);
return ['a', 'b', 'c'];
}
});
console.log(Object.getOwnPropertyNames(p));
console.log(Object.getOwnPropertySymbols(p));
console.log(Object.keys(p));
console.log(Reflect.ownKeys(p));
结果:
// target: true
// ['a', 'b', 'c']
// target: true
// []
// target: true
// []
// target: true
// ['a', 'b', 'c']
以下场景,proxy 会抛出 TypeError:
- 返回值数组的元素类型只能是
String或者Symbol。 - 结果列表必须包含目标对象的所有不可配置,自有属性。
- 不可扩展对象需要包含目标对象自有属性。
// 1. 返回值数组的元素类型只能是 `String` 或者 `Symbol`
let obj = { name: 'zhk' }
let p = new Proxy(obj, {
ownKeys: function(target) {
return ['name', 21, null];
}
});
console.log(Object.getOwnPropertyNames(p));
结果:
// Uncaught TypeError: 21 is not a valid property name at Function.getOwnPropertyNames
// 2. 结果列表必须包含目标对象的所有不可配置,自有属性
let obj = {}
Object.defineProperty(obj, 'name', {
configurable: false,
enumerable: true,
value: 'zhk'
})
let p = new Proxy(obj, {
ownKeys: function(target) {
return ['age'];
}
});
console.log(Object.getOwnPropertyNames(p));
结果:
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'name'
// 3. 不可扩展对象,返回值需要包含其 ownKey
let obj = { name: 'zhk' }
Object.preventExtensions(obj)
let p = new Proxy(obj, {
ownKeys: function(target) {
return ['a'];
}
});
console.log(Object.getOwnPropertyNames(p));
结果:
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'name'
2.12 preventExtensions()
拦截 Object.preventExtensions()
target所要拦截的目标对象。返回值布尔值。
let obj = {}
let p = new Proxy(obj, {
preventExtensions: function(target) {
console.log('target:', target === obj);
Object.preventExtensions(target);
return true;
}
});
console.log(Object.preventExtensions(p));
结果:
// called
// Proxy {}
以下场景,proxy 会抛出 TypeError:
- 目标对象是不可扩展的时候,才能返回
true
let obj = {}
let p = new Proxy(obj, {
preventExtensions: function(target) {
// Object.preventExtensions(target);
return false;
}
});
console.log(Object.preventExtensions(p));
结果:
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned falsish
2.13 setPrototypeOf()
以下参数传递给 setPrototypeOf 方法。
target被拦截目标对象。prototype对象新原型或为null。返回值布尔类型的值。
let obj = {}
let newProto = {}
let p = new Proxy(obj, {
setPrototypeOf(target, newProto) {
return false;
}
});
Object.setPrototypeOf(p, newProto);
结果:
// 如果 return true ,则修改成功
// Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned falsish for property 'undefined'
以下场景,proxy 会抛出 TypeError:
- 如果
target不可扩展,原型参数必须与Object.getPrototypeOf(target)的值相同
let obj = {}
let newProto = {}
Object.preventExtensions(obj)
let p = new Proxy(obj, {
setPrototypeOf(target, newProto) {
return true;
}
});
console.log(Object.getPrototypeOf(obj) === newProto)
Object.setPrototypeOf(p, newProto);
结果:
// false
// Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned truish for setting a new prototype on the non-extensible proxy target
3. this指向问题
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m()
proxy.m()
结果:
// false
// true
由此可知,当我们通过 Proxy 代理目标对象 target 时,通过代理访问内部属性方法,其 this 指向的是 Proxy 对象,而不是目标对象。
解决办法:可以在
handle方法中绑定原始对象的this
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {
get(target, props) {
return target[props].bind(target)
}
};
const proxy = new Proxy(target, handler);
target.m()
proxy.m()
结果:
// false
// false