[关闭]
@lenville 2015-10-31T00:16:20.000000Z 字数 4984 阅读 592

深入浅出ES6

深入浅出ES6(十三):类 Class

作者 Eric Faust 译者 刘振涛

译者按:ECMAScript 6已经正式发布了,作为它最重要的方言,Javascript也即将迎来语法上的重大变革,InfoQ特开设“深入浅出ES6”专栏,来看一下ES6将给我们带来哪些新内容。本专栏文章来自Mozilla Web开发者博客,由作者授权翻译并发布。

你可能觉得之前讲解的内容略显复杂,今天我们就讲解一些相对简单的内容,不再是生成器(Generator)这样前所未闻的全新编码方式,也不是诸如代理(Proxy)这种为JavaScript内部算法工作原理提供钩子的全能对象,更不是能够为开发提供便利的新型数据结构。简单来说,我们将要一起讨论如何根据语言习惯简化对象构造函数的创建过程。

目前面临的问题

假如我们想要创建一个经典的面向对象设计示例:Circle类。想象一下我们正在为一个简单的Canvas库编写这个Circle类,在众多需要考虑的因素中,我们可能更想了解以下功能的实现方式:

按照目前常见的JS编码风格,我们首先应该以函数的形式创建一个构造函数,然后给该函数添加任何我们可能想要的属性,然后用一个对象替换构造函数的prototype属性。这个prototype对象将包含构造函数创建的实例的所有初始化属性。下面是一个简单的示例,可以直接作为样板文件(boilerplate)重复使用:

  1. function Circle(radius) {
  2. this.radius = radius;
  3. Circle.circlesMade++;
  4. }
  5. Circle.draw = function draw(circle, canvas) { /* Canvas绘制代码 */ }
  6. Object.defineProperty(Circle, "circlesMade", {
  7. get: function() {
  8. return !this._count ? 0 : this._count;
  9. },
  10. set: function(val) {
  11. this._count = val;
  12. }
  13. });
  14. Circle.prototype = {
  15. area: function area() {
  16. return Math.pow(this.radius, 2) * Math.PI;
  17. }
  18. };
  19. Object.defineProperty(Circle.prototype, "radius", {
  20. get: function() {
  21. return this._radius;
  22. },
  23. set: function(radius) {
  24. if (!Number.isInteger(radius))
  25. throw new Error("圆的半径必须为整数。");
  26. this._radius = radius;
  27. }
  28. });

这段代码非常繁琐且不符合人的直觉,要想读懂必须对函数的运行方式有着非凡的掌握,然后你才能理解各种已装载的属性与生成的实例对象进行绑定的方式。如果这种方法看起来很复杂,不要担心,这篇文章会为你展示一种更简单的方法来实现所有这些功能。

方法定义语法

ES6提供一种向对象添加特殊属性的新语法,可以帮助我们清理这些方法。给Circle.prototype添加area方法非常简单,但是给radius添加getter/setter方法对就很难。随着JS引入越来越多的面向对象方法,人们开始对简化给对象添加访问器的方法感兴趣。我们需要一种功能类似obj.prop = method的新方法来给对象添加“方法”,同时不借助Object.defineProperty的力量。人们想要能够简单地实现以下功能:

其中一些功能在以前无法实现,例如:我们不能通过给obj.prop赋值来定义getter或setter。因此,我们亟需新语法来编写以下代码:

  1. var obj = {
  2. // 现在不再使用function关键字给对象添加方法
  3. // 而是直接使用属性名作为函数名称。
  4. method(args) { ... },
  5. // 只需在标准函数的基础上添加一个“*”,就可以声明一个生成器函数。
  6. *genMethod(args) { ... },
  7. // 借助|get|和|set|可以在行内定义访问器。
  8. // 只是定义内联函数,即使没有生成器。
  9. // 注意通过这种方式装载的getter不能接受参数
  10. get propName() { ... },
  11. // 注意通过这种方式装载的setter至少接受一个参数
  12. set propName(arg) { ... },
  13. // []语法可以用于任意支持预计算属性名的地方,来满足上面的第4中情况。
  14. // 这意味着你可以使用symbol,调用函数,联结字符串
  15. // 或其它可以给property.id求值的表达式。
  16. // 这个语法对访问器或生成器同样有效,我在这里只是举个例子。
  17. [functionThatReturnsPropertyName()] (args) { ... }
  18. };

现在,我们可以用这种新语法重写上面的代码片段:

  1. function Circle(radius) {
  2. this.radius = radius;
  3. Circle.circlesMade++;
  4. }
  5. Circle.draw = function draw(circle, canvas) { /* Canvas绘制代码 */ }
  6. Object.defineProperty(Circle, "circlesMade", {
  7. get: function() {
  8. return !this._count ? 0 : this._count;
  9. },
  10. set: function(val) {
  11. this._count = val;
  12. }
  13. });
  14. Circle.prototype = {
  15. area() {
  16. return Math.pow(this.radius, 2) * Math.PI;
  17. },
  18. get radius() {
  19. return this._radius;
  20. },
  21. set radius(radius) {
  22. if (!Number.isInteger(radius))
  23. throw new Error("圆的半径必须为整数。");
  24. this._radius = radius;
  25. }
  26. };

讲究地说,这段代码与上面的代码段并不完全相同,装载后的对象字面量中的方法定义是可配置(configurable)和可枚举(enumerable)的,然而在第一段代码段中却不是这样。事实上,很少有人会注意到这个问题,我决定为了简洁起见暂时省略可枚举性和可配置性。

不过,这段代码依然变得更好了,不是么?不幸的是,即使有了新的方法定义语法,我们仍然不能武装到牙齿,所以仍然需要通过定义函数来定义Circle类。没有一种方法能够让你在定义函数时就获取它的属性。

类定义语法

尽管这比以前更好,但是它仍然不能满足人们对于简洁的JavaScript面向对象解决方案的渴望。在其它语言中,有一个句法结构可以用来处理面向对象设计的问题,经过一番讨论后他们将其命名为类(Class)

好吧,让我们也来添加一些类。

我们需要这样一个系统:给命名构造函数添加方法的同时给函数的.prototype属性也添加相应方法,从而用这个类构造出的实例也包含相应的方法。既然我们掌握了一种崭新的方法定义语言,我们一定要物尽其用。在类的所有实例中,我们只需要一种区分普通函数与特殊函数的方法,在C++或Java中,这种功能对应的关键字是static。这种方法看起来不错,让我们用起来!

我们还需要一个方法,可以在一堆方法中指定出唯一的构造函数。在C++或Java中,构造函数与类同名,并且没有返回类型。既然JS没有返回类型,我们无论如何都需要一个.constructor属性来支持向后兼容性,你可以称之为方法构造函数(method constructor)。

将所有的概念组合到一起后,我们可以重写Circle类并实现所有功能:

  1. class Circle {
  2. constructor(radius) {
  3. this.radius = radius;
  4. Circle.circlesMade++;
  5. };
  6. static draw(circle, canvas) {
  7. // Canvas绘制代码
  8. };
  9. static get circlesMade() {
  10. return !this._count ? 0 : this._count;
  11. };
  12. static set circlesMade(val) {
  13. this._count = val;
  14. };
  15. area() {
  16. return Math.pow(this.radius, 2) * Math.PI;
  17. };
  18. get radius() {
  19. return this._radius;
  20. };
  21. set radius(radius) {
  22. if (!Number.isInteger(radius))
  23. throw new Error("圆的半径必须为整数。");
  24. this._radius = radius;
  25. };
  26. }

哇嗷!我们不仅可以实现Circle所需的功能,还能使代码如此简洁,这比刚开始好多了!

虽然如此,有的人有可能会遇到问题或碰到边缘用例。我会尝试着预测你们将会遇到的问题并一一解答:

感谢Jason OrendorffJeff Walden引导我设计这一功能并为我所有的实现做代码审查,正是有了他们我才能实现类的相关特性。

下一次,Jason Orendorff将会为我们深入浅出讲解letconst,欢迎继续加入我们!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注