原文转载 @artiely
假设我们有一个函数tracePropAccess(obj, propKeys),该函数 obj 在 propKeys 设置或获取的属性(其键在 Array 中)时进行记录。在以下代码中,我们将该函数应用于类的实例 Point:
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `Point(${this.x}, ${this.y})`; } } const p = new Point(5, 7); // 追踪属性 `x` and `y` p = tracePropAccess(p, ["x", "y"]);复制成功
我们希望设置或获取属性时得到以下效果
> p.x GET x 5 > p.x = 21 SET x=21 21复制成功
基于proxy的简单实现
function tracePropAccess(obj, propKeys) { const propKeySet = new Set(propKeys); return new Proxy(obj, { get(target, propKey, receiver) { if (propKeySet.has(propKey)) { console.log("GET " + propKey); } return Reflect.get(target, propKey, receiver); }, set(target, propKey, value, receiver) { if (propKeySet.has(propKey)) { console.log("SET " + propKey + "=" + value); } return Reflect.set(target, propKey, value, receiver); }, }); }复制成功
基于以上我们可以实现日志打印,数据统计等
在访问属性时,JavaScript 非常宽容。例如,如果您尝试读取属性并拼写错误的名称,则不会得到异常,而会得到结果undefined。在这种情况下,您可以使用代理获取例外。其工作原理如下。我们使代理成为对象的原型。
如果在对象中找不到属性,get 则会触发。如果该属性甚至在代理之后的原型链中不存在,则会引发异常。否则,我们返回继承属性的值。我们将操作转发到目标(目标的原型也是代理的原型)。
const PropertyChecker = new Proxy( {}, { get(target, propKey, receiver) { if (!(propKey in target)) { throw new ReferenceError("Unknown property: " + propKey); } return Reflect.get(target, propKey, receiver); }, } );复制成功
让我们使用PropertyChecker创建的对象:
> const obj = { __proto__: PropertyChecker, foo: 123 }; > obj.foo // 自己的属性 123 > obj.fo ReferenceError: Unknown property: fo > obj.toString() // 继承的方法 '[object Object]'复制成功
如果我们PropertyChecker使用构造函数,则可以通过 extends 继承类
function PropertyChecker() {} PropertyChecker.prototype = new Proxy( {}, { get(target, propKey, receiver) { if (!(propKey in target)) { throw new ReferenceError("Unknown property: " + propKey); } return Reflect.get(target, propKey, receiver); }, } ); class Point extends PropertyChecker { constructor(x, y) { super(); this.x = x; this.y = y; } } const p = new Point(5, 7); console.log(p.x); // 5 console.log(p.z); // ReferenceError复制成功
某些 Array 方法可让您通过引用-1 获得最后一个元素,例如:
> ['a','b','c']。slice(-1) ['c']复制成功
通过方括号运算符([])访问元素时,该方法不起作用。但是,我们可以使用代理来添加该功能。以下函数createArray()创建支持负索引的数组。它通过将代理包装 Array 实例来实现。
function createArray(...elements) { const handler = { get(target, propKey, receiver) { const index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } return Reflect.get(target, propKey, receiver); }, }; const target = []; target.push(...elements); return new Proxy(target, handler); } const arr = createArray("a", "b", "c"); console.log(arr[-1]); // c复制成功
数据绑定是关于对象之间的数据同步。或用于一种流行的基于 MVC 模式的库
常见的有 vue,immer,mobx,...
要实现数据绑定,您必须观察并响应对对象所做的更改。在下面的代码片段中,我概述了观察更改对数组的工作方式。
function createObservedArray(callback) { const array = []; return new Proxy(array, { set(target, propertyKey, value, receiver) { callback(propertyKey, value); return Reflect.set(target, propertyKey, value, receiver); }, }); } const observedArray = createObservedArray((key, value) => console.log(`${key}=${value}`) ); observedArray.push("a");复制成功
代理可用于创建可以在其上调用任意方法的对象。在以下示例中,该函数createWebService创建一个这样的对象service。调用service方法,检索具有相同名称的 Web 服务资源的内容。
const service = createWebService('http://example.com/data'); // 读取json数据 http://example.com/data/employees service.employees().then(json => { const employees = JSON.parse(json); ··· });复制成功
createWebService 实现
function createWebService(baseUrl) { return new Proxy( {}, { get(target, propKey, receiver) { return () => httpGet(baseUrl + "/" + propKey); }, } ); }复制成功
httpGet 实现
function httpGet(url) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); Object.assign(request, { onload() { if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }, onerror() { reject(new Error("XMLHttpRequest Error: " + this.statusText)); }, }); request.open("GET", url); request.send(); }); }复制成功
const { METHODS } = require("http"); const api = new Proxy( {}, { get(target, propKey) { const method = METHODS.find((method) => propKey.startsWith(method.toLowerCase()) ); if (!method) return; const path = "/" + propKey .substring(method.length) .replace(/([a-z])([A-Z])/g, "$1/$2") .replace(/\$/g, "/$/") .toLowerCase(); return (...args) => { const finalPath = path.replace(/\$/g, () => args.shift()); const queryOrBody = args.shift() || {}; console.log(method, finalPath, queryOrBody); return fetch(finalPath, { method, body: queryOrBody }); }; }, } ); // GET / api.get(); // GET /users api.getUsers(); // GET /users/1234/likes api.getUsers$Likes("1234"); // GET /users/1234/likes?page=2 api.getUsers$Likes("1234", { page: 2 }); // POST /items with body api.postItems({ name: "Item name" }); // api.foobar is not a function api.foobar();复制成功
let handlers = { get(target, property) { if (!target.init) { // 初始化对象 ["GET", "POST"].forEach((method) => { target[method] = (url, params = {}) => { return fetch(url, { headers: { "content-type": "application/json", }, mode: "cors", credentials: "same-origin", method, ...params, }).then((response) => response.json()); }; }); } return target[property]; }, }; let API = new Proxy({}, handlers); await API.GET("XXX"); await API.POST("XXX", { body: JSON.stringify({ name: 1 }), });复制成功
const www = new Proxy(new URL("https://www"), { get: function get(target, prop) { let o = Reflect.get(target, prop); console.log("🚀 ~ file: 2021-2-23-proxy.md ~ line 21 ~ get ~ o", o, prop); if (typeof o === "function") { return o.bind(target); } if (typeof prop !== "string") { return o; } if (prop === "then") { return Promise.prototype.then.bind(fetch(target)); } target = new URL(target); target.hostname += `.${prop}`; console.log("get", get); return new Proxy(target, { get }); }, }); // 访问百度 www.baidu.com.then((response) => { console.log(response.status); // ==> 200 }); // 使用 async/await 语法: (async () => { const response = (await www.baidu.com) + "foo/1111"; console.log(response.ok); // ==> true console.log(response.status); // ==> 200 })();复制成功
可撤销引用的工作方式如下: 不允许用户直接访问对象属性(或者转发你的服务器资源),用户完成引用后,通过撤消引用(将其关闭)来保护资源。此后,再引用将引发异常,并且不再转发任何内容。
const resource = { x: 11, y: 8 }; const { reference, revoke } = createRevocableReference(resource); // 引用授权 console.log(reference.x); // 11 // 撤销 revoke(); console.log(reference.x); // TypeError: Revoked复制成功
代理非常适合实现可撤销引用,因为它们可以拦截和转发操作。这是基于代理的简单实现createRevocableReference:
function createRevocableReference(target) { let enabled = true; return { reference: new Proxy(target, { get(target, propKey, receiver) { if (!enabled) { throw new TypeError('Revoked'); } return Reflect.get(target, propKey, receiver); }, has(target, propKey) { if (!enabled) { throw new TypeError('Revoked'); } return Reflect.has(target, propKey); }, ··· }), revoke() { enabled = false; }, }; }复制成功
简化
function createRevocableReference(target) { let enabled = true; const handler = new Proxy( {}, { get(dummyTarget, trapName, receiver) { if (!enabled) { throw new TypeError("Revoked"); } return Reflect[trapName]; }, } ); return { reference: new Proxy(target, handler), revoke() { enabled = false; }, }; }复制成功
不过我们不必自己实现撤销,因为 proxy 自带改方法
function createRevocableReference(target) { const handler = {}; // {} 就会转发所有 const { proxy, revoke } = Proxy.revocable(target, handler); return { reference: proxy, revoke }; }复制成功
防止用户输入不合法
var person = { name: "Artiely", }; var typeSafePerson = createTypeSafeObject(person); typeSafePerson.name = "Mike"; // ok typeSafePerson.age = 18; // ok typeSafePerson.age = "red"; // throws an error, different types复制成功
只需简单的判断当前赋值的类型是否等于上次的类型
function createTypeSafeObject(object) { return new Proxy(object, { set: function(target, property, value) { var currentType = typeof target[property], newType = typeof value; if (property in target && currentType !== newType) { throw new Error( "Property " + property + " must be a " + currentType + "." ); } else { target[property] = value; } }, }); }复制成功
let p = validator({}); p.age = 18; p.age = "young"; // throws an error p.age = 200; // throws an error复制成功
const validator = (target) => { return new Proxy((target = {}), { set(target, props, value) { if (props === "age") { if (!Number.isInteger(value) || value > 200 || value < 0) { throw new TypeError("age should be an integer between 0 and 150"); } target[props] = value; return true; } }, }); };复制成功
数据对应关系
JavaScript Street -- 232200 Python Street -- 234422 Golang Street -- 231142复制成功
两组映射关系表
const location2postcode = { "JavaScript Street": 232200, "Python Street": 234422, "Golang Street": 231142, }; const postcode2location = { "232200": "JavaScript Street", "234422": "Python Street", "231142": "Golang Street", };复制成功
使用示例
let person = { name: 'Jon' } let p = postcodeValidate(person) p.postcode = 232200 p.location >JavaScript Street复制成功
实现
const postcodeValidate = (obj) => { return new Proxy(obj, { set(item, property, value) { if (property === "location") { item.postcode = location2postcode[value]; } if (property === "postcode") { item.location = postcode2location[value]; } }, }); };复制成功
使带有_的属性私有化,外界不可访问,如下
let obj = { name: "artiely", _age: 18, }; let objProxy = setPrivateField(obj); obj._age; //undefined _age in objProxy; // false复制成功
function setPrivateField(obj, prefix = "_") { return new Proxy(obj, { has: (obj, prop) => { if (typeof prop === "string" && prop.startsWith(prefix)) { return false; } return prop in obj; }, ownKeys: (obj) => { return Reflect.ownKeys(obj).filter( (prop) => typeof prop !== "string" || !prop.startsWith(prefix) ); }, get: (obj, prop) => { if (typeof prop === "string" && prop.startsWith(prefix)) { return undefined; } return obj[prop]; }, }); }复制成功
起因
const obj = { name: "artiely", }; console.log(obj.age); // undefined复制成功
let handler = { get: function(target, props) { return props in target ? target[props] : "未设置值"; }, }; let obj = { name: "artiely", }; let p = new Proxy(obj, handler); console.log(p.age); // 未设置的值复制成功
const logUpdate = require("log-update"); const asciichart = require("asciichart"); const chalk = require("chalk"); const Measured = require("measured"); const timer = new Measured.Timer(); const history = new Array(120); history.fill(0); const monitor = (obj) => { return new Proxy(obj, { get(target, propKey) { const origMethod = target[propKey]; if (!origMethod) return; return (...args) => { const stopwatch = timer.start(); const result = origMethod.apply(this, args); return result.then((out) => { const n = stopwatch.end(); history.shift(); history.push(n); return out; }); }; }, }); }; const service = { callService() { return new Promise((resolve) => setTimeout(resolve, Math.random() * 50 + 50) ); }, }; const monitoredService = monitor(service); setInterval(() => { monitoredService .callService() .then(() => { const fields = [ "min", "max", "sum", "variance", "mean", "count", "median", ]; const histogram = timer.toJSON().histogram; const lines = [ "", ...fields.map( (field) => chalk.cyan(field) + ": " + (histogram[field] || 0).toFixed(2) ), ]; logUpdate(asciichart.plot(history, { height: 10 }) + lines.join("\n")); }) .catch((err) => console.error(err)); }, 100);复制成功
const cacheTarget = (target, ttl = 60) => { const CREATED_AT = Date.now(); const isExpired = () => Date.now() - CREATED_AT > ttl * 1000; const handler = { get: (target, prop) => (isExpired() ? undefined : target[prop]), }; return new Proxy(target, handler); }; const cache = cacheTarget({ age: 25 }, 5); console.log(cache.age); setTimeout(() => { console.log(cache.age); }, 6 * 1000); //运行结果如下: 25; undefined;复制成功
const bankAccount = { balance: 10, name: "Artiely", get dollars() { console.log("计算美元"); return this.balance * 0.1547 }, }; let cache = { currentBalance: null, currentValue: null, }; const handler = { get: function(obj, prop) { if (prop === "dollars") { let value = cache.currentBalance !== obj.balance ? obj[prop] : cache.currentValue; cache.currentValue = value; cache.currentBalance = obj.balance; return value; } return obj[prop]; }, }; const wrappedBankAcount = new Proxy(bankAccount, handler); console.log(wrappedBankAcount.dollars); console.log(wrappedBankAcount.dollars); console.log(wrappedBankAcount.dollars); console.log(wrappedBankAcount.dollars); // OUTPUT: // 计算美元 // 34.3008459 // 34.3008459 // 34.3008459 // 34.3008459复制成功