超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

ES6 - Proxy 对象

概述

Proxy 对象是定义在全局对象上的一个对象的构造函数,通过对目标对象的一些基本操作进行重定义并生成新的代理对象。说起来比较绕,看如下示例:

var obj = new Proxy({}, {
set(target, key, val) {
console.log('call setter');
return Reflect.set(target, key, val);
}
});

obj.val = 3;
// call setter
// 3

Proxy 对象生成的新对象修改了 {} 对象的赋值方式,使用 obj.val 赋值,比原始行为多了一行打印日志。

使用 Proxy 对象生成代理对象的语法如下:

var proxyObj = new Proxy(target, handler);

其中 handler 是拥有一系列需要更改的默认行为的方法的函数的集合,支持更改的行为和 Reflect 对象的静态函数一一对应:

  • apply: 拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct: 拦截 new 命令,比如 new proxy(...args)
  • defineProperty: 拦截 Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值
  • deleteProperty: 拦截 delete proxy[propKey] 的操作,返回一个布尔值
  • get: 拦截对象属性的读取,比如 proxy.fooproxy['foo']
  • getOwnPropertyDescriptor: 拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
  • getPrototypeOf: 拦截 Object.getPrototypeOf(proxy)Object.prototype.__proto__Object.prototype.isPrototypeOf()Object.getPrototypeOf()Reflect.getPrototypeOf()instanceof 运算符 ,返回一个对象
  • has: 拦截 propKey in proxy 的操作,返回一个布尔值
  • isExtensible: 拦截 Object.isExtensible(proxy)
  • ownKeys: 拦截 Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy),返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性
  • preventExtensions: 拦截 Object.preventExtensions(proxy),返回一个布尔值
  • set: 拦截对象属性的设置,比如 proxy.foo = vproxy['foo'] = v
  • setPrototypeOf: 拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值

Proxy 支持修改的这些行为和 Reflect 中的行为意义一致,不再一一说明。

应用场景

Proxy 顾名思义,类似于设计模式中的代理模式,通常有一下几种用途:

取消代理

对于某些场合下,可能会需要收回对代理的使用,这种情况下可以使用 Proxy.revocable,该方法返回一个对象,对象包含两个属性,一个属性是 proxy 实例,另一个是取消 proxy 实例的方法:

var {proxy, revoke} = Proxy.revocable({}, {});

proxy.value = 1;
proxy.value; //1

revoke();
proxy.value; //Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

访问控制和管理

在 js 语法中并不存在私有属性,我们可以使用 Proxy 阻止属性被访问:

var obj = {
_private: '_private',
public: 'public'
}

var proxyObj = new Proxy(obj, {
get(target, key) {
// 设置下划线开头的属性为私有属性
if (key.indexOf('_') > -1) {
throw Error("私有属性,禁止访问");
}
return Reflect.get(target, key);
}
});

proxyObj.public // "public"
proxyObj._private // Uncaught Error: 私有属性,禁止访问

通过修改 obj 的 getter,将所有的下划线 _ 属性的访问拦截,并抛出异常,其他的属性正常返回值。

在设置私有属性的时候最好把私有属性遍历和 setter 以及删除也禁用了:

var obj = {
_private: '_private',
public: 'public'
}

var proxyObj = new Proxy(obj, {
// 设置读取下划线开头的属性
get(target, propKey) {
if (propKey.indexOf('_') > -1) {
throw Error("私有属性,禁止访问");
}
return Reflect.get(target, propKey);
},
// 禁止为下划线开头的属性赋值
set(target, propKey) {
if (propKey.indexOf('_') > -1) {
throw Error("私有属性,禁止访问");
}
return Reflect.get(target, propKey);
},
// 禁止删除下划线开头的属性
deleteProperty(target, propKey) {
if (propKey.indexOf('_') > -1) {
throw Error("私有属性,禁止删除");
}
return Reflect.deleteProperty(target, propKey);
},
// 获取对象成员跳过下划线开头的属性
ownerKeys(target) {
return Reflect.ownerKeys(target).filter(item => item[0] !== '_');
}
});

在进行类库编写的时候经常会有接口过期,为了过渡,通常我们不会删除该方法,但是最好能够给出警告信息:

var obj = {
oldFunc: () => {},
newFunc: () => {}
}

var proxy = new Proxy(obj, {
get(target, key) {
if (key == 'oldFunc') {
console.warn('警告! oldFunc 已过时,请使用 newFunc 代替。');
}
return Reflect.get(target, key);
}
})

proxy.oldFunc(); // 警告! oldFunc 已过时,请使用 newFunc 代替。

同样我们也可以进行方法拦截,过滤,记录日志等操作,操作和上面类似,不再详细说明。

校验

通过 Proxy 可以很方便的实现属性校验:

var validatorCreater = (target, validator) => new Proxy(target, {
_validator: validator,
set(target, key, value, receiver) {
if (this._validator[key]) {
if (!this._validator[key](value)) {
throw Error(`${value} is not a valid value`);
}
return Reflect.set(target, key, value, receiver);
}
throw Error(`${key} is not a valid property`);
}
});

var validators = {
name(val) {
return typeof val === 'string';
},
age(val) {
return typeof val === 'number' && val > 0;
}
}

var person = validatorCreater({}, validators);

person.name = 1; // Uncaught Error: name is not a valid property
person.name = '1'; // "1"

person.age = '1'; // Uncaught Error: 1 is not a valid value
person.age = -1; // Uncaught Error: -1 is not a valid value
person.age = 12; // "12"

数据变化监听

当数据发生改变时,能够监听到这种变化,这是数据绑定的实现基础,来看下面这个例子:

实现 onChange(obj, callback) 函数,当 obj 变化时(新增、删除、修改、查找),调用 callback 函数

function onChange(objToWatch, onChangeFunction) {
if (typeof objToWatch !== "object") {
throw TypeError("objToWatch must be a object");
}

return new Proxy(objToWatch, {
get(target, propKey, receiver) {
onChangeFunction();
const result = Reflect.get(target, propKey, receiver);
if (typeof result === "object") {
return onChange(result, onChangeFunction);
}
return result;
},
set(target, propKey, value, receiver) {
onChangeFunction();
return Reflect.set(target, propKey, value, receiver);
},
deleteProperty(target, propKey) {
onChangeFunction();
delete target[propKey];
return true;
}
});
}

// 测试
let counter = 0;
const logger = () => {
counter++;
};
const obj = { a: { b: { c: { d: "123" } } } };
const proxy = onChange(obj, logger);

console.log(proxy.a); // 访问属性
console.log(counter); // 1

console.log(proxy.a.b.c.d); // 访问多层属性,每层都会触发
console.log(counter); // 5

proxy.a = "b"; // 修改属性
console.log(counter); // 6

delete proxy.a; // 删除属性
console.log(counter); // 7

Decorator

ProxyDecorator 在某些情况下还是挺像的,使用上可以区分为:Proxy 的核心作用是控制外界对被代理者内部的访问,Decorator 的核心作用是增强被装饰者的功能。有些功能的实现可以用Proxy 也可以使用 Decorator

参考