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 |
|
这代码不仅仅很繁琐,它还离理想的有些远。它要求对函数的工作方式拥有很好的理解,这很重要,同时,了解各种将属性设置到创建的实例对象中。如果这方法看起来很复杂,不要担心。这整篇文章的目的就是展示一种很简单的方式来做到这些事情。
定义方法的语法
在第一次尝试新方法之前,ES6提供了一种新的语法来为一个对象增加特殊的属性。对于上面的例子,它可以很容易地为Circle.prototype
添加area
的方法,它会让 radius
的setter/getter方法更为有效些。作为JS一个面向对象的新方法,人们很乐意去使用一个设计更为精简的对象访问方式。我们需要一个新的方式来为一个对象增加方法,比如添加时如obj.prop = method
,而不需要Object.defineProperty
这么笨重。人们希望可以让下列的事情更为简单:
- 为一个对象增加普通的函数属性。
- 为一个对象增加生成器函数属性。
- 为一个对象增加普通的访问函数属性。
- 为一个已经完成的对象,通过
[]
来进行以上三种的操作。我们将这称为 计算属性名称(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 |
|
利用新的语法,我们可以编写如下
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 |
|
显然,这代码与之前的并不完全一样。这里的对象方法定义是可以配置和可列举的,但是在开始的代码中并不没有配置和列举。在实践中,这是很少会引起注意,我为了简洁会减少之前的其中列举和配置。
接着,这代码是不是更好些了,对不对?不幸的是,即使有新的方法定义语法,我们还是没有很多方式来定义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 |
|
哇!我们不仅可以将与Circle
有关的所有方法整合一起,而且所有事情都看起来很好很简洁。这肯定要优于我们开始做的代码。
尽管如此,你可能还存在部分的边界上问题。我在这里预测并定位一些问题:
分号怎么样? – 有个趋向是“使JS类更像传统中的类”,所以我们决定用更传统的分隔符(分号)。不喜欢它?它是可选的,分号不是必须的。
如果我不想操作构造函数,但又想为已经创建好的对象增加方法,怎么办? – 这也可以。这
constructor
方法也是可选的。如果你没有定义构造函数,系统会默认如你编写constructor() {}
。构造函数可以是生成器吗? – 不!如果添加的构造函数不是普通函数时会出现
TypeError
的错误,这里生成器和访问器是一样的。我能不能定义
constructor
为一个计算属性名称? – 很不幸,不行。那样真的很难被解释到,所以我们不能这样子。如果你为一个计算属性定义一方法,并命名为constructor
,你也将会得到一个名为constructor
的方法,那么你也不会得到一个类的构造函数了(想要操作方法,又想成为构造函数,那是不可能的)。当我修改
Circle
的值时,会不会错误地生成一个新的Circle? – 不!就像函数的表达式一样,类会为给定的名称进行内部的绑定,这绑定不能为外部操作所改变,所以无论你怎么设置Circle的内部作用域中的值,Circle.circlesMade++
的值都会是期望中的一样。好吧,我可以将一个对象直接作为函数的参数,但新类语法并不能这样子 – 很幸运,ES6有这类的表达式。它可以命名也可以不命名,表现的效果与上面描述的是一样的,除了ES6不会在声明的作用域创建变量。
那它有上面提到可列举等恶作功能吗? – 人们都想做到这点,所以你可以给对象安装方法,但列举对象的属性时,你只可以获取给这对象增加的属性,这样也更为合理些。因此,类中的安装方法是配置型的,但并不可列举。
等,哪有实例变量?那static常量? – 你打到我了,你提到的这些并没在刚才的ES6的类定义中。但是,有好消息。它些已经和其它一些特性提到规格进程中了。我是十分支持使用
static
和const
在类语法中定义值的。实际上,它已经提到会议上了。我认为,我们可以在将来看到更多有关于此的讨论。好的,很好!我能使用它吗? – 并不能真正使用,但有些可用的工具(特别是 Babel ),所以今天你可以通过他们来使用类语法。很不幸运,在是所有主流的浏览器中运行还需要些时间。我已经把今天讨论到的都在Nightly version of Firefox中实现了,同时它也在Edge和Chrome实例了,但默认是不打开的。很不幸,现在看来在Safari中并没有实现。
Java 和 C++ 有子类和super关键字,但这里并没有提到,那JS呢 – 它可以。但是,这值得另外一文章来进行讨论。注意查看我们之后的关于子类的更新文章,同时我们还将讨论更多的JS类的功能。
如果没有 Jason Orendorff 和 Jeff Walden 负责的代码检查和指导,我将不会可能实现类。
下周,Jason Orendorff 会在一周的休息后回来,将讨论let
和const
主题。