阿西河

所有教程

公众号
🌙
阿西河前端的公众号

我的收藏

    最近访问  (文章)

      教程列表

      抓包专区
      测试专区

      JS 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;
        }
      };
      
      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
      

      扩展构造函数

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

      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;
        }
      });
      
      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;
        }
      });
      
      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 = 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);
      

      规范

      SpecificationStatusComment
      ECMAScript 2015 (6th Edition, ECMA-262)
      Proxy
      StandardInitial definition.
      ECMAScript 2016 (ECMA-262)
      Proxy
      Standard
      ECMAScript 2017 (ECMA-262)
      Proxy
      Standard
      ECMAScript Latest Draft (ECMA-262)
      Proxy
      Draft
      目录
      目录