美文网首页前端译趣我爱编程
ES6实战:如何使用Proxy

ES6实战:如何使用Proxy

作者: linc2046 | 来源:发表于2018-05-24 10:05 被阅读19次

    ES6实战:如何使用Proxy

    引言

    计算机术语中,代理定位于你和通信方之间。
    代理大部分指的是代理服务器,一个在浏览器和存放网页的web服务器之间的设备。
    代理服务器能够修改请求和响应。
    例如, 它可以缓存经常访问的资源,提供给更多的用户,提高访问效率。

    ES6代理定位代码与对象之间。
    代理允许执行元编程操作,如,拦截调用监察或改变对象的属性。

    下面的术语是和ES6代理有关系的:

    目标对象
    代理将会虚拟化的原始对象。
    可以是js对象, 如: jquery对象或者原生对象, 数组,甚至是其他代理。

    处理者对象
    实现了代理行为的对象。

    追踪函数
    处理函数内部定义的函数,当特定属性或方法调用时,提供目标对象的访问。
    下面简单示例可以最佳诠释,我们创建一个有三个熟悉的目标对象target:

    const target = {
      a: 1,
      b: 2,
      c: 3
    };
    
    

    我们再创建一个处理者对象,拦截所有get操作,拦截过后返回目标对象属性或数字42。

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

    我们创建新代理对象,传入目标对象和处理者对象,这段代码可以和代理进行数据交互,而不是直接访问目标对象本身:

    const proxy = new Proxy(target, handler);
    
    console.log(proxy.a);  // 1
    console.log(proxy.b);  // 2
    console.log(proxy.c);  // 3
    console.log(proxy.meaningOfLife);  // 42
    
    

    我们扩展下代理处理者对象,让它只允许设置a-z单字母属性:

    const handler = {
      get: function(target, name) {
        return (name in target ? target[name] : 42);
      },
    
      set: function(target, prop, value) {
        if (prop.length == 1 && prop >= 'a' && prop <= 'z') {
          target[prop] = value;
          return true;
        } else {
          throw new ReferenceError(prop + ' cannot be set');
          return false;
        }
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    proxy.a = 10;
    proxy.b = 20;
    proxy.ABC = 30;
    // Exception: ReferenceError: ABC cannot be set
    
    

    代理捕获类型

    我们看到getset生效,可能是最有用的捕获。
    但是还有其他可用捕获类型支持代理处理者对象。

    • construct(target, argList)

      捕获新对象new操作符的创建。

    • get(target, property)

      捕获对象get方法,必须返回属性值

    • set(target, property, value)

      捕获对象set方法,必须设置属性值。成功返回true。严格模式中, 返回false会抛出类型错误。

    • deleteProperty(target, property)

      捕获针对对象属性的删除操作,必须返回true或false。

    • apply(target, thisArg, argList)

      捕获对象函数调用

    • has(target, property)

      捕获in操作符,必须返回true或false。

    • ownKeys(target)

      捕获Object.getOwnPropertyNames, 必须返回枚举对象。

    • getPrototypeOf(target)

      捕获Object.getPrototypeOf, 必须返回对象原型或空。

    • setPrototypeOf(target, prototype)

      捕获Object.setPrototypeOf设置原型对象,无返回值。

    • isExtensible(target)

      捕获Object.isExtensible, 决定对象是否可以添加新属性,必须返回true或false。

    • preventExtensions(target)

      捕获Object.preventExtensions, 防止对象添加新属性,必须返回true或false.

    • getOwnPropertyDescriptor(target, property)

      捕获Object.getOwnPropertyDescriptor, 返回undefined或属性描述对象,有value,writable,get,set,configurableenumerable.

    • defineProperty(target, property, descriptor)

      捕获Object.defineProperty, 定义或修改对象属性。必须返回true或false.
      如果目标对象属性成功定义返回true,反之false.

    代理示例1: 监控

    代理允许创建任何通用包装器,无需改变目标对象内部代码。

    本例中,我们创建一个监控代理,计算属性的访问次数。
    首先,我们需要创建makeProfiler工厂函数,返回代理对象,保持计数状态:

    // 创建监控代理
    function makeProfiler(target) {
    
      const
        count = {},
        handler = {
    
          get: function(target, name) {
            if (name in target) {
              count[name] = (count[name] || 0) + 1;
              return target[name];
            }
          }
        };
    
      return {
        proxy: new Proxy(target, handler),
        count: count
      }
    };
    
    

    我们可以应用代理包装器至任意对象或其他代理。例如:

    const myObject = {
      h: 'Hello',
      w: 'World'
    };
    
    // 创建对象代理
    const pObj = makeProfiler(myObject);
    
    // 访问属性
    console.log(pObj.proxy.h); // Hello
    console.log(pObj.proxy.h); // Hello
    console.log(pObj.proxy.w); // World
    console.log(pObj.count.h); // 2
    console.log(pObj.count.w); // 1
    
    

    这只是个简单示例, 想象如果不用代理,实现几个不同对象属性访问计数需要的代码量。

    代理示例2:双向绑定

    对象之间可以进行双向绑定。
    特别用在js mvc库中, DOM变化时用来更新内部对象。

    假设有一个输入框,id是inputname:

    <input type="text" id="inputname" value="" />
    
    

    还有js对象myUser, 有id属性指向input。

    // #inputname 输入框的内部状态
    const myUser = {
      id: 'inputname',
      name: ''
    };
    
    

    我们的首要目标是用户改变输入框值时,更新myUser
    这可以通过绑定onchange事件实现:

    inputChange(myUser);
    
    // bind input to object
    function inputChange(myObject) {
      if (!myObject || !myObject.id) return;
    
      const input = document.getElementById(myObject.id);
      input.addEventListener('onchange', function(e) {
        myObject.name = input.value;
      });
    }
    
    

    我们的下一个目标是更改对象值时更新输入框显示。
    原生js实现并简单, 但代理提供一种实现方案:

    // 代理处理者
    const inputHandler = {
      set: function(target, prop, newValue) {
        if (prop == 'name' && target.id) {
          // 更新对象属性
          target[prop] = newValue;
    
          // 更新输入框值
          document.getElementById(target.id).value = newValue;
          return true;
        }
        else return false;
      }
    }
    
    // 创建代理
    const myUserProxy = new Proxy(myUser, inputHandler);
    
    // 设置新名称
    myUserProxy.name = 'Craig';
    console.log(myUserProxy.name); // Craig
    console.log(document.getElementById('inputname').value); // Craig
    
    

    这也许不是最有效率的双向绑定方案,但代理让你可以改变已存在对象行为,无需改变内部代码。

    未来示例

    Hemanth.HM 在文章JavaScript中的负数数组下标中建议使用代理实现负数数组下标。
    例如, arr[-1]返回最后一个元素。arr[-2]返回倒数第二个元素,以此类推。

    Nicholas C. Zakas在使用ES6 代理创建类型安全的属性
    中描述了如何使用代理通过验证新值实现类型安全。
    上例中,我们可以查证myUserProxy.name总是设置成字符串类型,否则抛错。

    代理支持

    代理的作用或许没有那么显而易见。
    但是代理提供了强大的元编程机会。
    js作者Brendan Eich,认为代理很好!

    目前除了IE11, node环境和现代浏览器都提供proxy支持。
    但是记住不是所有浏览器支持所有的捕获。
    查询浏览器兼容速查表, 可以知道代理支持情况。

    另外可惜的是,无法通过使用polyfill或用Babel转译ES6代理代码实现代理,因为代理强大到没有ES5实现支持。

    译者注

    • 原文链接

    • 因译者水平有限,如有错误,欢迎指正交流

    相关文章

      网友评论

        本文标题:ES6实战:如何使用Proxy

        本文链接:https://www.haomeiwen.com/subject/qzfdjftx.html