ES2015 代理简介

ES2015 代理(在 Chrome 49 及更高版本中)为 JavaScript 提供了中介 API,使我们能够捕获或拦截对目标对象的所有操作,并修改此目标的操作方式。

代理有许多用途,包括:

  • 断球
  • 对象虚拟化
  • 资源管理
  • 进行性能分析或记录日志以进行调试
  • 安全性和访问权限控制
  • 对象使用合约

Proxy API 包含一个 Proxy 构造函数,该构造函数接受指定的目标对象和处理程序对象。

var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);

代理的行为由处理程序控制,该处理程序可以通过多种实用的方式修改目标对象的原始行为。该处理程序包含可选的陷阱方法(例如 .get().set().apply()),系统会在对代理执行相应操作时调用这些方法。

断球

首先,我们将使用 Proxy API 获取一个普通对象,并向其中添加一些拦截中间件。请注意,传递给构造函数的第一个参数是目标(要代理的对象),第二个参数是处理程序(代理本身)。在这里,我们可以为 getter、setter 或其他行为添加钩子。

var target = {};

var superhero = new Proxy(target, {
    get: function(target, name, receiver) {
        console.log('get was called for:', name);
        return target[name];
    }
});

superhero.power = 'Flight';
console.log(superhero.power);

在 Chrome 49 中运行上述代码,我们会得到以下结果:

get was called for: power  
"Flight"

正如我们在实践中所看到的,正确地对代理对象执行属性 get 或属性 set 会导致对处理程序上的相应陷阱进行元级调用。处理程序操作包括属性读取、属性赋值和函数应用,所有这些操作都会转发到相应的陷阱。

陷阱函数可以选择任意实现操作(例如将操作转发到目标对象)。如果未指定陷阱,默认情况下确实会发生这种情况。例如,下面是一个仅执行此操作的无操作转发代理:

var target = {};

var proxy = new Proxy(target, {});
    // operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been  forwarded
console.log(target.paul);

我们刚刚介绍了如何代理普通对象,但我们同样可以轻松代理函数对象,其中函数就是我们的目标。这次,我们将使用 handler.apply() 陷阱:

// Proxying a function object
function sum(a, b) {
    return a + b;
}

var handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log(`Calculate sum: ${argumentsList}`);
        return target.apply(thisArg, argumentsList);
    }
};

var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3

识别代理

您可以使用 JavaScript 等式运算符 (=====) 观察代理的身份。众所周知,当应用于两个对象时,这些运算符会比较对象身份。以下示例演示了此行为。比较两个不同的代理会返回 false,即使底层目标相同也是如此。同样,目标对象与其任何代理都不同:

// Continuing previous example

var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false

理想情况下,您应该无法区分代理对象和非代理对象,这样,代理的使用实际上不会影响应用的结果。这正是 Proxy API 不提供检查对象是否为代理的方法,也不为对象上的所有操作提供陷阱的原因之一。

使用场景

如前所述,代理具有广泛的用例。上述许多功能(例如访问控制和性能分析)都属于通用封装容器:封装同一地址“空间”中的其他对象的代理。还提到了虚拟化。虚拟对象是模拟其他对象的代理,而这些对象无需位于同一地址空间中。例如,远程对象(模拟其他空间中的对象)和透明 Future(模拟尚未计算的结果)。

将代理用作处理程序

代理处理程序的一个非常常见的用例是在对封装的对象执行操作之前执行验证或访问控制检查。只有在检查成功后,系统才会转发操作。以下验证示例对此进行了演示:

var validator = {
    set: function(obj, prop, value) {
    if (prop === 'yearOfBirth') {
        if (!Number.isInteger(value)) {
        throw new TypeError('The yearOfBirth is not an integer');
        }

        if (value > 3000) {
        throw new RangeError('The yearOfBirth seems invalid');
        }
    }

    // The default behavior to store the value
    obj[prop] = value;
    }
};

var person = new Proxy({}, validator);

person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception

此模式的更复杂示例可能会考虑代理处理程序可以拦截的所有不同操作。可以想象,某种实现必须在每个陷阱中重复访问检查和转发操作的模式。

由于每个操作可能都需要以不同的方式转发,因此很难轻松进行抽象化处理。在理想情况下,如果所有操作都可以通过单个陷阱统一汇总,则处理程序只需在单个陷阱中执行一次验证检查。为此,您可以将代理处理程序本身实现为代理。很抱歉,这超出了本文的讨论范围。

对象扩展

代理的另一个常见用例是扩展或重新定义对对象的操作的语义。例如,您可能希望处理程序记录操作、通知观察器、抛出异常(而不是返回未定义),或者将操作重定向到不同的目标进行存储。在这些情况下,使用代理可能会导致与使用目标对象完全不同的结果。

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 Vehicle = function(name){
    this.name = name;
};

var Car = extend(Vehicle, function(name, year) {
    this.year = year;
});

Car.prototype.style = "Saloon";

var Tesla = new Car("Model S", 2016);

console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year);  // 2016

访问权限控制

访问控制是代理的另一个很好的用例。您可以传递封装在某种保护膜中的代理,而不是将目标对象传递给不可信的代码。当应用认为不可信代码已完成特定任务后,可以撤消引用,从而将代理与其目标分离。膜会将此分离递归地扩展到可从定义的原始目标访问的所有对象。

将反射与代理搭配使用

Reflect 是一个新的内置对象,可为可拦截的 JavaScript 操作提供方法,非常适合与代理一起使用。实际上,Reflect 方法与代理处理程序的方法相同。

像 Python 或 C# 这样的静态类型语言早已提供反射 API,但作为动态语言,JavaScript 实际上并不需要反射 API。有人可能会说 ES5 已经提供了许多反射功能,例如 Array.isArray()Object.getOwnPropertyDescriptor(),在其他语言中这些功能也被视为反射。ES2015 引入了 Reflection API,该 API 将包含此类别的未来方法,使其更易于推理。这很有意义,因为 Object 旨在作为基本原型,而不是反射方法的存储分区。

使用 Reflect,我们可以改进之前的 Superhero 示例,以便对 get 和 set 陷阱进行适当的字段拦截,如下所示:

// Field interception with Proxy and the Reflect API

var pioneer = new Proxy({}, {
    get: function(target, name, receiver) {
        console.log(`get called for field: ${name}`);
        return Reflect.get(target, name, receiver);
    },

    set: function(target, name, value, receiver) {
        console.log(`set called for field: ${name} and value: ${value}`);
        return Reflect.set(target, name, value, receiver);
    }
});

pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName

输出结果:

set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName

另一个示例是,您可能希望:

  • 将代理定义封装在自定义构造函数中,以避免每次需要使用特定逻辑时都手动创建新的代理。

  • 添加了“保存”更改的功能,但仅当数据实际发生更改时才会执行(假设是因为保存操作非常耗费资源)。

function Customer() {

    var proxy = new Proxy({
    save: function(){
        if (!this.dirty){
        return console.log('Not saving, object still clean');
        }
        console.log('Trying an expensive saving operation: ', this.changedProperties);
    },

    }, {

    set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];

        if(target.changedProperties.indexOf(name) == -1){
        target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
    }

    });

    return proxy;
}


var customer = new Customer();

customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation:  ["name", "surname"]
customer.save();

如需查看更多 Reflect API 示例,请参阅 Tagtree 的 ES6 代理

对 Object.observe() 进行多重填充

虽然我们要向 Object.observe() 说再见,但现在可以使用 ES2015 代理对其进行 polyfill。Simon Blackwell 最近编写了一个基于代理的 Object.observe() shim,值得一试。Erik Arvidsson 早在 2012 年就编写了一个相当符合规范的版本。

浏览器支持

Chrome 49、Opera、Microsoft Edge 和 Firefox 支持 ES2015 代理。虽然 Safari 对此功能的公开信号褒贬不一,但我们仍然保持乐观。Reflect 已在 Chrome、Opera 和 Firefox 中推出,并且正在为 Microsoft Edge 开发中。

Google 已发布适用于代理的有限兼容性插件。此方法只能用于通用封装容器,因为它只能代理在创建代理时已知的属性。

深入阅读