ES2015 代理简介

艾迪·奥斯曼
Addy Osmani

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

代理有很多用途,包括:

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

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

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

代理的行为由 handler 控制,该处理程序可以通过很多有用的方式修改 target 对象的原始行为。处理程序包含可选的陷阱方法(例如 .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 或属性设置会导致对处理程序上的相应 trap 的元级调用。处理程序操作包括属性读取、属性分配和函数应用,所有这些操作都会被转发到相应的 trap。

trap 函数可以视需要任意实现某项操作(例如将操作转发到目标对象)。如果未指定 trap,默认情况下就会发生这种情况。例如,以下空操作转发代理可以执行以下操作:

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() trap:

// 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

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

由于每个操作都必须以不同的方式转发,因此这可能难以抽象化。在理想情况下,如果所有操作都可以通过一个 trap 统一漏斗,则处理程序只需在单个 trap 中执行一次验证检查。为此,您可以将代理处理程序本身作为代理。很遗憾,这超出了本文讨论范围。

对象扩展

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

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 并不需要将它当作动态语言。可以辩称,ES5 已经具有很多反射功能,例如 Array.isArray()Object.getOwnPropertyDescriptor(),在其他语言中会被视为反射。ES2015 引入了 Reflection API,该 API 将用于存储此类未来推出的方法,使其更易于推断。这是合理的,因为对象是基础原型,而不是反射方法的桶。

使用 Reflect,我们可以改进之前的超级英雄示例,以便在 get 和 set trap 上实现适当的场区拦截,如下所示:

// 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 代理

Polyfilling Object.observe()

虽然我们要告别 Object.observe(),但现在可以使用 ES2015 代理对它们执行 polyfill 操作。Simon Blackwell 最近编写了一个基于代理的 Object.observe() shim,值得一看。埃里克·阿维德森还在 2012 年就编写了一个相当完整的规范版本。

浏览器支持

Chrome 49、Opera、Microsoft Edge 和 Firefox 支持 ES2015 代理。Safari 对这项功能的公开信号不尽相同,但我们仍保持乐观。Reflect 适用于 Chrome、Opera 和 Firefox,并且正在开发中,以供 Microsoft Edge 使用。

Google 发布了适用于代理的有限 polyfill。此属性只能用于通用封装容器,因为它只能代理创建代理时已知的属性。

深入阅读