文章目录
什么Proxy
proxy从字面的意思去理解就是代理、代理模式。
在JavaScript中,是ES6的新增对象。
在MDN上,对Proxy的解释为:Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
个人理解是:Proxy可以让你对JavaScript中的对象的基本操作进行自定义,想咋折腾就咋折腾。当对象去执行基本操作时,就会去执行你自定义的操作,而不是对象本身的操作。
他人理解:Proxy可以在目标对象之前架设一层‘拦截’,外界对该对象的访问,都必须先通过这层拦截,因此提供了这样一层机制,可以对外界的访问进行过滤和修改。
Proxy使用
let proxy = new Proxy(target, handler);
target——是要包装的对象,可以是任何东西,包括函数
handler——代理配置:带有拦截操作方法的对象,如get用于读取target的属性,set用于写入target的属性
//创建一个空handler的proxy
let target = {};
let proxy = new Proxy(target,{}); //空的handler对象
proxy.test = 5; //写入proxy对象
console.log(target.test); //5——test属性出现在了target中
console.log(proxy.test); //5——从proxy中也可以读取到test属性
for(let key in proxy) console.log(key); //test——迭代也能正常工作
上述代码中,handler是一个空对象,没有任何拦截效果,因此访问proxy就等同于访问target.
Proxy拦截方法
对于对象的大多数操作,JavaScript规范中有一个内部方法,它描述了最底层的工作方式。例如[[Get]],用于读取属性的内部方法,[[Set]]用于写入属性的内部方法。这些方法仅在规范中使用,我们不能直接通过方法名调用它们。Proxy会拦截这些方法的调用,下表是规定的方法:
内部方法 | Handler 方法 | 何时触发 |
---|---|---|
[[Get]] | get | 读取属性 |
[[Set]] | set | 写入属性 |
[[HasProperty]] | has | in 操作符 |
[[Delete]] | deleteProperty | delete 操作符 |
[[Call]] | apply | 函数调用 |
[[Construct]] | construct | new 操作符 |
[[GetPrototypeOf]] | getPrototypeOf | Object.getPrototypeOf |
[[SetPrototypeOf]] | setPrototypeOf | Object.setPrototypeOf |
[[IsExtensible]] | isExtensible | Object.isExtensible |
[[PreventExtensions]] | preventExtensions | Object.preventExtensions |
[[DefineOwnProperty]] | defineProperty | Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] | getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, for…in, Object.keys/values/entries |
[[OwnPropertyKeys]] | ownKeys | Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for…in, Object/keys/values/entries |
get方法
该方法用于读取属性
而要拦截读取操作,handler应该有get(target, property, receiver)方法
读取属性时触发该方法,参数如下:
- target——目标对象,
- property——目标属性名
- receiver——如果目标属性时一个getter访问器属性,则receiver就是本次读取属性所在的this对象,也就是proxy对象本身。目前不需要该参数
//实现一个对不存在的数组项返回0的数组
let array = [0,1,2];
array = new Proxy(array,{
get(target, prop){
if(prop in target){
return target[prop];
}else{
return 0; //默认值
}
}
});
console.log(array[1]); //1
console.log(array[5]); //0,不存在该项,本应该返回undefined,但是修改后可以返回0
代理应该在所有地方都完全替代目标对象,目标对象被代理后,就不应该再去引用目标对象,否则就容易造成混乱,而是通过代理来操作。
set方法
//set用于设置属性,当写入属性时,set方法就会被触发
set(target, property, value, receiver)
target——目标对象,该对象被作为第一个参数传递给new Proxy
property——目标属性名称
value——目标属性值
receiver——与get方法类似
如果写入操作(setting)成功,set方法将返回true,否则返回false
//一个只能用用于存放数字的数组,如果添加了其他类型的值,则报错
let numbers = [];
numbers = new Proxy(numbers, {
set(target, prop, val){ //拦截写入属性操作
if(typeof val == 'number'){
target[prop] = val;
return true; //必须要返回true,否则也会报错TyperError
}else{
return false;
}
}
});
//proxy拦截的只是[[Set]]操作,但是数组的其他方法不会拦截
numbers.push(1);
numbers.push(2);
console.log(numbers.length); //2
numbers.push('test'); //TypeError
ownKeys & getOwnPropertyDescriptor
Object.keys,for…in循环和大多数遍历对象属性的方法都是使用内部方法[[OwnPropertyKeys]]来获取属性列表,参照上面的方法表,该内部方法对应了一些方法:
- Object.getOwnPropertyNames(obj) 返回非 Symbol 键。
- Object.getOwnPropertySymbols(obj) 返回 symbol 键。
- Object.keys/values() 返回带有 enumerable 标志的非 Symbol 键/值
- for…in 循环遍历所有带有 enumerable 标志的非 Symbol 键,以及原型对象的键
//使用ownKeys方法来拦截for...in的遍历,跳过下划线_开头的属性
let user = {
name:'simon',
age:24,
_password: '******'
};
user = new Proxy(user,{
ownKeys(target){
return Object.keys(target).filter(key=>!key.startsWith('_'));
}
});
for(let key in user) console.log(key); //name,age
console.log(Object.keys(user)); //name,age
console.log(Object.values(user)); //simone,24
deleteProperty方法
JavaScript没有公共变量与私有变量之分,但是平常遵循一个约定,就是下划线_开头的属性和方法都是内部的,不应该从外部访问它们。
//实际上我们在外部也是可以访问的
let user = {
name:'simon',
_password:'******',
};
console.log(user._password); //******
但是通过Proxy可以禁止访问下划线开头的属性和方法:
- get 读取此类型属性时抛出错误
- set写入此类型属性时抛出错误
- deleteProperty删除属性时抛出错误
- ownKeys在使用for…in和Object.keys这样的方法时也排除_开头的属性
let user = {
name:'simon',
_password:'******'
};
user = new Proxy(user,{
get(target,prop){ //拦截属性读取
if(prop.startsWith('_')){
throw new Error('禁止访问');
}
let value = target[prop];
//需要value.bind(target)时因为防止当对象方法需要用到内部变量时,就不会触发错误,如需要检查密码,就需要用到内部变量,通过bidn来绑定到对象上,也就能访问了
return (typeof(value) === 'function') ? value.bind(target) : value;
},
set(target, prop, val){ //拦截属性写入
if(prop.startsWith('_')){
throw new Error('禁止写入');
}else{
target[prop] = val;
return true;
}
},
deleteProperty(target, prop){ //拦截属性删除
if(prop.startsWith('_')){
throw new Error('禁止删除');
}else{
delete target[prop];
return true;
}
},
ownKeys(target){ //拦截属性读取属性列表
return Object.keys(target).filter(key=>!key.startsWith('_'));
},
});
try{ //不允许访问
console.log(user._password); //Error:禁止访问
}catch(e){console.log(e.message);}
try{ //不允许修改
user._password = 'test'; //Error:禁止写入
}catch(err){console.log(err.message);}
try{ //也不能新增带_的属性
user._age = 24; //Error:禁止写入
}catch(err){console.log(err.message);}
try{ //能新增普通属性
user.job = 'student'; //
}catch(err){console.log(err.message);}
try{
delete user._password; //Error:禁止删除
}catch(e){console.log(e.message);}
for(let key in user) console.log(key); //name,job
has方法
has 会拦截in的调用
has(target, property):
target——目标对象,
property——属性名称
let range = {
start: 1,
end: 10
};
console.log(2 in range); //false
range = new Proxy(range,{
has(target, prop){
return prop >= target.start && prop <= target.end;
}
});
console.log(8 in range); //true
console.log(11 in range); //false
apply
proxy可以对apply方法代理,基本语法apply(target, thisArg,args):
- target——目标对象,这里一般指的函数
- thisArg——this的值
- args——参数列表
function delay(fun,ms){
return ()=>{
setTimeout(()=> fun.apply(this,arguments),ms);
}
}
function sayHello(user){
console.log(`Hello, ${user}!`);
}
console.log(sayHello.length); //1——length表示函数参数的个数
sayHello = delay(sayHello, 3000);
console.log(sayHello.length); //0——在包装器中,参数个数为0
//proxy可以将所有东西转发到目标对象
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
console.log(sayHi.length); // 1——原函数中的参数会全部传过来
sayHi("simon"); // Hello, simon!
proxy与Object.defineProperty的区别
proxy和Object.defineProperty都可用于数据绑定,都能监听数据的变化,但是两者也存在区别:
- Object.defineProperty(object,prop,description)是ES5提供的方法,该方法能在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。description所表示的属性描述符有两种形式:数据描述符和存取描述符,具体可见认识Object.defineProperty
- Proxy(target,handler)是ES6提供的方法,Object.defineProperty只能重定义属性的读取(get)和设置(set)内部方法,Proxy提供了更多的内部方法,如in、delelte、ownKeys等
参考链接: https://zh.javascript.info/proxy#proxy-apply