Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

术语

handler
包含陷阱(traps)的占位符对象。
traps
提供属性访问的方法。这类似于操作系统中捕获器的概念。
target
代理虚拟化的对象。它通常用作代理的存储后端。根据目标验证关于对象不可扩展性或不可配置属性的不变量(保持不变的语义)。

语法

let p = new Proxy(target, handler);

参数

target
Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
一个对象,其属性是当执行一个操作时定义代理的行为的函数。

方法

Proxy.revocable()
创建一个可撤销的 Proxy对象。

handler 对象的方法

handler 对象是一个占位符对象,它包含Proxy的捕获器。

示例

基础示例

在以下简单的例子中,当对象中不存在属性名时,缺省返回数为37。例子中使用了 get

let handler = {
    get: function(target, name){
        return name in target ? target[name] : 37;
    }
};

let 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

无操作转发代理

在以下例子中,我们使用了一个原生 JavaScript 对象,代理会将所有应用到它的操作转发到这个对象上。

let target = {};
let p = new Proxy(target, {});

p.a = 37;   // 操作转发到目标

console.log(target.a);    // 37. 操作已经被正确地转发

验证

通过代理,你可以轻松地验证向一个对象的传值。这个例子使用了 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

扩展构造函数

方法代理可以轻松地通过一个新构造函数来扩展一个已有的构造函数。这个例子使用了constructapply

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

操作 DOM 节点

有时你希望切换两个不同的元素的属性或类名。下面展示了如何使用 set

let view = new Proxy({
  selected: null
},
{
  set: function(obj, prop, newval) {
    let oldval = obj[prop];

    if (prop === 'selected') {
      if (oldval) {
        oldval.setAttribute('aria-selected', 'false');
      }
      if (newval) {
        newval.setAttribute('aria-selected', 'true');
      }
    }

    // The default behavior to store the value
    obj[prop] = newval;

    // 表示成功
    return true;
  }
});

let i1 = view.selected = document.getElementById('item-1');
console.log(i1.getAttribute('aria-selected')); // 'true'

let i2 = view.selected = document.getElementById('item-2');
console.log(i1.getAttribute('aria-selected')); // 'false'
console.log(i2.getAttribute('aria-selected')); // 'true'

值修正及附加属性

以下products代理会计算传值并根据需要转换为数组。这个代理对象同时支持一个叫做 latestBrowser的附加属性,这个属性可以同时作为 getter 和 setter。

let products = new Proxy({
  browsers: ['Internet Explorer', 'Netscape']
},
{
  get: function(obj, prop) {
    // 附加属性
    if (prop === 'latestBrowser') {
      return obj.browsers[obj.browsers.length - 1];
    }

    // 缺省行为是返回属性值
    return obj[prop];
  },
  set: function(obj, prop, value) {
    // 附加属性
    if (prop === 'latestBrowser') {
      obj.browsers.push(value);
      return;
    }

    // 如果不是数组则进行转换
    if (typeof value === 'string') {
      value = [value];
    }

    // 缺省行为是保存属性值
    obj[prop] = value;

    // 表示成功
    return true;
  }
});

console.log(products.browsers); // ['Internet Explorer', 'Netscape']
products.browsers = 'Firefox'; // ?传入一个 string (错误地)
console.log(products.browsers); // ['Firefox'] <- ?没问题, ?得到的是一个 array

products.latestBrowser = 'Chrome';
console.log(products.browsers); // ['Firefox', 'Chrome']
console.log(products.latestBrowser); // 'Chrome'

通过属性查找数组中的特定对象

以下代理为数组扩展了一些实用工具。可以看到,你可以灵活地“定义”属性,而不需要使用 Object.defineProperties方法。以下例子可以用于通过单元格来查找表格中的一行。在这种情况下,target 是table.rows

let products = new Proxy([
  { name: 'Firefox', type: 'browser' },
  { name: 'SeaMonkey', type: 'browser' },
  { name: 'Thunderbird', type: 'mailer' }
],
{
  get: function(obj, prop) {
    // 缺省行为是返回属性值, prop ?通常是一个整数
    if (prop in obj) {
      return obj[prop];
    }

    // 获取 products 的 number; 它是 products.length 的别名
    if (prop === 'number') {
      return obj.length;
    }

    let result, types = {};

    for (let product of obj) {
      if (product.name === prop) {
        result = product;
      }
      if (types[product.type]) {
        types[product.type].push(product);
      } else {
        types[product.type] = [product];
      }
    }

    // 通过 name 获取 product
    if (result) {
      return result;
    }

    // 通过 type 获取 products
    if (prop in types) {
      return types[prop];
    }

    // 获取 product type
    if (prop === 'types') {
      return Object.keys(types);
    }

    return undefined;
  }
});

console.log(products[0]); // { name: 'Firefox', type: 'browser' }
console.log(products['Firefox']); // { name: 'Firefox', type: 'browser' }
console.log(products['Chrome']); // undefined
console.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
console.log(products.types); // ['browser', 'mailer']
console.log(products.number); // 3

一个完整的 traps 列表示例

出于教学目的,这里为了创建一个完整的 traps 列表示例,我们将尝试代理化一个非原生对象,这特别适用于这类操作:由 发布在 document.cookie页面上的“小型框架”创建的docCookies全局对象。

/*
  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);

规范

Specification Status Comment
ECMAScript 2015 (6th Edition, ECMA-262)
Proxy
Standard Initial definition.
ECMAScript 2016 (ECMA-262)
Proxy
Standard
ECMAScript 2017 (ECMA-262)
Proxy
Standard
ECMAScript Latest Draft (ECMA-262)
Proxy
Draft

浏览器兼容性

Update compatibility data on GitHub
Desktop Mobile Server
Chrome Edge Firefox Internet Explorer Opera Safari Android webview Chrome for Android Firefox for Android Opera for Android Safari on iOS Samsung Internet Node.js
Proxy Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.apply Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.construct Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.defineProperty Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.deleteProperty Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.enumerate
Deprecated Non-standard
Chrome No support No Edge No support No Firefox No support 37 — 47 IE No support No Opera No support No Safari No support No WebView Android No support No Chrome Android No support No Firefox Android No support 37 — 47 Opera Android No support No Safari iOS No support No Samsung Internet Android No support No nodejs No support No
handler.get Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.getOwnPropertyDescriptor Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.getPrototypeOf Chrome Full support 49 Edge No support No Firefox Full support 49 IE No support No Opera Full support 36 Safari No support No WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 49 Opera Android Full support 36 Safari iOS No support No Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.has Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.isExtensible Chrome Full support 49 Edge Full support 12 Firefox Full support 31 IE No support No Opera Full support 36 Safari ? WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 31 Opera Android Full support 36 Safari iOS ? Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.ownKeys Chrome Full support 49 Edge Full support 12 Firefox Full support 18
Notes
Full support 18
Notes
Notes In Firefox 42, the implementation got updated to reflect the final ES2015 specification: The result is now checked if it is an array and if the array elements are either of type string or of type symbol. Enumerating duplicate own property names is not a failure anymore.
IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18
Notes
Full support 18
Notes
Notes In Firefox 42, the implementation got updated to reflect the final ES2015 specification: The result is now checked if it is an array and if the array elements are either of type string or of type symbol. Enumerating duplicate own property names is not a failure anymore.
Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.preventExtensions Chrome Full support 49 Edge Full support 12 Firefox Full support 22 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 22 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.set Chrome Full support 49 Edge Full support 12 Firefox Full support 18 IE No support No Opera Full support 36 Safari Full support 10 WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 18 Opera Android Full support 36 Safari iOS Full support 10 Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
handler.setPrototypeOf Chrome Full support 49 Edge Full support 12 Firefox Full support 49 IE No support No Opera Full support 36 Safari ? WebView Android Full support 49 Chrome Android Full support 49 Firefox Android Full support 49 Opera Android Full support 36 Safari iOS ? Samsung Internet Android Full support 5.0 nodejs Full support 6.0.0
revocable Chrome Full support 63 Edge Full support 12 Firefox Full support 34 IE No support No Opera Full support Yes Safari Full support 10 WebView Android Full support 63 Chrome Android Full support 63 Firefox Android Full support 34 Opera Android Full support Yes Safari iOS Full support 10 Samsung Internet Android Full support 8.0 nodejs Full support 6.0.0

Legend

Full support  
Full support
No support  
No support
Compatibility unknown  
Compatibility unknown
Non-standard. Expect poor cross-browser support.
Non-standard. Expect poor cross-browser support.
Deprecated. Not for use in new websites.
Deprecated. Not for use in new websites.
See implementation notes.
See implementation notes.

参考

版权声明

一些内容(如文本、例子)是复制自或修改自ECMAScript wiki(版权声明 CC 2.0 BY-NC-SA)。