定义
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法
const p = new Proxy(target, handler)
参数
-
target
:被代理的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理) -
handler
:可以操作代理对象的操作对象
废话不多说,使用起来。
兼容性
可以看到,IE 、 Opera Mini 和 Baidu Browser 一路扑街,这大概也是 vue3 抛弃 IE 的原因吧,毕竟,鱼和熊掌不可兼得。关于
handler
的一些方法兼容性,也可以通过caniuse查到。
简单代理
let user = {
name: '刘备',
age: 18
}
let userP = new Proxy(user, {
get(obj, key) {
return obj[key]
}
});
userP.name = '关羽';
userP.age = 14;
userP.sex = '男';
console.log(userP.name, userP.age); // 关羽 14
而当你打印
console.log(user.name); // 关羽
console.log(user.sex); // 男
得到的结果分别是关羽和男, 这是为什么?
proxy代理可以无操作转发代理,代理会将所有应用到它的操作转发到这个对象上。也就是说,代理对象的所有赋值,数据变更等操作,被代理对象也会做出相应的操作。
所以当你给代理对象 userP
赋值 userP.name = '关羽'
,被代理对象 user
也发生了同样的操作 user.name = '关羽'
。
handler方法
handler方法有很多,不一一介绍,感兴趣可以去MDN查阅相关文档,看下用的比较多的。
-
handler.get()
:拦截对象的读取属性操作。 -
handler.set()
:设置属性值操作的捕获器。 -
handler.apply()
:拦截函数的调用。 -
handler.construct()
:拦截new
操作符。 -
handler.defineProperty()
:拦截对对象的Object.defineProperty()
操作。 -
handler.deleteProperty()
:拦截对对象属性的delete
操作。
get()
接收三个个参数
-
target
:目标对象。 -
property
:被获取的属性名。 -
receiver
:Proxy
或者继承Proxy
的对象,可选
案例:实现一个生成各种 DOM
节点的通用函数 dom
。(来自阮一峰ECMAScript 6 入门)
const dom = new Proxy({}, {
get(target, property) {
return function(attrs = {}, ...children) {
const el = document.createElement(property);
for (let prop of Object.keys(attrs)) {
el.setAttribute(prop, attrs[prop]);
}
for (let child of children) {
if (typeof child === 'string') {
child = document.createTextNode(child);
}
el.appendChild(child);
}
return el;
}
}
});
const el = dom.div({},
'Hello, my name is ',
dom.a({href: '//example.com'}, 'Mark'),
'. I like:',
dom.ul({},
dom.li({}, 'The web'),
dom.li({}, 'Food'),
dom.li({}, '…actually that\'s it')
)
);
document.body.appendChild(el);
效果
set()
接收四个参数
-
target
:目标对象。 -
property
:将被设置的属性名或 Symbol。 -
value
:新属性值。 -
receiver
:通常是proxy
本身,但handler
的set
方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是proxy
本身)。
案例:通过监听 input 输入框,给页面给上的元素实时更新数值
let inputDom = new Proxy({
inputValue: null,
value: ''
}, {
set: function(obj, key, value) {
console.log(value);
if ( obj.inputValue ) {
obj.inputValue.innerHTML = value;
}
// 默认行为是存储被传入 setter 函数的属性值
obj[key] = value;
// 表示操作成功
return true;
}
});
inputDom.inputValue = document.getElementById('input_value');
document.getElementById('input').addEventListener('input', (e)=>{
inputDom.value = e.target.value;
})
综合案例1:给一个数组添加数据,而不是修改,记录数组的变化历史。
let products = new Proxy({
data: ['apple'],
lastValue: '' // 最后添加进来的值
},{
get(obj, key) {
if(key === 'lastValue') {
return obj['data'][obj['data'].length -1]
}
return obj[key]
},
set (obj, key, value) {
if(value in obj[key]) {
return false
}
obj[key][obj[key].length] = value
return true;
}
})
分别做以下操作
console.log(products.lastValue); // apple
products.data = 'car';
console.log(products.lastValue); // car
products.data = 'ice cream';
console.log(products.lastValue); // ice cream
console.log(products.data); // ['apple', 'car', 'ice cream']
综合案例2:正则匹配校验传入的 phoneNumber
是否合法
定义一个验证的 handler
let phoneValidator = {
set(obj, key, value) {
// 先判断 key 是否为 phoneNumber
if(key === 'phoneNumber') {
let reg = /^(?:(?:\+|00)86)?1[3-9]\d{9}$/
if(!reg.test(value)) {
throw new TypeError('你输入的手机号码不正确!')
}
}
obj[key] = value;
// 表示设置值生效
return true;
}
}
代理验证
let person = new Proxy({
phoneNumber: ''
}, phoneValidator);
person.phoneNumber = '13299998888';
console.log(person.phoneNumber); // 13299998888
person.phoneNumber = '231';
console.log(person.phoneNumber); // Uncaught TypeError: 你输入的手机号码不正确!
看了这些案例你应该能想到其他更多的用法,从此,你的业务逻辑代码又可以少些很多。
注意:
- 如果一个属性不可配置(
configurable
)且不可写(writable
),则Proxy
不能修改该属性,否则通过Proxy
对象访问该属性会报错。 - 如果目标对象自身的某个属性不可写,那么
set
方法将不起作用。
比如:
let target = Object.defineProperties({},{
action: {
value: '学习',
configurable: false,
writable: false
}
})
let handler = {
get(target, key) {
return '不学习';
}
}
let proxy = new Proxy(target, handler);
console.log(proxy.action); // Uncaught TypeError
let target = {};
Object.defineProperty(target, 'action', {
value: '睡觉',
writable: false
})
let handler = {
set(target, key, value, receiver) {
target[key] = 'value';
return true
}
}
let proxy = new Proxy(target, handler);
proxy.action = '后悔';
console.log(proxy.action); // Uncaught TypeError
apply()
捕获函数的调用,接收三个参数。
-
target
:目标对象(函数)。 -
thisArg
:被调用时的上下文对象。 -
argumentsList
:被调用时的参数数组。
注意:target
必须是函数才可被捕获,否则就会抛出 TypeError
错误。
该方法拦截以下操作:
proxy(...args)
-
Function.prototype.apply()
和Function.prototype.call()
Reflect.apply()
function parent(num1, num2) {
console.log(num1 + num2);
};
var parentProxy = new Proxy(parent, {
apply(target, thisArg, argumentsList) {
console.log(argumentsList[0] + argumentsList[1]);
}
})
parent(4, 5); // 9
parentProxy(40, 50); // 90
如果这时候把 thisArg
打印出来,就会发现是个 undefined
我们把代码改一下
function parent(name) {
return '这是' + name;
};
var parentProxy = new Proxy(parent, {
apply(target, thisArg, argumentsList) {
// 有thisArg上下文的时候就用name
let name = (thisArg && thisArg.name) || '不知道谁人'
return '这是' + name +'的儿子' + argumentsList[0] + '和' + argumentsList[1]
}
})
// 定义一个全局变量name
var name = 'window';
console.log(parent('曹操'));; // 这是曹操
console.log(parentProxy('张三', '李四')); // 这是不知道谁人的儿子张三和李四 thisArg是个undefined
console.log(parentProxy.call(null, '王五', '老六')); // 这是不知道谁人的儿子王五和老六 thisArg是个null
console.log(parentProxy.apply(this, ['location', 'history'])); // 这是window的儿子location和history thisArg是window
console.log(Reflect.apply(parentProxy,null,['杂七', '杂八'])); // 这是不知道谁人的儿子杂七和杂八 thisArg是个null
let otherParent = function() {
this.name = '刘备';
console.log(parentProxy.call(this, '刘婵','刘永')); // 这是刘备的儿子刘婵和刘永 thisArg是 callParent {name: '刘备'}
}
new otherParent();
construct()
拦截 new
操作符,接收三个参数。
-
target
:目标对象。 -
arguments
:Listconstructor
的参数列表。 -
newTarget
:最初被调用的构造函数。
其返回值必须是个对象,如果不返回或者返回的不是一个对象,则会抛出一个错误 Uncaught TypeError
let proxyC = new Proxy(function() {}, {
construct(target, arguments, newTarget) {
console.log(target);
console.log(arguments);
console.log(newTarget);
return {
value:arguments[0]
}
}
})
console.log(new proxyC('参数')); // {value: '参数'}
如果代理的不是一个可调用的函数
let proxyC = new Proxy({}, {
construct(target, arguments, newTarget) {
return { }
}
})
console.log(new proxyC('参数'));
或者返回的不是一个对象
let proxyC = new Proxy(function () {}, {
construct(target, arguments, newTarget) {
return arguments[0]
}
})
console.log(new proxyC('参数'));
defineProperty()
接收三个参数
-
target
:目标对象。 -
property
:待检索其描述的属性名。 -
descriptor
:待定义或修改的属性的描述符。
var numbers = new Proxy({}, {
defineProperty: function(target, prop, descriptor) {
console.log('prop: ' + prop + '--' + descriptor.value);
return true;
}
});
var desc = {
value: 10,
configurable: true,
enumerable: true
};
Object.defineProperty(numbers, 'a', desc); // prop: a--10
注意:当且仅当该属性的 enumerable
键值为 true
时,该属性才会出现在对象的枚举属性中。
Object.defineProperty(numbers, 'b', {
value: 20,
configurable: true,
enumerable: false
}); // prop: b--20
numbers.b = 45; // prop: b--45
console.log(numbers.b); // undefined 因为不可枚举
如果目标对象不可扩展(non-extensible),则defineProperty()不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty()方法不得改变这两个设置。
Object.defineProperty(numbers, 'b', {
value: 20,
configurable: false
}); // prop: b--20
numbers.b = 45; // Uncaught TypeError
deleteProperty()
拦截对对象属性的 delete
操作,接收两个参数。
-
target
:目标对象。 -
property
:待删除的属性名。
var userinfo = new Proxy({
name: '朱凤丽',
age: 19
}, {
deleteProperty(target, prop) {
if(prop === 'name') {
return false
}
delete target[prop]
return true;
}
});
delete userinfo.name;
delete userinfo.age;
console.log(userinfo.name); // 朱凤丽
console.log(userinfo.age); // undefined
注意:如果目标对象的属性是不可配置的,那么该属性不能被删除。
以上,关于 proxy
就写到这里,想了解更多其他函数的可以去MDN查阅相关文档。
参考文章:
Proxy MDN
阮一峰ECMAScript 6 入门