ES6 in Depth: Classes

ES6 In Depth: Classes

今天,我们可以从这一系列的之前复杂的文章中得到些喘息的机会。现在没有新的、没见过的 生成器、代理,这些会影响JS内部运算逻辑的hook,没有新的数据结构可去除用户自己的需求解决方案。相反,我们来讨论些语法性的、惯用的、待解释清楚的旧问题:JS中的对象构建器。

问题

我们想要创建一个最为精典的面向对象原则的例子:圆形类。想象一下,我们为一个画布的库编写一个圆形类。在所有事情中,我们需要知道怎么做到以下项:

  • 在一画布中画一个圆形
  • 记录画布中圆形的所有数量
  • 记录每一圆形的半径,并确定如何读写这值
  • 计算圆形的面积

这用JS的语言可以这么说:我们首先应该创建一个函数构建器,然后为这个函数增加必要的属性,接着使用一个对象来替换构建器中的prototype的属性。当我们开始创建一个实例对象时,这个prototype对象会包含所有属性。即使一个简单的例子,当你输入所有数据时,多数这样的模板也就完成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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() {
    if (!Number.isInteger(radius))
      throw new Error("Circle radius must be an integer");
    this._radius = radius;
  }
});

这代码不仅仅很繁琐,它还离理想的有些远。它要求对函数的工作方式拥有很好的理解,这很重要,同时,了解各种将属性设置到创建的实例对象中。如果这方法看起来很复杂,不要担心。这整篇文章的目的就是展示一种很简单的方式来做到这些事情。

定义方法的语法

在第一次尝试新方法之前,ES6提供了一种新的语法来为一个对象增加特殊的属性。对于上面的例子,它可以很容易地为Circle.prototype添加area的方法,它会让 radius的setter/getter方法更为有效些。作为JS一个面向对象的新方法,人们很乐意去使用一个设计更为精简的对象访问方式。我们需要一个新的方式来为一个对象增加方法,比如添加时如obj.prop = method,而不需要Object.defineProperty这么笨重。人们希望可以让下列的事情更为简单:

  1. 为一个对象增加普通的函数属性。
  2. 为一个对象增加生成器函数属性。
  3. 为一个对象增加普通的访问函数属性。
  4. 为一个已经完成的对象,通过[]来进行以上三种的操作。我们将这称为 计算属性名称(Computed property names)。

有些事情在之前不能操作的。例如,没有一种方式来为obj.prop定义setter和getter方法。因此,新的语法是必定要添加的。你现在可以类似编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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) { ... }
};

利用新的语法,我们可以编写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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,就如我们必须定义函数。没有方法当你在定义函数时,又可以访问其属性。

类定义语法

尽管上面代码比较开始的代码已有改进了,但是它依然未达到人们想要的结果,人们希望在JS中有一个简洁的面向对象的解决方案。其它编程语言中有一结构来处理面向对象设计,他们称这结构为类(class)。

这很合理。接着,让我们开始添加类。

我们希望有一个系统,它允许我们增加一个构造函数的方法,其类似于为.prototype增加方法一般,以至于方法能出现在这个类的所有实例中。后来,我们拥有了喜爱的定义新方法的语法,我们肯定会使用它。然后,我们只需要有一种方式来区分哪个是作用类中所有实例的,哪个是作用特定的实例。在C++或Java,其关键字是static,这看起来也很好,我们使用它。

现在,需要在众多的方法中选择一个来作为构造函数,这很有用。在C++或Java中,它是和类史一样的,并且不会返回类型。因此,JS的构造函数也不会返回类型,同时我们需要一个.constructor的属性,为了向后兼容,让我们把这方法叫做constructor

把所有的东西结合一起,我们可能重新编写我们的Circle类,结果会是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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) {
    thsi._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有关的所有方法整合一起,而且所有事情都看起来很好很简洁。这肯定要优于我们开始做的代码。

尽管如此,你可能还存在部分的边界上问题。我在这里预测并定位一些问题:

  • 分号怎么样? – 有个趋向是“使JS类更像传统中的类”,所以我们决定用更传统的分隔符(分号)。不喜欢它?它是可选的,分号不是必须的。

  • 如果我不想操作构造函数,但又想为已经创建好的对象增加方法,怎么办? – 这也可以。这constructor方法也是可选的。如果你没有定义构造函数,系统会默认如你编写constructor() {}

  • 构造函数可以是生成器吗? – 不!如果添加的构造函数不是普通函数时会出现TypeError的错误,这里生成器和访问器是一样的。

  • 我能不能定义constructor为一个计算属性名称? – 很不幸,不行。那样真的很难被解释到,所以我们不能这样子。如果你为一个计算属性定义一方法,并命名为constructor,你也将会得到一个名为constructor的方法,那么你也不会得到一个类的构造函数了(想要操作方法,又想成为构造函数,那是不可能的)。

  • 当我修改Circle的值时,会不会错误地生成一个新的Circle? – 不!就像函数的表达式一样,类会为给定的名称进行内部的绑定,这绑定不能为外部操作所改变,所以无论你怎么设置Circle的内部作用域中的值,Circle.circlesMade++的值都会是期望中的一样。

  • 好吧,我可以将一个对象直接作为函数的参数,但新类语法并不能这样子 – 很幸运,ES6有这类的表达式。它可以命名也可以不命名,表现的效果与上面描述的是一样的,除了ES6不会在声明的作用域创建变量。

  • 那它有上面提到可列举等恶作功能吗? – 人们都想做到这点,所以你可以给对象安装方法,但列举对象的属性时,你只可以获取给这对象增加的属性,这样也更为合理些。因此,类中的安装方法是配置型的,但并不可列举。

  • 等,哪有实例变量?那static常量? – 你打到我了,你提到的这些并没在刚才的ES6的类定义中。但是,有好消息。它些已经和其它一些特性提到规格进程中了。我是十分支持使用staticconst在类语法中定义值的。实际上,它已经提到会议上了。我认为,我们可以在将来看到更多有关于此的讨论。

  • 好的,很好!我能使用它吗? – 并不能真正使用,但有些可用的工具(特别是 Babel ),所以今天你可以通过他们来使用类语法。很不幸运,在是所有主流的浏览器中运行还需要些时间。我已经把今天讨论到的都在Nightly version of Firefox中实现了,同时它也在Edge和Chrome实例了,但默认是不打开的。很不幸,现在看来在Safari中并没有实现。

  • Java 和 C++ 有子类和super关键字,但这里并没有提到,那JS呢 – 它可以。但是,这值得另外一文章来进行讨论。注意查看我们之后的关于子类的更新文章,同时我们还将讨论更多的JS类的功能。

如果没有 Jason Orendorff 和 Jeff Walden 负责的代码检查和指导,我将不会可能实现类。

下周,Jason Orendorff 会在一周的休息后回来,将讨论letconst主题。

Comments