linqinghao/blog

理解ES6的Proxy

Opened this issue · 0 comments

Proxy是ES6中的新特性之一。Proxy 用于修改某些操作的默认行为,比如属性get\set,属性枚举,函数调用等。借助Proxy,我们可以对javascript元级别进行编程,也即元编程。
Proxy可以理解成是对目标对象的一种拦截,或者代理,对目标对象的访问都必须通过这层拦截。所以可以在拦截这一层做过滤、改写等等操作。

Proxy基础使用

ES6原生提供Proxy构造函数,用于生成Proxy实例。该构造函数需要传递两个参数:

  • target 目标对象,即要代理的对象
  • handler 包含一系列拦截行为的配置对象

一个简单的拦截读取属性行为的例子:

let target = {
    name: 'allin';
};
console.log(target.name); // allin
let p = new Proxy(target,  {
    get: function(target, property) {
        return 'zoe'; // 拦截并返回一个新的返回值
    }
});
p.name; // zoe

上面的例子中,我实例化了一个Proxy实例,对target对象的属性访问进行了拦截,其中,handler配置对象有一个get方法,get方法的两个参数分别是目标对象和所要访问的属性。因此,访问任何属性都会返回'zoe'。

Proxy支持的拦截操作

Proxy支持的拦截操作有很多,我只列出一些常用的拦截行为,具体的见Proxy-MDN

  1. get(target, property, receiver)
    该方法用于拦截属性的读取,最后的receiver参数是可选参数,当target对象设置了propKey属性的get函数,receiver对象会绑定get函数的this对象。

  2. set(target, property, receiver)
    该方法用于拦截对象属性的设置。

    例如:

    let handler = {
        set: function(obj, prop, value) {
            if (prop === 'age') {
                if(!Number.isInteger(value)) {
                    throw new Error('年龄必须是整数!')
                }
            }
            obj[prop] = value;
        }
    }
    let person = new Proxy({}, handler);
    person.name = 'allin';
    person.age = 'old'; // ERROR
    person.age = 18; // GREAT
    person.age; //18

上面的例子我们设置了set方法,用于校验age属性是否是整数,如果不是则抛出类型错误,这种数据校验在对一个对象赋值时是非常有用的。

  1. defineProperty(target, propKey, propDesc)
    该方法用于拦截Object.defineProperty。

    例子:

    var p = new Proxy({}, {
    defineProperty: function(target, key, des) {
    	return false;
    }
    });
    Object.defineProperty(p, name, {
    value: 'allin'
    });  // Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property ''

    上面例子中定义了对defineProperty的拦截,导致后面在添加新属性时报错。

  2. apply(target, object, args)
    该方法用于拦截Proxy实例作为函数调用的操作。比如proxy(...args), proxy.call(obj, ...args), proxy.apply(...)。

    例子:

    var target = function() { return 'target' };
    var p = new Proxy(target, {
        apply: function(target, ctx, args) {
            return 'proxy';
        }
    });
    p() == target(); // false

    上面例子中,定义了apply拦截符,当调用p时,就会返回新的字符串。

  3. construct(target, args, proxy)
    该方法用于拦截Proxy实例作为构造函数调用的操作。比如 new Proxy(...)。

    例子:

    var p = new Proxy(function() {}, {
        construct: function(target, args) {
            return {
                value: args[0] * 2
            }
        }
    });
    new p(1).value; // 2

    上面的例子中,使用了construct拦截了new命令,使其实例化时返回一个新的对象。

Proxy 的用途

Proxy的功能非常强大,配合另一个ES6特性Reflect。Proxy能用来数据验证,数据绑定,数据观察等等。下面举个简单的例子:

const { METHODS } = require('http')
const api = new Proxy({},
  {
    get(target, propKey) {
      const method = METHODS.find(method => 
        propKey.startsWith(method.toLowerCase()))
      if (!method) return
      const path =
        '/' +
        propKey
          .substring(method.length)
          .replace(/([a-z])([A-Z])/g, '$1/$2')
          .replace(/\$/g, '/$/')
          .toLowerCase()
      return (...args) => {
        const finalPath = path.replace(/\$/g, () => args.shift())
        const queryOrBody = args.shift() || {}
        // You could use fetch here
        // return fetch(finalPath, { method, body: queryOrBody })
        console.log(method, finalPath, queryOrBody)
      }
    }
  }
)
// GET /
api.get()
// GET /users
api.getUsers()
// GET /users/1234/likes
api.getUsers$Likes('1234')
// GET /users/1234/likes?page=2
api.getUsers$Likes('1234', { page: 2 })
// POST /items with body
api.postItems({ name: 'Item name' })
// api.foobar is not a function
api.foobar()

上面例子中使用Proxy实现了一个小型请求库,是不是非常有趣~

参考链接

  1. https://medium.com/dailyjs/how-to-use-javascript-proxies-for-fun-and-profit-365579d4a9f8
  2. https://github.com/mikaelbr/proxy-fun
  3. https://medium.com/@ideepak.jsd/why-to-use-javascript-proxy-5cdc69d943e3