深入 ES6 - 类

原文出自 ES6 in depths, 作者 Eric Faust, 翻译:落在深海

ES6 In Depth 系列将详细解读 ES6 的新特性。

我们从上周文章介绍的错综复杂后得到一丝喘息的机会。今天不会有从未见过的 generators 的写法; 没有操纵 Javascript 内部算法的 强大代理对象;没有避免自己刀工火种的新数据结构。取代的是,我们要讨论下句法和语义上一个遗留问题: Javascript 的对象构造。

问题所在

比方说,我们打算创建面向对象原则最经典的例子:Circle 类。幻想我们正使用 Canvas 库来画一个圆形。理想情况下,我们想要知道如何做到以下几点:

依照现在的 Js 写法,我们应该创建一个构造函数,并为函数添加我们需要的属性,然后用对象来替换构造函数的 prototype 属性。这里 prototype 对象包含了构造函数创建的实例对象的所有属性。再举个简单的例子,当你敲出所有代码,会有一对引用:

function Circle(radius) {
    this.radius = radius;
    Circle.circlesMade++;
}

Circle.draw = function draw(circle, canvas) { /* Canvas drawing code */ }

Object.defineProperty(Circle, "circlesMade", {
    get: function() {
        return !this._count ? 0 : this._count;
    },
    set: function(val) {
        this._count = val;
    }
});

Circle.prototype = {
    area: function area() {
        return Math.pow(this.radius, 2) * Math.PI;
    }
};

Object.defineProperty(Circle.prototype, "radius", {
    get: function() {
        return this._radius;
    },

    set: function(radius) {
        if (!Number.isInteger(radius))
            throw new Error("Circle radius must be an integer.");
        this._radius = radius;
    }
});  

代码冗长且反直觉。需要一个更直接的理解它如何工作的方式,以及众多属性如何被创建到实例对象上。本文旨在为达成这些提供更简单的方式。

方法定义

为解决这个问题,ES6 起初尝试为对象添加特殊属性提供一种新语法。尽管为 Circle.prototype 添加 area 方法很容易,然而 radius 的 getter/setter 方法似乎就太笨重了。由于 Js 发展的更接近面向对象,人们对设计更清洁的对象访问器越发感兴趣。针对对象添加方法,我们需要一种类似 obj.prop = method 的方式,而不是笨重的 Object.defineProperty。这样的方式需要很容易做到:

  1. 为对象添加普通函数属性。

  2. 为对象添加 generator 函数。

  3. 为对象添加访问器。

  4. 为已构造的对象,通过 [ ] 的语法添加上面三点的任何属性。这个被称作*被计算出的属性名称*。

放在以前,其中的几点很难实现。例如,根本没办法通过 obj.prop 来定义 getter 或 setter 方法。于是出现了新语法,你现在可以这样写:

var obj = {
    // Methods are now added without a function keyword, using the name of the
    // property as the name of the function.
    method(args) { ... },

    // To make a method that's a generator instead, just add a '*', as normal.
    *genMethod(args) { ... },

    // Accessors can now go inline, with the help of |get| and |set|. You can
    // just define the functions inline. No generators, though.

    // Note that a getter installed this way must have no arguments
    get propName() { ... },

    // Note that a setter installed this way must have exactly one argument
    set propName(arg) { ... },

    // To handle case (4) above, [] syntax is now allowed anywhere a name would
    // have gone! This can use symbols, call functions, concatenate strings, or
    // any other expression that evaluates to a property id. Though I've shown
    // it here as a method, this syntax also works for accessors or generators.
    [functionThatReturnsPropertyName()] (args) { ... }
};

使用新语法,我们可以重写之前的例子:

function Circle(radius) {
    this.radius = radius;
    Circle.circlesMade++;
}

Circle.draw = function draw(circle, canvas) { /* Canvas drawing code */ }

Object.defineProperty(Circle, "circlesMade", {
    get: function() {
        return !this._count ? 0 : this._count;
    },

    set: function(val) {
        this._count = val;
    }
});

Circle.prototype = {
    area() {
        return Math.pow(this.radius, 2) * Math.PI;
    },

    get radius() {
        return this._radius;
    },
    set radius(radius) {
        if (!Number.isInteger(radius))
            throw new Error("Circle radius must be an integer.");
        this._radius = radius;
    }
};

刨根问底,这段代码跟第一个例子并不完全等价。对象字面量里定义的方法是可配置和可枚举的,而第一个例子里的访问器并不是。实践中这点很少被提及,我们也暂时跳过这部分。

变得更好了不是么?遗憾的是,虽然用到了最新的语法,我们仍旧对定义 Circle 力不从心,你正在定义的属性,就没法获得到它。

类定义

到目前为止,还算不错,然而很难让那些想拥有更清洁面向对象设计语法的人得到满足。关于面向对象设计,其他语言有一种结构,他们议论到,那种结构叫做*类*。

有道理,那我们也加上类吧。

我们希望能够为构造器函数及它的 .prototype 添加方法,这样他们就会在类的实例对象作用域使用。既然有了新的方法定义语法,就一定得用上它。我们要做的仅仅是区分类方法和实例方法。在 C++ 或者 Java,这个关键字是 static,看起来不错,我们也这样用吧。

现在如果有某种方式,能从一堆方法里指定某个函数作为构造函数,将显得非常有用。C++ 或 Java 里, 他被定义为跟类名同名,没有返回类型。由于 Js 没有返回值一说,而且我们还是希望有个 .constructor 属性,为了向后兼容,那就称它为 constructor 吧。

组合起来,Circle 可以重写为:

class Circle {
    constructor(radius) {
        this.radius = radius;
        Circle.circlesMade++;
    };

    static draw(circle, canvas) {
        // Canvas drawing code
    };

    static get circlesMade() {
        return !this._count ? 0 : this._count;
    };
    static set circlesMade(val) {
        this._count = val;
    };

    area() {
        return Math.pow(this.radius, 2) * Math.PI;
    };

    get radius() {
        return this._radius;
    };
    set radius(radius) {
        if (!Number.isInteger(radius))
            throw new Error("Circle radius must be an integer.");
        this._radius = radius;
    };
}

哇哦,我们不仅把所有 Circle 相关的代码放一起了,而且所有的方法看起来如此干净。比第一个例子不知道好到哪里去了。

即使这样,你们可能还会举些极端例子。我早猜到了,这里先列出一部分:

没有 Jason OrendorffJeff Walden 的指导及频繁又细致的代码审查,我可能根本无法实现 Javascript 的类。

下周,Jason Orendorff 度假回来,来给大家讲解 let 跟 const。

comments powered by Disqus
返回 写的 拍的 标签