« 回到博客列表

每天一点ES6(18):Class

Tags: es6, class

过去

从我们学习 JavaScript 的第一天起,我们就被告知,JavaScript 是面向对象的,虽然它的语法和传统的 C 风格相去甚远,甚至有人并不认可 JavaScript 是面向对象的。但不管怎么说,这并不影响我们使用 JavaScript 去实现面向对象。

在过去(也就是 ES6 之前的时代),我们使用这样的语法来定义一个类:

如今,你依然可以通过这种方式去定义一个类,当然我推荐你一个更好的方式:

现在

ES6 对开发者影响最大的改变之一,就是改变了类定义的语法(或者说“新增”比较合适,毕竟老的依然能用,只是不再推荐),使得在 JavaScript 中定义类的语法更加接近 C 风格,现在你可以使用如下的代码来重写上面的代码:

运行一下,结果没有什么不同,但是语法看上去更符合我们的认知(请原谅一个毕业自常规本科教育的开发者的世界观)

语法糖

类定义语法的变更带给我的第一个疑虑是:我们接触了这么久的原型链机制,是否在 ES6 的新语法下也被改变了呢?并没有!

新语法的加入并没有改变 JavaScript 基于原型链的本质,只是在语法上更加符合了 C 风格。上面代码中的constructortoString其实就是Point.prototype.constructorPoint.prototype.toString,Point 本身的类型也还是函数(就是constructor),一切只是换了个马甲而已,并没有本质上的区别。

如果我们把两种语法的代码整理成下面的样子,一切就很好理解了:

还是有变化的

新的语法在使用上同样也受到其它 ES6 特性的影响,例如:新语法定义的类方法是不可枚举的,Object.keys不会得到任何内容,但Object.getOwnPropertyNames可以。类的属性名也可以采用表达式,[variable]是一个合法的属性名,具体取决于variable变量的取值,当然这不仅仅局限于类,这是 ES6 中对象的新特性。

严格模式

类内部是采用严格模式的,事实上 ES6 的另一个新特性“模块”(我们会在下一篇讲到)也是基于严格模式的,考虑到从 ES6 开始的代码都是运行在模块之中,可以说 ES6 在语言层面上就是按照严格模式来设计的,我们在使用时候就直接默认遵循严格模式来就好了,不用再做区分了。

构造函数

类的构造函数可以为空,甚至省略(JS 引擎会自动创建一个空的构造函数,因此实际上等效于为空),默认返回实例对象,但允许指定返回另一个对象。

类必须使用new进行调用,不能像普通函数一样直接调用,否则会报错。

Class 表达式

和函数表达式类似,类也支持这样的写法:

需要注意的是,和函数表达式一样,MyClass才是真正的类名,Me只在类内部可以作为一个内部代号,指代自身,在类外部是访问不到的。

立即执行的 Class

听说过立即执行函数吧,这个也差不多,同样是临时定义立即调用:

变量不提升

类定义不存在变量提升,这一点有些特殊,但为了实现继承,这是必须的。

从最佳实践的角度,为了不给开发过程埋坑,最好还是手动提升变量,即始终遵循“先定义后使用”的规则,绕开可能的变化。即便发生提升也会得到相同的结果。

私有属性和私有方法

在传统的 C 风格中,属性前面都会有publicprotectedprivate三大修饰符加持。虽然类语法的存在是为了让类定义的代码看起来更接近 C 风格,但 ES6 并没有加入这些修饰符,事实上,ES6 中类的属性和方法都还是公开的,虽然我们可以通过变量名前加下划线、立即执行函数、把私有变量移出模块等“自己骗自己”的方法来模拟,但这些都不是真正的私有。

关于这个问题,有一个颇具争议但看样子依然会被写进标准的提案,建议在属性名前加#来表示私有变量,并且在语言层面上支持私有变量。根据提案,#并不算是修饰符,而是变量名的一部分,也就是说#xx是两个变量,虽然语法上允许这种形式的“重名”,但实际使用中建议还是避免这种骚操作。

至于为什么会选择#这个符号,据说主要有两个原因,一个是为了保留 JavaScript 是动态语言(没有类型申明)的特点,private不太适合,因此只能选择一个新的符号。Ruby 使用了@,但 ES6 在此之前已经把@用于了装饰器(后面文章会讲到),因此一通 pass 下来,好像就#还不错,就它了。

比较奇怪的是,尽管私有属性在实例上不可访问,但可以在类内部定义一个常规方法,接收一个实例作为参数,在方法中直接访问该实例的私有属性。这算是一个骚操作吧,不是很能理解为什么要这么设计,如果这个操作是通过调用 getter 函数来间接访问私有变量,似乎就合理多了。

this

类内部的this一律指向类的实例,只要你不做什么骚操作,你几乎不用担心this会指错。如果你非要展现自己风骚的一面,那就自求多福吧。(方法自然是有的,但你还是不知道的好,免得知道以后乱用)

name

由于类的本质依然是函数,因此函数的name属性在类中依然适用,可以返回类名。

Generator

如果某个方法之前加上了*,它就是一个 Generator 函数;特殊一点,如果这个方法名刚好叫* [Symbol.iterator],恭喜你,实例化这个类你就过得了一个遍历器,至于具体能遍历出啥来,就看你方法中怎么定义了。

静态方法

类语法支持定义静态方法,语法非常符合认知:(两种定义方法都可以)

注意,静态方法中的this指向的是类本身,而不是实例,也就是这里的this只能访问静态方法和静态变量。

静态属性

ES6 规定类内部只有静态方法,没有静态属性,但后续的提案很快就把这块加上了。当然 ES5 的语法依然有效。

new.target

这算是一个骚操作吧——给关键字加属性。

这个属性用在构造函数中(无论是 class 还是 function),返回new命令作用于的那个构造函数,如果构造函数不是通过new调用的,new.target会返回undefined

这个属性可以用来确定构造函数是怎么调用的,比如强制构造函数只能用new调用,或者强制某各类不能直接实例化,必须继承后才可实例化。(子类继承父类时,new.target返回子类)

继承

Class 提供了extends关键字用于实现继承,相比修改原型链的写法,新语法更直观方便。

子类构造函数中必须首先调用super(),否则子类无法得到this。子类自己的this依赖于父类的this,需要先得到父类同样的实例属性和方法,然后再覆盖以自己的实例属性和方法。

子类可以省略构造函数,这个方法会被默认添加,并传入和父类构造函数相同的参数,等于直接取得父类中定义的内容。如果你需要自定义子类的实例化方式,就需要遵循上面的步骤,手动调用super()

继承机制

ES5 中,本质是先构造子类的this,再添加父类的属性(Parent.apply(this)),最后添加子类的属性。

ES6 中,是先通过父类获取this,再添加子类的属性,所以子类构造函数必须先调用super()才能正常访问this

Object.getPrototypeOf()

Object.getPrototypeOf()用于获取一个子类的父类、(你或许见过用__proto__来获取原型的方式,这是现代浏览器自己实现的快捷方式,不属于语言标准,Node.js 也不支持这样的语法,不建议使用)

super

super既可以作为函数(用在构造函数中,代表父类的构造函数,返回父类实例),也可以作为对象(用在子类实例方法中,指向父类的原型对象;用在子类静态方法中,指向父类)。

super用作函数时,虽然它代表父类的构造函数,但返回的是子类的实例,即super内部的this指的是子类,因此super相当于Parent.prototype.constructor.call(this)。如果在父类的构造函数里执行console.log(new.target.name),则它的子类在执行super()时这段代码会打出子类的构造函数,而不是父类。

super出现在子类的实例方法中时,由于它指向父类的原型对象,因此只有定义在父类原型上的属性和方法才能被super访问到(Parent.prototype.XXX),定义在父类实例上的属性和方法super是访问不到的。子类实例方法在通过super调用父类方法时,方法内部的this指向当前子类实例(super.method.call(this)),如果通过super对某个属性赋值(不太能想象什么场景下会这么做,因此还是避免这种骚操作比较好),这时的super就是this,即该属性被认为是子类的实例属性。通过这种方式定义的属性,在后续的代码中可以通过this.XXX访问到,但如果访问super.XXX,实际访问的是Parent.prototype.XXX

spuer出现在子类的静态方法中时,由于它指向父类,因此只能访问到静态的属性和方法。子类静态方法中通过super调用父类方法时,方法内部的this指向子类,而不是子类的实例。

super在使,用时必须显式地指定是作为函数还是对象使用,否则会报错(比如console.log(super))。一般情况下,在构造函数中用作函数,在方法中用作对象,应该是遇不到这类问题的。

由于 JavaScript 中的对象总是继承自其他对象的,所以可以在任何对象中使用super关键字。

小结

Class 的出现给我们写代码的方式带来了巨大的改变,但更多的是语法层面的改变。如果你来自 Java、C# 等传统编程语言,你会觉得一切都是那么的熟悉。

原本的老方法得到了保留,如果你是个怀旧的人,你可以继续使用你熟悉的语法,但从发展的眼光来看,我们还是建议尽早迁移到新的语法。

由于一些历史原因,JavaScript 的设计存在大量的缺陷,ES6 的发布掀起了对 JavaScript 大修补的热潮,现在的 JavaScript 正在集各家之所长,快速成长。

下一篇,我们将探索可能是 ES6 中对未来影响最大的一项改变 —— 模块。

该系列的其他文章

上一篇:每天一点ES6(17):Async / Await

下一篇:每天一点ES6(19):Module