w3ctech

使用ES6 代理创建“自卫”对象

翻译:Arthur Tsia 原作者:Nicholas C. Zakas 2015/2/5 月影校订

过去的一周里,我花了一个小时调试一个问题,最终我跟踪到了一个愚蠢的问题,。 在给定的对象上,我引用了一个根本不存在的属性。我引用了request.code属性, 而实际我应该引用的是 request.query.code。在再三纠结自己为啥没能及早发现这个问题后,一个根本性的问题让我警觉。这个也是很多JavaScript 反对者一直诟病的类型安全问题。

在这种情况下,这些反对者,是正确的。如果我使用类型安全的编程语言,那么在属性不存在的情况下,我会收到错误提醒,也会由此节约掉我生命中的一小时。这个问题已经不是我第一次碰到的类型错误,似乎还会再遇到。每次碰到这种情况,我会停下手边的工作,思考通过什么方法可以避免类型错误的发生。但是,一直也没有找到一个靠谱的答案或者说解决方案,直到 ECMAScript 6的出现

ECMAScript 5

虽然ECMAScript5 确实对于操纵已经存在的对象属性方面做得很漂亮,但是,对于那些对象属性不存在的情况,它确无能为力。通过ES5,你可以操作一个存在的对象属性不被覆盖or重写(设置 writable 为 false即可)或者被删除(设置 configurable 为false)。你可以操作一个对象不被赋予新的属性(通过使用 Object.preventExtensions())或者 设置一个对象所有属性为只读且不能被删除(Object.freeze())

如果你不想一个对象的所有属性都设置为只读,你可以使用 Object.seal().这个方法,它可以禁止新的属性被添加,已经存在的属性被删除,其他都和操作一个对象属性的方法是一样的,比如读、写。在ES5中,这是最接近我的期望,通过 Object.seal()方法来约束特殊对象的接口 一个约束对象,在严格模式下,当你尝试给一个对象增加新属性时,会抛出错误

 "use strict";

var person = {
    name: "Nicholas"
};

Object.seal(person);

person.age = 20;    // Error!

当你尝试通过添加新属性来改变一个对象接口时,这个方法非常奏效,它会提示你。 美中不足地,当你尝试读取一个不存在的对象[原文接口]属性时,它不会抛出错误。

用ES6 代理来解决

代理(Proxy)在ES6里经历了曲折的历史。在TC-39决定以非常戏剧性的方式改变代理之前,早期的提案在Firefox和Chrome两者间都被执行。在我看来,这些变化是向着好的方向发展地,因为他们从原有的代理提案中解决掉了不少未完善的地方。(针对早期提案,我做过部分研究)

最大的改变就是引入了目标对象与代理对象之间的交互。取而代之的不是为特定操作定义拦截器,而是直接通过代理对象来拦截对目标对象的操作。代理通过一系列的方法对应ES底层的操作。比方说,无论何时当你读取一个对象属性的值时,是有一个名叫 [[Get]]的操作由JS引擎来执行。这个[[Get]]操作是内置行为,不能改变。可是呢,代理对象允许你去“拦截”对 [[Get]]的调用,执行你想要的逻辑行为。考虑一下下面的代码

var proxy = new Proxy({ name: "Nicholas" }, {
    get: function(target, property) {
        if (property in target) {
            return target[property];
        } else {
            return 35;
        }
    }
});

console.log(proxy.time);        // 35
console.log(proxy.name);        // "Nicholas"
console.log(proxy.title);       // 35

代理使用了一个新对象作为目标对象(Proxy()方法的第一个参数)。第二个参数是你想要拦截的操作。get方法对应底层的[[Get]]操作(只要其他操作未被捕获,都是正常执行)。拦截器接收目标对象为第一个参数,属性名为第二个参数。上面的代码检查目标对象是否有该属性,有则返回适当的值。如果目标对象没有这个属性,这个拦截方法设定为忽略这两个参数,总是返回35。所以呢无论访问哪个不存在的属性,返回值都是35。

“自卫”起来

了解如何截获对[[Get]]的操作,我们就能创建一个“自卫”对象。我之所以说“自卫”,原因是他们的行为很像一个“自卫”的青年,他们试图维护他们的独立,纵然父母对他们有看法(“我不是一个孩子了,你们为什么还一直拿我当个小孩子呢???”),目标是当访问一个不存在的属性时,抛出错误(“我不是一个傻子,你们为什么还一直拿我当个傻子呢!!!”), 使用get 捕获器是可以实现的,只需要一点点代码。

function createDefensiveObject(target) {

    return new Proxy(target, {
        get: function(target, property) {
            if (property in target) {
                return target[property];
            } else {
                throw new ReferenceError("Property \"" + property + "\" does not exist.");
            }
        }
    });
}

createDefensiveObject()函数接收一个目标对象并且生成一个自卫对象。当属性被访问时,代理通过get拦截器来检查该属性。如果目标对象有该属性,返回对应属性值,如果没有,则会抛出错误,下面有个例子:

var person = {
    name: "Nicholas"
};

var defensivePerson = createDefensiveObject(person);

console.log(defensivePerson.name);        // "Nicholas"
console.log(defensivePerson.age);         // Error!

看,name属性有返回值,没有罢工,而 age 属性则抛出了错误提示。 自卫对象允许所有已存在的属性可读,不存在的属性访问是要抛出错误的。然而,你同样可以增加新的属性而不会报错

var person = {
    name: "Nicholas"
};

var defensivePerson = createDefensiveObject(person);

console.log(defensivePerson.name);        // "Nicholas"

defensivePerson.age = 13;
console.log(defensivePerson.age);         // 13

所以自卫对象保留了改变的能力,除非你做点什么来改变这种状况。可以一直添加属性,但是访问一个不存在的属性就会抛出错误而不仅仅返回 undefined

标准的特性检测技术仍会正常工作而不会有错

var person = {
    name: "Nicholas"
};

var defensivePerson = createDefensiveObject(person);

console.log("name" in defensivePerson);               // true
console.log(defensivePerson.hasOwnProperty("name"));  // true

console.log("age" in defensivePerson);                // false
console.log(defensivePerson.hasOwnProperty("age"));   // false

接下来,你可以真正意义上防卫一个对象的接口了,当访问一个不存在的属性时,禁止添加和报错,只需要几步

var person = {
    name: "Nicholas"
};

Object.preventExtensions(person);

var defensivePerson = createDefensiveObject(person);


defensivePerson.age = 13;                 // Error!
console.log(defensivePerson.age);         // Error!

这种情况下,当你尝试读取或者写入一个不存在的属性时,defensivePerson抛出了错误。 这会很有效地模拟类型安全的语言来验证接口

在定义数据接口或者构造器时,也许是使用自卫对象的最佳时刻,因为这通常表明你拥有一个清晰定义的想去维持的约定。比如:

function Person(name) {
    this.name = name;

    return createDefensiveObject(this);
}

var person = new Person("Nicholas");

console.log(person.age);         // Error!

通过在一个构造器内部调用createDefensiveObject()方法,你可以有效地确保Person所有的实例都是 “自卫的”

结论

最近JavaScript已经取得了很大的进展,但是我们还是有很多方法可以达到安全类型语言鼓吹的节省时间功能的相同效果。需要时,ES6代理提供了一个强大的方法来执行接口协议。最有用地使用场景是用在构造器或者ES6的类[classes],但是对于其他对象转成自卫对象同样有效。构建自卫对象的目地是使错误更明晰,更可见。他们可能并不适合所有的对象,但当定义API接口协议时,自卫对象确实还是有用武之地的。

原文地址 http://www.nczonline.net/blog/2014/04/22/creating-defensive-objects-with-es6-proxies/

w3ctech微信

扫码关注w3ctech微信公众号

共收到1条回复

  • cool

    回复此楼