ES6实战:如何使用Proxy

  • ES6实战:如何使用Proxy
    • 引言
    • 代理捕获类型
    • 代理示例1: 监控
    • 代理示例2:双向绑定
    • 未来示例
    • 代理支持
  • 译者注

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:



还有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)