• 从使用场景了解proxy
  • Artiely
  • #proxy
  • 2021-02-23
  • 1699
  • 9 min read
  • loading...

从使用场景了解 proxy

前面讲过一篇proxy 的深入理解,现在就带大家了解一下proxy的实际应用,更深入的了解proxy的妙用及价值吧! 呼应上了~

由俭入奢

跟踪属性访问(get,set)

假设我们有一个函数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"]);
复制成功
1
2
3
4
5
6
7
8
9
10
11
12

我们希望设置或获取属性时得到以下效果

> p.x
GET x
5
> p.x = 21
SET x=21
21
复制成功
1
2
3
4
5
6

基于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);
    },
  });
}
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

基于以上我们可以实现日志打印,数据统计等

获取未知属性的警告

在访问属性时,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);
    },
  }
);
复制成功
1
2
3
4
5
6
7
8
9
10
11

让我们使用PropertyChecker创建的对象:

> const obj = { __proto__: PropertyChecker, foo: 123 };
> obj.foo  // 自己的属性
123
> obj.fo
ReferenceError: Unknown property: fo
> obj.toString()  // 继承的方法
'[object Object]'
复制成功
1
2
3
4
5
6
7

如果我们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
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

负数组索引(get)

某些 Array 方法可让您通过引用-1 获得最后一个元素,例如:

> ['a''b''c']。slice(-1['c']
复制成功
1
2

通过方括号运算符([])访问元素时,该方法不起作用。但是,我们可以使用代理来添加该功能。以下函数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
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

数据绑定

数据绑定是关于对象之间的数据同步。或用于一种流行的基于 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");
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13

访问 RESTful 接口服务

代理可用于创建可以在其上调用任意方法的对象。在以下示例中,该函数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);
    ···
});
复制成功
1
2
3
4
5
6
7

createWebService 实现

function createWebService(baseUrl) {
  return new Proxy(
    {},
    {
      get(target, propKey, receiver) {
        return () => httpGet(baseUrl + "/" + propKey);
      },
    }
  );
}
复制成功
1
2
3
4
5
6
7
8
9
10

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();
  });
}
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 另一种实现
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();
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  • 再来一种
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 }),
});
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 有趣的方式
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
})();
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

撤销引用

可撤销引用的工作方式如下:
不允许用户直接访问对象属性(或者转发你的服务器资源),用户完成引用后,通过撤消引用(将其关闭)来保护资源。此后,再引用将引发异常,并且不再转发任何内容。

const resource = { x: 11, y: 8 };
const { reference, revoke } = createRevocableReference(resource);

// 引用授权
console.log(reference.x); // 11
// 撤销
revoke();

console.log(reference.x); // TypeError: Revoked
复制成功
1
2
3
4
5
6
7
8
9

代理非常适合实现可撤销引用,因为它们可以拦截和转发操作。这是基于代理的简单实现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;
        },
    };
}
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

简化

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;
    },
  };
}
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

不过我们不必自己实现撤销,因为 proxy 自带改方法

function createRevocableReference(target) {
  const handler = {}; // {} 就会转发所有
  const { proxy, revoke } = Proxy.revocable(target, handler);
  return { reference: proxy, revoke };
}
复制成功
1
2
3
4
5

创建类型检查

防止用户输入不合法

var person = {
  name: "Artiely",
};

var typeSafePerson = createTypeSafeObject(person);

typeSafePerson.name = "Mike"; // ok
typeSafePerson.age = 18; // ok
typeSafePerson.age = "red"; // throws an error, different types
复制成功
1
2
3
4
5
6
7
8
9

只需简单的判断当前赋值的类型是否等于上次的类型

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;
      }
    },
  });
}
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

字段校验

let p = validator({});
p.age = 18;
p.age = "young"; // throws an error
p.age = 200; // throws an error
复制成功
1
2
3
4
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;
      }
    },
  });
};
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13

级联属性

数据对应关系

JavaScript Street  --  232200
Python Street -- 234422
Golang Street -- 231142

复制成功
1
2
3
4

两组映射关系表

const location2postcode = {
  "JavaScript Street": 232200,
  "Python Street": 234422,
  "Golang Street": 231142,
};
const postcode2location = {
  "232200": "JavaScript Street",
  "234422": "Python Street",
  "231142": "Golang Street",
};
复制成功
1
2
3
4
5
6
7
8
9
10

使用示例

let person = {
  name: 'Jon'
}
let p = postcodeValidate(person)
p.postcode = 232200
p.location
>JavaScript Street
复制成功
1
2
3
4
5
6
7

实现

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];
      }
    },
  });
};
复制成功
1
2
3
4
5
6
7
8
9
10
11
12

私有化属性

使带有_的属性私有化,外界不可访问,如下

let obj = {
  name: "artiely",
  _age: 18,
};
let objProxy = setPrivateField(obj);

obj._age; //undefined
_age in objProxy; // false
复制成功
1
2
3
4
5
6
7
8
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];
    },
  });
}
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

日志打印

代理属性查找

起因

const obj = {
  name: "artiely",
};
console.log(obj.age);
// undefined
复制成功
1
2
3
4
5
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); // 未设置的值
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13

监听每个异步的过程

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);
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

缓存

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;
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

计算属性

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
复制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43