ES6中的类
Class 在编程语言里是 类 的意思,很多面向对象语言都有这个概念和写法。其实在 ES6 之前,JavaScript 根本没有 类 的概念,更别谈 类 继承的问题了。如果你非要说有,那只不过是采用类似如下的模式来模拟的:
function Person(name) { |
它的基本流程是这样的,首先定义一个构造函数,在函数里定义相关的属性,然后在这个构造函数的原型上添加相关方法,接着通过关键词 new 来生成一个新的实例,最后在这个实例对象上调用构造函数的相关方法。
在代码的最后,我们还用 instanceof
操作符来判断 my 是否为 Person 的实例。
这样,通过 Person 构造函数生成的实例,都能共享它的方法。虽然,达到了类似 类 继承 的目的,但这种做法却有些曲折,而且,在代码理解上也显得不太直白。因为直观上如果是一个类的话,属性 和 方法 应该写在一起的。
于是,ES6 中引入了 Class。
一、Class 的定义
类的定义,它是通过关键词 class
然后后面跟着 类名,接着是一对花括号,花括号中是这个类的 属性 和 方法。
1.1 基本定义(声明定义)
我们首先来看下声明定义的方式,见以下代码:
class Person { |
可以看到,类 里面的代码是两个方法,它们之间没有 逗号 隔开,并且是使用 ES6 对象中方法定义的简写形式。
注意,倘若你使用普通语法去定义里面的方法,则代码会出现报错。另外,类 中定义属性,必须使用 constructor
这个函数名,否者也无法正确或者 类 的属性。
这段代码中 类 的定义,与上面代码 构造函数 的定义最终得结果是等价的。并且通过 typeof Person
我们可以知道,Person
类 和 上面中构造函数一样,它们本质上都是 函数。因此,ES6 中 类 的定义可以看作是一种语法糖。
但它们之间有几点不同:
- Class 内部代码的运行,默认是严格模式下,所以你无需手动设置
- Class 中的方法都是不可枚举的
- 调用 Class 定义的类,必须使用
new
来创建实例,否则会报错
不存在声明提升
在之前,函数声明可以被提升(hoisted)。即无论函数定义是在函数调用前、还是在调用后,代码都能正常运行:
sum(1, 5); // 6 |
但是 Class 并不存在这样的概念,如果不在声明 类 之前就实例化这个类,则会报错:
var my = new Person('yix'); // 错误: Person is not defined |
1.2 表达式定义
我们都知道,函数定义通常有这么两种方式:函数声明、函数表达式。前面说过,类 在本质上也是函数,因此它和函数一样,也有这么两种形式。
通过 表达式 的形式来定义类,只需要将 类名 前置到 class 前面,像这样:
var Person = class { |
此时,我们看到 类名 为 Person 。
当然,如果你想省略将实例化对象赋值给变量这一步的话,你可以通过这样:
var my = new class { |
命名的表达式定义
上面的例子相当于创建了一个匿名的 类,为了方便起见,你也可以像命名函数那样,给 表达式 进行命名:
var Person = class PersonInner { |
看到,代码依旧正常、等价的运行。只是不同的是,在 class 外部,PersonInner 的数据类型是 undefined 。
或许你会觉得,在 class 后面跟着一个 标识符 有什么用呢?因为它的值是 undefined,在 class 外部,它根本不具备标识类的功能啊。
的确如此,PersonInner 可以用在内部,表示当前类:
var Person = class PersonInner { |
它只能在类的方法定义中,如果用在 constructor 里,则会报错:
var Person = class PersonInner { |
二、Class 的继承
在 ES6 中,我们可以通过 extends
关键字来轻松的实现继承,如下:
class A { |
上面的代码表示,类 B 继承了 A 的所有属性和方法。
但是,因为子类自身没有 this 对象。它必须通过在子类的 constructor
函数中使用 super
关键字,调用 super()
方法。这样才能获取到对应的 this,以下是最简单的 super
用法:
class A { |
由上面的代码,我们可以知道,在 super()
调用之后, 返回了 子类 的实例,才能使用 this 对象。否者,会抛出错误。
这里演示了 B类 继承了 A类 的 showName 方法:
class A { |
super
一般有两种用途,即 函数调用 和 对象访问。
super 的函数调用
函数调用 是最常用的方法,它主要用于 子类 继承 父类 时,在 constructor
函数中作为 父类 构造函数的引用,返回 子类 的实例,这样,我们就能在 子类 中使用 this 对象:
class A { |
在上面的代码中,new.target
表示当前执行的函数。这里的 new A()
和 new B()
返回的结果都是自身的 类 的构造函数。
super 的对象访问
而对于 对象访问,则表示在 子类 中,super
这个对象是父类的 原型对象。比如:
class A { |
上面代码中,类A 的原型上定义了一个方法 showNumber
,所以在 类B 中 super.showNumber
就表示访问 类A 原型对象上的 showNumber
方法,因此返回 1 。
三、Class 构造函数中 new.target
还记得吗?在描述 Class 的继承 这一小节中,我们提到了 new.target
属性,它的值取决于是在哪个 Class 下的函数里被使用。这样的话,我们可以通过它,来确定哪个类被调用了。
来个最简单的验证:
class A { |
但对于有继承关系的实例化对象,则 new.target
将返回的是 子类 。
class A { |
不过要注意的是,new.target
属性只能使用在 constructor
函数内部,否者会报错。
四、Class 的静态方法
什么是静态方法?我们知道,Class 中定义的方法,都会被其创建的实例所继承。但如果你不希望实例继承 类 的方法,你可以在定义方法时,在前面加上关键词 static
,而这种通过前置 static
关键词所定义的方法,就是 静态方法。
静态方法,只能通过 Class 自身进行调用。来看下相关演示:
class A { |
上面的代码分别在 A类 上定义了一个 普通(动态或实例)的方法 showName
和 静态方法 showJob
。可以看到,静态方法只能在 A类(类本身)上进行调用,而在实例上则直接跑错。
五、Class 的成员名称
像以字面量的形式定义对象一样,Class 中的 方法 名不一定非要使用 标识符,你同样也可以使用计算名,比如:
var funcName = 'showName'; |
六、Class 的Generator方法
类似的,也可以在 Class 中添加 Generator函数 :
class A { |
七、Class 的 get 和 set
在 ES5 中,我们可以通过关键词 get
和 set
来实现 getter 和 setter,从而读取和设置对象的属性。
而在 ES6 的 Class 中,虽然有关属性的操作都是在构造函数 constructor 中,但通过上面的描述,我们知道 对象 与 Class 有很多相似之处,当然,我们也可以通过 get
和 set
来进行属性的操作:
class A { |
由上面的代码,我们可以知道,当 Class 属性被设置新值时,无需读取,对应的属性值就立马呈现出来。很多前端 MVVM 库中的单向数据绑定,都是基于此原理来实现的。