Class 在编程语言里是 类 的意思,很多面向对象语言都有这个概念和写法。其实在 ES6 之前,JavaScript 根本没有 类 的概念,更别谈 类 继承的问题了。如果你非要说有,那只不过是采用类似如下的模式来模拟的:

function Person(name) {
this.name = name;
}

Person.prototype.showName = function() {
console.log(this.name);
};

var my = new Person('yix');

my.showName(); // "yix"

console.log(my instanceof Person); // true

它的基本流程是这样的,首先定义一个构造函数,在函数里定义相关的属性,然后在这个构造函数的原型上添加相关方法,接着通过关键词 new 来生成一个新的实例,最后在这个实例对象上调用构造函数的相关方法。

在代码的最后,我们还用 instanceof 操作符来判断 my 是否为 Person 的实例。

这样,通过 Person 构造函数生成的实例,都能共享它的方法。虽然,达到了类似 类 继承 的目的,但这种做法却有些曲折,而且,在代码理解上也显得不太直白。因为直观上如果是一个类的话,属性 和 方法 应该写在一起的。

于是,ES6 中引入了 Class。

一、Class 的定义

类的定义,它是通过关键词 class 然后后面跟着 类名,接着是一对花括号,花括号中是这个类的 属性 和 方法。

1.1 基本定义(声明定义)

我们首先来看下声明定义的方式,见以下代码:

class Person {
constructor(name) {
this.name = name;
}

showName() {
console.log(this.name);
}
}

var my = new Person('yix');

my.showName(); // "yix"

console.log(my instanceof Person); // true
console.log(typeof Person); // function

可以看到,类 里面的代码是两个方法,它们之间没有 逗号 隔开,并且是使用 ES6 对象中方法定义的简写形式。

注意,倘若你使用普通语法去定义里面的方法,则代码会出现报错。另外,类 中定义属性,必须使用 constructor 这个函数名,否者也无法正确或者 类 的属性。

这段代码中 类 的定义,与上面代码 构造函数 的定义最终得结果是等价的。并且通过 typeof Person 我们可以知道,Person 类 和 上面中构造函数一样,它们本质上都是 函数。因此,ES6 中 类 的定义可以看作是一种语法糖。

但它们之间有几点不同:

  • Class 内部代码的运行,默认是严格模式下,所以你无需手动设置
  • Class 中的方法都是不可枚举的
  • 调用 Class 定义的类,必须使用 new 来创建实例,否则会报错

不存在声明提升

在之前,函数声明可以被提升(hoisted)。即无论函数定义是在函数调用前、还是在调用后,代码都能正常运行:

sum(1, 5); // 6

function sum(num1, num2) {
console.log(num1 + num2);
}

但是 Class 并不存在这样的概念,如果不在声明 类 之前就实例化这个类,则会报错:

var my = new Person('yix'); // 错误: Person is not defined

my.showName();

class Person {
constructor(name) {
this.name = name;
}

showName() {
console.log(this.name);
}
}

1.2 表达式定义

我们都知道,函数定义通常有这么两种方式:函数声明、函数表达式。前面说过,类 在本质上也是函数,因此它和函数一样,也有这么两种形式。

通过 表达式 的形式来定义类,只需要将 类名 前置到 class 前面,像这样:

var Person = class {
constructor(name) {
this.name = name;
}

showName() {
console.log(this.name);
}
}

var my = new Person('yix');

my.showName(); // "yix"

console.log(my instanceof Person); // true
console.log(typeof Person); // "function"
console.log(Person.name); // "Person"

此时,我们看到 类名 为 Person 。

当然,如果你想省略将实例化对象赋值给变量这一步的话,你可以通过这样:

var my = new class {
constructor(name) {
this.name = name;
}

showName() {
console.log(this.name);
}
}('yix');

my.showName(); // "yix"

命名的表达式定义

上面的例子相当于创建了一个匿名的 类,为了方便起见,你也可以像命名函数那样,给 表达式 进行命名:

var Person = class PersonInner {
constructor(name) {
this.name = name;
}

showName() {
console.log(this.name);
}

showClassName() {
console.log(this.name);
}
};

var my = new Person('yix');

my.showName(); // "yix"

console.log(my instanceof Person); // true
console.log(typeof Person); // function
console.log(typeof PersonInner); // undefined
console.log(Person.name); // PersonInner

看到,代码依旧正常、等价的运行。只是不同的是,在 class 外部,PersonInner 的数据类型是 undefined 。

或许你会觉得,在 class 后面跟着一个 标识符 有什么用呢?因为它的值是 undefined,在 class 外部,它根本不具备标识类的功能啊。

的确如此,PersonInner 可以用在内部,表示当前类:

var Person = class PersonInner {
showClassName() {
console.log(PersonInner.name);
}
};

var my = new Person();

my.showClassName(); // "PersonInner"

它只能在类的方法定义中,如果用在 constructor 里,则会报错:

var Person = class PersonInner {
constructor(name) {
this.name = name;
}

showName() {
console.log(PersonInner.name);
}
};

var my = new Person('yix');

console.log(Person.name); // "PersonInner"

二、Class 的继承

在 ES6 中,我们可以通过 extends 关键字来轻松的实现继承,如下:

class A {
//...
}

class B extends A {
//...
}

上面的代码表示,类 B 继承了 A 的所有属性和方法。

但是,因为子类自身没有 this 对象。它必须通过在子类的 constructor 函数中使用 super 关键字,调用 super() 方法。这样才能获取到对应的 this,以下是最简单的 super 用法:

class A {

}

class B extends A {
constructor() {
// console.log(this); // 错误:this is not defined
super();
console.log(this); // B {}
}
}

new B();

由上面的代码,我们可以知道,在 super() 调用之后, 返回了 子类 的实例,才能使用 this 对象。否者,会抛出错误。

这里演示了 B类 继承了 A类 的 showName 方法:

class A {
showName() {
console.log(this.name);
}
}

class B extends A {
constructor(name) {
super();
this.name = name;
}
}

var b = new B('bbb');

b.showName(); // "bbb"

super 一般有两种用途,即 函数调用 和 对象访问。

super 的函数调用

函数调用 是最常用的方法,它主要用于 子类 继承 父类 时,在 constructor 函数中作为 父类 构造函数的引用,返回 子类 的实例,这样,我们就能在 子类 中使用 this 对象:

class A {
constructor() {
console.log(new.target.name);
}
}

class B extends A {
constructor() {
super();
}
}

new A(); // A
new B(); // B

在上面的代码中,new.target 表示当前执行的函数。这里的 new A()new B() 返回的结果都是自身的 类 的构造函数。

super 的对象访问

而对于 对象访问,则表示在 子类 中,super 这个对象是父类的 原型对象。比如:

class A {
showNumber() {
return 1;
}
}

class B extends A {
constructor() {
super();
console.log(super.showNumber()); // 1
}
}

new B();

上面代码中,类A 的原型上定义了一个方法 showNumber,所以在 类B 中 super.showNumber 就表示访问 类A 原型对象上的 showNumber 方法,因此返回 1 。

三、Class 构造函数中 new.target

还记得吗?在描述 Class 的继承 这一小节中,我们提到了 new.target 属性,它的值取决于是在哪个 Class 下的函数里被使用。这样的话,我们可以通过它,来确定哪个类被调用了。

来个最简单的验证:

class A {
constructor() {
console.log(new.target === A);
}
}

var a = new A(); // true

但对于有继承关系的实例化对象,则 new.target 将返回的是 子类 。

class A {
constructor() {
console.log(new.target === A);
console.log(new.target.name);
}
}

var a = new A();

// true
// A

class B extends A {

}

var b = new B();

// false
// B

不过要注意的是,new.target 属性只能使用在 constructor 函数内部,否者会报错。

四、Class 的静态方法

什么是静态方法?我们知道,Class 中定义的方法,都会被其创建的实例所继承。但如果你不希望实例继承 类 的方法,你可以在定义方法时,在前面加上关键词 static,而这种通过前置 static 关键词所定义的方法,就是 静态方法。

静态方法,只能通过 Class 自身进行调用。来看下相关演示:

class A {
showName() {
console.log('yix');
}

static showJob() {
console.log('web');
}
}

var a = new A();

A.showJob(); // "web"

a.showName(); // "yix"
a.showJob(); // 错误:a.showJob is not a function

上面的代码分别在 A类 上定义了一个 普通(动态或实例)的方法 showName 和 静态方法 showJob。可以看到,静态方法只能在 A类(类本身)上进行调用,而在实例上则直接跑错。

五、Class 的成员名称

像以字面量的形式定义对象一样,Class 中的 方法 名不一定非要使用 标识符,你同样也可以使用计算名,比如:

var funcName = 'showName';

class A {

constructor(name) {
this.name = name;
}

[funcName]() {
console.log(this.name); // "yix"
}
}

var a = new A('yix');

a.showName();

六、Class 的Generator方法

类似的,也可以在 Class 中添加 Generator函数 :

class A {

constructor(arr) {
this.arr = arr;
}

*showNumber() {
for (var i of this.arr) {
yield i;
}
}
}

var a = new A(['aa', 'bb', 'cc']),
g = a.showNumber();


console.log(g.next().value);
console.log(g.next().value);
console.log(g.next().value);

// "aa"
// "bb"
// "cc"

七、Class 的 get 和 set

在 ES5 中,我们可以通过关键词 getset 来实现 getter 和 setter,从而读取和设置对象的属性。

而在 ES6 的 Class 中,虽然有关属性的操作都是在构造函数 constructor 中,但通过上面的描述,我们知道 对象 与 Class 有很多相似之处,当然,我们也可以通过 getset 来进行属性的操作:

class A {
constructor(arr) {

}

get prop() {
return 'aa';
}

set prop(value) {
console.log('新值: '+ value);
}
}

var a = new A();

console.log(a.prop); // "aa"

a.prop = 'bb'; // 新值: "bb"

由上面的代码,我们可以知道,当 Class 属性被设置新值时,无需读取,对应的属性值就立马呈现出来。很多前端 MVVM 库中的单向数据绑定,都是基于此原理来实现的。