ES6 Proxy 拦截方法

69 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第21天,点击查看活动详情

ES6 Proxy 拦截方法

拦截方法

下面是 Proxy 支持的拦截操作一览,一共 13 种。

-   get(target, propKey, receiver) :拦截对象属性的读取,比如 proxy.foo 和 proxy['foo']。
-   set(target, propKey, value, receiver) :拦截对象属性的设置,比如 proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
-   has(target, propKey) :拦截 propKey in proxy 的操作,返回一个布尔值。
-   deleteProperty(target, propKey) :拦截 delete proxy[propKey] 的操作,返回一个布尔值。
-   ownKeys(target) :拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
-   getOwnPropertyDescriptor(target, propKey) :拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
-   defineProperty(target, propKey, propDesc) :拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
-   preventExtensions(target) :拦截 Object.preventExtensions(proxy),返回一个布尔值。
-   getPrototypeOf(target) :拦截 Object.getPrototypeOf(proxy),返回一个对象。
-   isExtensible(target) :拦截 Object.isExtensible(proxy),返回一个布尔值。
-   setPrototypeOf(target, proto) :拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
-   apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
-   construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args)。

get()

`get` 方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 Proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

`get` 方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。

var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
  get: function(target, propKey) {
    if (propKey in target) {
      return target[propKey];
    } else {
      throw new ReferenceError("Prop name "" + propKey + "" does not exist.");
    }
  }
});

proxy.name // "张三"
proxy.age // 抛出一个错误


`get` 方法可以继承。

let proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET ' + propertyKey);
    return target[propertyKey];
  }
});

let obj = Object.create(proto);
obj.foo // "GET foo"

上面代码中,拦截操作定义在 Prototype 对象上面,所以如果读取 obj 对象继承的属性时,拦截会生效。

下面的例子使用 `get` 拦截,实现数组读取负数的索引。

function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c


上面代码中,数组的位置参数是 -1,就会输出数组的倒数第一个成员。

利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。

var pipe = (function () {
  return function (value) {
    var funcStack = [];
    var oproxy = new Proxy({} , {
      get : function (pipeObject, fnName) {
        if (fnName === 'get') {
          return funcStack.reduce(function (val, fn) {
            return fn(val);
          },value);
        }
        funcStack.push(window[fnName]);
        return oproxy;
      }
    });

    return oproxy;
  }
}());

var double = n => n * 2;
var pow    = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;

pipe(3).double.pow.reverseInt.get; // 63


上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。

下面的例子则是利用 get 拦截,实现一个生成各种 DOM 节点的通用函数 dom。

```
const dom = new Proxy({}, {
  get(target, property) {
    return function(attrs = {}, ...children) {
      const el = document.createElement(property);
      for (let prop of Object.keys(attrs)) {
        el.setAttribute(prop, attrs[prop]);
      }
      for (let child of children) {
        if (typeof child === 'string') {
          child = document.createTextNode(child);
        }
        el.appendChild(child);
      }
      return el;
    }
  }
});

const el = dom.div({},
  'Hello, my name is ',
  dom.a({href: '//example.com'}, 'Mark'),
  '. I like:',
  dom.ul({},
    dom.li({}, 'The web'),
    dom.li({}, 'Food'),
    dom.li({}, '…actually that's it')
  )
);

document.body.appendChild(el);


下面是一个 get 方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。

const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true


上面代码中,proxy 对象的 getReceiver 属性是由 proxy 对象提供的,所以 receiver 指向 proxy 对象。

const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  }
});

const d = Object.create(proxy);
d.a === d // true


上面代码中,d 对象本身没有 a 属性,所以读取 d.a 的时候,会去 d 的原型 proxy 对象找。这时,receiver 就指向 d,代表原始的读操作所在的那个对象。

如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。


const target = Object.defineProperties({}, {
 foo: {
value: 123,
writable: false,
configurable: false
 },
 });

 const handler = {
get(target, propKey) {
return 'abc';
  }
};

const proxy = new Proxy(target, handler);

  proxy.foo
// TypeError: Invariant check failed