JavaScript 的 Proxy 对象是 ES2015,也就是 ES6 版本添加的。其官方定义为:
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
它本质上就是一个代理,如果学过设计模式的话,其实很好理解。我之前写过的js版本的设计模式也写过这个。有兴趣可以参照:https://github.com/jeremyjone/design-pattern-js/blob/master/src/mode/proxy-mode.js
先举一个小例子
先看一下代码:
/**
* 使用ES6语法的Proxy类演示 代理模式的示例,明星 - 经纪人
*/
let star = {
name: "张xx",
age : 25,
phone: "138123456789"
}
let agent = new Proxy(star, {
get: function(target, key) {
if (key === "phone") {
return "agent phone: 13555555555";
}
else if (key === "price") {
return 150000;
}
return target[key];
},
set: function(target, key, val) {
if (key === "customPrice") {
if (val < 10000) {
throw new Error("价格太低");
} else {
target[key] = val;
return true;
}
}
}
})
// test
console.log(agent.name);
console.log(agent.phone);
console.log(agent.age);
console.log(agent.price);
agent.customPrice = 120000; // OK
console.log(agent.customPrice);
agent.customPrice = 1000; // Error
console.log(agent.customPrice);
在这里,我们有一个明星 张xx
,他很忙,不可能每件事情都自己处理,所以他需要一个代理人,也就是我们平时说的经纪人。当有人希望联系到该明星时,它会直接拨打电话,这时通过代理模式,经纪人会接到电话,这样对明星来说,电话的隐私被保护了起来。
大体上,Proxy 就是这样的作用。当然,它不仅仅可以这样。
官方文档解读
语法
从上面的例子可以看到,Proxy 的语法应该如下:
const p = new Proxy(target, handler)
其中:
-
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。 -
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
方法
Proxy 包含一个方法,其语法:
Proxy.revocable(target, handler);
作用:创建一个可撤销的Proxy对象。
官方示例:
var revocable = Proxy.revocable({}, {
get(target, name) {
return "[[" + name + "]]";
}
});
var proxy = revocable.proxy;
proxy.foo; // "[[foo]]"
revocable.revoke();
console.log(proxy.foo); // 抛出 TypeError
proxy.foo = 1 // 还是 TypeError
delete proxy.foo; // 又是 TypeError
typeof proxy // "object",因为 typeof 不属于可代理操作
更多内容,可以看官网,这个不是重点。
handler 对象的方法
handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
-
handler.preventExtensions()
Object.preventExtensions 方法的捕捉器。 -
handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor 方法的捕捉器。 -
handler.has()
in 操作符的捕捉器。 -
handler.get()
属性读取操作的捕捉器。 -
handler.set()
属性设置操作的捕捉器。 -
handler.deleteProperty()
delete 操作符的捕捉器。 -
handler.ownKeys()
Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 -
handler.apply()
函数调用操作的捕捉器。 -
handler.construct()
new 操作符的捕捉器。
上面这些都是标准的捕捉器,还有一些不标准的捕捉器已经被废弃并移除了。
使用
这里,我们最常使用的应该是 get
和 set
,这也是最基础的用法,不管是最开始的小例子,还是使用 Object.defineProperty
的监控方式,都是通过覆写这两个方法,达到一个代理监控的效果。
比如赋值时我们希望可以验证、重新整理数据等,都可以通过 Proxy 得到想要的结果。
基础示例
当对象中不存在属性时,返回默认值
const handler = {
get: function(obj, prop) {
// 当属性不存在,返回一个默认值。
return prop in obj ? obj[prop] : 37;
}
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37
无操作转发代理
代理可以将所有应用到它的操作转发到原生JS对象上:
let target = {}; // 原生对象
let p = new Proxy(target, {});
p.a = 37; // 操作转发到目标
console.log(target.a); // 37. 操作已经被正确地转发
同理,如果操作 target
,同时也会转发到 p
上。
验证
通过对 set
覆写,达到验证的效果,这个在实际生产中用处很大。
其实上面的明星小例子已经给出了示例,它验证的是明星代言费。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
// 表示成功
return true;
}
};
let person = new Proxy({}, validator);
person.age = 100;
console.log(person.age);
// 100
person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integer
person.age = 300;
// 抛出异常: Uncaught RangeError: The age seems invalid
扩展构造函数
这个就比较高级的用法了,Proxy 可以很轻松的扩展一个已有的构造函数。
function extend(sup, base) {
var descriptor = Object.getOwnPropertyDescriptor(
base.prototype, "constructor"
);
base.prototype = Object.create(sup.prototype);
var handler = {
construct: function(target, args) {
var obj = Object.create(base.prototype);
this.apply(target, obj, args);
return obj;
},
apply: function(target, that, args) {
sup.apply(that, args);
base.apply(that, args);
}
};
var proxy = new Proxy(base, handler);
descriptor.value = proxy;
Object.defineProperty(base.prototype, "constructor", descriptor);
return proxy;
}
var Person = function (name) {
this.name = name
};
var Boy = extend(Person, function (name, age) {
this.age = age;
});
Boy.prototype.sex = "M";
var Peter = new Boy("Peter", 13);
console.log(Peter.sex); // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age); // 13
一个完整的 traps 列表示例
/*
var docCookies = ... get the "docCookies" object here:
https://developer.mozilla.org/zh-CN/docs/DOM/document.cookie#A_little_framework.3A_a_complete_cookies_reader.2Fwriter_with_full_unicode_support
*/
var docCookies = new Proxy(docCookies, {
"get": function (oTarget, sKey) {
return oTarget[sKey] || oTarget.getItem(sKey) || undefined;
},
"set": function (oTarget, sKey, vValue) {
if (sKey in oTarget) { return false; }
return oTarget.setItem(sKey, vValue);
},
"deleteProperty": function (oTarget, sKey) {
if (sKey in oTarget) { return false; }
return oTarget.removeItem(sKey);
},
"enumerate": function (oTarget, sKey) {
return oTarget.keys();
},
"ownKeys": function (oTarget, sKey) {
return oTarget.keys();
},
"has": function (oTarget, sKey) {
return sKey in oTarget || oTarget.hasItem(sKey);
},
"defineProperty": function (oTarget, sKey, oDesc) {
if (oDesc && "value" in oDesc) { oTarget.setItem(sKey, oDesc.value); }
return oTarget;
},
"getOwnPropertyDescriptor": function (oTarget, sKey) {
var vValue = oTarget.getItem(sKey);
return vValue ? {
"value": vValue,
"writable": true,
"enumerable": true,
"configurable": false
} : undefined;
},
});
/* Cookies 测试 */
alert(docCookies.my_cookie1 = "First value");
alert(docCookies.getItem("my_cookie1"));
docCookies.setItem("my_cookie1", "Changed value");
alert(docCookies.my_cookie1);
实际使用
相比其他 handler
,生产中更多是大量的使用 get
和 set
。
比如,我们现在有一个对象,里面包含时间 date
字段。我们的需求是,希望外部有一个变量 dynamicDate
动态联动该 date
字段,当 date
字段改变时,dynamicDate
可以动态改变。
我们现在将它做成一个代理。
let target = {date: "2020-07-28"};
let dynamicDate = null;
let p = new Proxy(target, {
get: function(obj, prop) {
if (prop === "date") {
dynamicDate = new Date(obj[prop]);
}
return obj[prop];
},
set: function(obj, prop, value) {
// 减少不必要的重复赋值操作
if (obj[prop] !== value) {
obj[prop] = value;
}
return true;
}
})
这时,当 target
的数据改变时,dynamicDate
和 p
可以同时更新。
文章评论