简介
ES6中新增了操作对象的新API:Proxy对象。从字面上看可以理解为代理器,主要用于改变对象的默认访问行为,实际表现是在访问对象之前增加一层拦截,任何对对象的访问行为都会通过这层拦截。在拦截中,可以增加自定义的行为。
基本语法:const proxy = new Proxy(target, handler)
实际是一个构造函数,接收两个参数,一个是目标对象target;另一个是配置对象handler,用来定义拦截的行为。
// 定义target目标对象
const person = {
name: 'kingx',
age: 23
};
// 定义handle配置对象
let handler = {
get: function (target, prop, receiver) {
console.log("你访问了person的属性");
return target[prop];
}
};
const p = new Proxy(person, handler);
console.log(p.name);
// 你访问了person的属性
// kingx
console.log(person.name) // 直接通过目标对象person访问name属性,则不会触发拦截行为。
// kingx
const p2 = new Proxy(person, {}); // 配置对象为空对象,则没有设置任何拦截,实际是对目标对象的访问
console.log(p2.name); // kingx
注意事项:
(1)必须通过代理实例访问:如果需要配置对象的拦截行为生效,那么必须是对代理实例的属性进行访问,而不是直接对目标对象进行访问。
(2)配置对象不能为空对象:如果需要配置对象的拦截行为生效,那么配置对象不能为空对象。如果为空对象,则代表没有设置任何拦截,实际是对目标对象的访问。另外配置对象不能为null,否则会抛出异常。
函数
Proxy支持总共13种函数:
- get(target, propKey, receiver)。拦截对象属性的读取操作,例如调用
person.name
或者person[name]
,其中target表示的是目标对象,propKey表示的是读取的属性值,receiver表示的是配置对象。 - set(target, propKey, value, receiver)。拦截对象属性的写操作,即设置属性值,例如
person.name='kingx'
或者person[name]='kingx'
,其中target表示目标对象,propKey表示的是将要设置的属性,value表示将要设置的属性的值,receiver表示的是配置对象。 - has(target, propKey)。拦截hasProperty的操作,返回一个布尔值,最典型的表现形式是执行
propKey in target
,其中target表示目标对象,propKey表示判断的属性。 - deleteProperty(target, propKey)。拦截
delete person[propKey]
的操作,返回一个布尔值,表示是否执行成功,其中target表示目标对象,propKey表示将要删除的属性。 - ownKeys(target)。拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环等操作,其中target表示的是获取对象自身所有的属性名。 - getOwnPropertyDescriptor(target, propKey)。拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
操作,返回属性的属性描述符构成的对象,其中target表示目标对象,propKey表示需要获取属性描述符集合的属性。 - defineProperty(target, propKey, propDesc)。拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy,propDescs)
操作,返回一个布尔值,其中target表示目标对象,propKey表示新增的属性,propDesc表示的是属性描述符对象。 - preventExtensions(target)。拦截
Object.preventExtensions(proxy)
操作,返回一个布尔值,表示的是让一个对象变得不可扩展,不能再增加新的属性,其中target表示目标对象。 - getPrototypeOf(target)。拦截
Object.getPrototypeOf(proxy)
操作,返回一个对象,表示的是拦截获取对象原型属性,其中target表示目标对象。 - isExtensible(target)。拦截
Object.isExtensible(proxy)
,返回一个布尔值,表示对象是否是可扩展的,其中target表示目标对象。 - setPrototypeOf(target, proto)。拦截
Object.setPrototypeOf(proxy, proto)
操作,返回一个布尔值,表示的是拦截设置对象的原型属性的行为,其中target表示目标对象,proto表示新的原型对象。 - apply(target, object, args)。拦截Proxy实例作为函数调用的操作,例如
proxy(...args)
、proxy.call(object,...args)
、proxy.apply(object, [...])
,其中target表示目标对象,object表示函数的调用方,args表示函数调用传递的参数。 - construct(target, args)。拦截Proxy实例作为构造函数调用的操作,例如
new proxy(...args)
,其中target表示目标对象,args表示函数调用传递的参数。
这些函数都有一个通用的特性,即如果在target中使用了this关键字,再通过Proxy处理后,this关键字指向的是Proxy的实例,而不是目标对象target。
const person = {
getName: function () {
console.log(this === proxy);
}
};
const proxy = new Proxy(person, {});
proxy.getName(); // true this指向Proxy的实例proxy
person.getName(); // false this指向person
get()函数
1. 读取不存在属性
在正常情况下,读取一个对象不存在的属性时,会返回“undefined”。通过Proxy的get()函数可以设置读取不存在的属性时抛出异常,从而避免对undefined值的兼容性处理。
let person = {
name: 'kingx'
};
const proxy = new Proxy(person, {
get: function (target, propKey) {
if(propKey in target) {
return target[propKey];
} else {
throw new ReferenceError(`访问的属性${propKey}不存在`);
}
}
});
console.log(proxy.name); // kingx
console.log(proxy.age); // ReferenceError: 访问的属性age不存在
2. 读取负索引的值
负索引实际就是从数组的尾部元素开始,从后往前,寻找元素的位置。
数组的索引值是从0开始依次递增的,正常情况下们无法读取负索引的值,但是通过Proxy的get()函数可以做到这一点。
const arr = [1, 4, 9, 16, 25];
const proxy = new Proxy(arr, {
get: function (target, index) {
index = Number(index);
if (index > 0) {
return target[index];
} else {
// 索引为负值,则从尾部元素开始计算索引
return target[target.length + index];
}
}
});
console.log(proxy[2]); // 9
console.log(proxy[-2]); // 16
3. 禁止访问私有属性
在一些约定俗成的写法中,私有属性都会以下画线(_)开头,当不希望用户能访问到私有属性,这可以通过设置Proxy的get()函数来实现。
const person = {
name: 'kingx',
_pwd: '123456'
};
const proxy = new Proxy(person, {
get: function (target, prop) {
if (prop.indexOf('_') === 0) { // 如果访问的某个属性是以下画线(_)开头的,则直接抛出异常
throw new ReferenceError('不可直接访问私有属性');
} else {
return target[prop];
}
}
});
console.log(proxy.name); // kingx
console.log(proxy._pwd); // ReferenceError: 不可直接访问私有属性
4. Proxy访问属性的限制
const target = Object.defineProperties({}, {
// 可写的name属性
name: {
value: 'kingx',
configurable: true, // 可配置即可以delete该属性
writable: true // 可以修改该属性值
},
// 不可写的age属性
age: {
value: 12,
configurable: false, // 不可配置,即这个属性不可被删除
writable: false // 不可写即不可以被修改,可以理解为常量
}
});
const proxy = new Proxy(target, {
get: function (targetObj, prop) {
return 'abc';
}
});
console.log(proxy.name); // abc
console.log(proxy.age); // Error: (expected '12' but got 'abc')
set()函数
1. 拦截属性赋值操作
const proxy = new Proxy({}, {
set: function (target, prop, value) {
if (prop === 'age') { // 只拦截age属性
if (!Number.isInteger(value)) { // 如果赋值的不是整数值,就抛出异常
throw new TypeError('The age is not an integer');
}
if (value > 200 || value < 0) { // 如果设置的值不在0~200以内,抛出RangeError异常
throw new RangeError('The age is invalid');
}
} else {
target[prop] = value;
}
}
});
proxy.name = 'kingx'; // 正常
proxy.age = 10; // 正常
proxy.age = 201; // RangeError: The age is invalid
2. 私有属性不应该被修改
可以通过get()函数实现私有属性不可以被访问,也可以通过set()函数实现私有属性不可以被修改
has()函数
1. 隐藏内部私有属性
has()函数用于拦截hasProperty()函数,即判断对象是否具有某个属性,而不是hasOwnProperty()函数,即has()函数不判断一个属性是对象自身的属性,还是对象继承的属性。如果具有则返回“true”,如果不具有则返回“false”,典型的就是in操作符。has()函数只会对in操作符生效,而不会对for…in循环操作符生效。
const obj = {
_name: 'kingx',
age: 13
};
const proxy = new Proxy(obj, {
has: function (target, prop) {
if(prop[0] === '_') { // 如果属性名第一个字符是下画线,则直接返回“false”,表示的是属性不存在对象中
return false;
}
return prop in target;
}
});
console.log('age' in proxy); // true
console.log('_name' in proxy); // false
for (let key in proxy) { // has()函数并没有生效。
console.log(proxy[key]);
}
// kingx
// 13
deleteProperty()函数
1. 禁止删除某些属性
deleteProperty()函数,用于拦截delete操作,返回“true”时表示属性删除成功,返回“false”时表示属性删除失败。
let obj = {
_name: 'kingx',
age: 12
};
const proxy = new Proxy(obj, {
deleteProperty: function (target, prop) {
if (prop[0] === '_') { // 禁止删除私有属性
throw new Error(`Invalid attempt to delete private "${prop}" property`);
}
return true;
}
});
delete proxy.age; // 删除成功
delete proxy._name; // Error: Invalid attempt to delete private "_name" property
apply()函数
1. 函数的拦截
apply()函数,用于拦截函数调用,可以加入自定义操作,从而得到新的函数处理结果。
函数调用包括直接调用、call()函数调用、apply()函数调用3种方式。
function sum(num1, num2) {
return num1 + num2;
}
const proxy = new Proxy(sum, {
apply: function (target, obj, args) {
console.log(target); // sum
return target.apply(obj, args) * 2;
}
});
console.log(proxy(1, 3)); // (1 + 3)×2 = 8
console.log(proxy.call(null, 3, 4)); // (3 + 4)×2 = 14
console.log(proxy.apply(null, [5, 6])); // (5 + 6)×2 = 22
使用场景
1. 实现真正的私有
- 不能访问到私有属性,如果访问到私有属性则返回“undefined”。
- 不能直接修改私有属性的值,即使设置了也无效。
不能遍历出私有属性,遍历出来的属性中不会包含私有属性。
const apis = {
_apiKey: '12ab34cd56ef',
getAllUsers: function () {
console.log('这是查询全部用户的函数');
},
getUserById: function (userId) {
console.log('这是根据用户id查询用户的函数');
},
saveUser: function (user) {
console.log('这是保存用户的函数');
}
};
const proxy = new Proxy(apis, {
get: function (target, prop) {
if (prop[0] === '_') {
return undefined;
}
return target[prop];
},
set: function (target, prop, value) {
if (prop[0] !== '_') {
target[prop] = value;
}
},
has: function (target, prop) {
if (prop[0] === '_') {
return false;
}
return prop in target;
}
});
console.log(proxy._apiKey); // undefined
console.log(proxy.getAllUsers()); // 这是查询全部用户的函数
proxy._apiKey = '123456789'; // 设置无效
console.log('getUserById' in proxy); // true
console.log('_apiKey' in proxy); // false
2. 增加日志记录
通过get()函数拦截到调用的函数名,然后会返回一个函数,在这个函数内通过apply()调用原始函数,然后调用记录操作日志的函数。
const apis = {
_apiKey: '12ab34cd56ef',
getAllUsers: function () {
console.log('这是查询全部用户的函数');
},
getUserById: function (userId) {
console.log('这是根据用户id查询用户的函数');
},
saveUser: function (user) {
console.log('这是保存用户的函数');
}
};
// 记录日志的方法
function recordLog() {
console.log('这是记录日志的函数');
}
const proxy = new Proxy(apis, {
get: function (target, prop) {
const value = target[prop];
return typeof val === 'function' ?
function (...args) {
// 此处增加一个调用记录日志的函数
recordLog();
val.apply(null, args);
} : val;
}
});
proxy.getAllUsers();
3. 提供友好提示或者阻止特定操作
某些被弃用的函数被调用时,给用户提供友好提示。get()函数
- 阻止删除属性的操作。deleteProperty()函数
- 阻止修改某些特定的属性的操作。set()函数
let dataStore = {
noDelete: 1234, // 不能删除的属性
oldMethod: function () {/*...*/}, // 已废弃的函数
doNotChange: “tried and true” // 不能改变的属性
};
let NO_DELETE = ['noDelete'];
let DEPRECATED = ['oldMethod'];
let NO_CHANGE = ['doNotChange'];
const proxy = new Proxy(dataStore, {
set(target, key, value, proxy) {
if (NO_CHANGE.includes(key)) {
throw Error(`Error! ${key} is immutable.`);
}
return true;
},
deleteProperty(target, key) {
if (NO_DELETE.includes(key)) {
throw Error(`Error! ${key} cannot be deleted.`);
}
return true;
},
get(target, key, proxy) {
if (DEPRECATED.includes(key)) {
console.warn(`Warning! ${key} is deprecated.`);
}
const val = target[key];
return typeof val === 'function' ?
function (...args) {
val.apply(null, args);
} : val;
}
});
proxy.doNotChange = "foo"; // Error! doNotChange is immutable.
delete proxy.noDelete; // Error! noDelete cannot be deleted.
proxy.oldMethod(); // Warning! oldMethod is deprecated.