ECMAScript 2015 中引入的 JavaScript 类 是 JavaScript 的现有基于原型的继承的语法糖。 类并不是 JavaScript 里加入的新的面向对象的继承模型。JavaScript 中的类只是能让我们用更简洁明了的语法创建对象及处理相关的继承。

定义类

类实际上是个“特殊的函数”,而且正如函数的定义方式有函数声明函数表达式两种一样,类的定义方式也有两种,分别是:类声明类表达式

类声明

类声明是定义类的一种方式,就像下面这样,使用 class 关键字后跟一个类名(这里是 Polygon),就可以定义一个类。

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

提升

函数声明和类声明之间的一个重要区别是函数声明是hoisted,类声明不会。你首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError

let p = new Polygon(); 
// ReferenceError

class Polygon {}

类表达式

一个类表达式是定义类的另一种方式。类表达式可以被命名或未被命名。赋予命名类表达式的名称是类的主体的本地名称。

/* 匿名类 */ 
let Polygon = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

/* 命名的类 */ 
let Polygon = class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

注意: 类表达式也存在与类声明相同的提升问题。

类体和方法定义

类的成员需要定义在一对花括号 {} 里,花括号里的代码和花括号本身组成了类体。类成员包括类构造器和类方法(包括静态方法和实例方法)。

严格模式

类体中的代码都强制在严格模式中执行。

构造器

构造器方法是一个特殊的类方法,其用于创建和初始化对象(用该类生成的)。一个类只能拥有一个名为 constructor 的方法,否则会抛出 SyntaxError 异常。

在子类的构造器中可以使用 super 关键字调用父类的构造器。

原型方法

参见方法定义

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  
  get area() {
    return this.calcArea()
  }

  calcArea() {
    return this.height * this.width;
  }
}
const square = new Polygon(10, 10);

// 100
console.log(square.area);

静态方法

static 关键字用来定义类的静态方法。静态方法是指那些不需要对类进行实例化,使用类名就可以直接访问的方法,需要注意的是静态方法不能被实例化的对象调用。静态方法经常用来作为工具函数。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;

        return Math.sqrt(dx*dx + dy*dy);
    }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));

用原型和静态方法装箱

当调用静态或原型方法而没有值为“this”的对象时,(或“this”作为布尔,字符串,数字,未定义或null), ,那么“this”值将在被调用的函数内部未定义。 不会发生自动装箱。即使我们以非严格模式编写代码,行为也是一样的。

class Animal { 
  speak() {
    return this;
  }
  static eat() {
    return this;
  }
}

let obj = new Animal();
let speak = obj.speak;
speak(); // undefined

let eat = Animal.eat;
eat(); // undefined

如果我们使用传统的基于函数的类来编写上述代码,那么基于调用该函数的“this”值将发生自动装箱。

function Animal() { }

Animal.prototype.speak = function() {
  return this;
}

Animal.eat = function() {
  return this;
}

let obj = new Animal();
let speak = obj.speak;
speak(); // global object

let eat = Animal.eat;
eat(); // global object

使用 extends 创建子类

extends 关键字在类声明或类表达式中用于创建一个类作为另一个类的一个子类。

class Animal { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  speak() {
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
// 'Mitzie barks.'
d.speak();

如果子类中存在构造函数,则需要在使用“this”之前首先调用super()。

也可以扩展传统的基于函数的“类”:

function Animal (name) {
  this.name = name;  
}
Animal.prototype.speak = function () {
  console.log(this.name + ' makes a noise.');
}

class Dog extends Animal {
  speak() {
    super.speak();
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
d.speak();

请注意,类不能扩展常规(不可构造/非构造的)对象。如果要继承常规对象,可以改用Object.setPrototypeOf():

var Animal = {
  speak() {
    console.log(this.name + ' makes a noise.');
  }
};

class Dog {
  constructor(name) {
    this.name = name;
  }
  speak() {
    super.speak();
    console.log(this.name + ' barks.');
  }
}
Object.setPrototypeOf(Dog.prototype, Animal);

var d = new Dog('Mitzie');
d.speak();

Species

你可能希望在派生数组类 MyArray 中返回 Array对象。这种类/种类模式允许你覆盖默认的构造函数。

例如,当使用像map()返回默认构造函数的方法时,您希望这些方法返回一个父Array对象,而不是MyArray对象。Symbol.species 符号可以让你这样做:

class MyArray extends Array {
  // Overwrite species to the parent Array constructor
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); 
// false
console.log(mapped instanceof Array);   
// true

使用 super 调用超类

super 关键字用于调用对象的父对象上的函数。

class Cat { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

Mix-ins 混合

抽象子类或者 mix-ins 是类的模板。 一个 ECMAScript 类只能有一个单超类,所以想要从工具类来多重继承的行为是不可能的。子类继承的只能是父类提供的功能性。因此,例如,从工具类的多重继承是不可能的。该功能必须由超类提供。

一个以超类作为输入的函数和一个继承该超类的子类作为输出可以用于在ECMAScript中实现混合:

var calculatorMixin = Base => class extends Base {
  calc() { }
};

var randomizerMixin = Base => class extends Base {
  randomize() { }
};

使用 mix-ins 的类可以像下面这样写:

class Foo { }
class Bar extends calculatorMixin(randomizerMixin(Foo)) { }

规范

Specification Status Comment
ECMAScript 2015 (6th Edition, ECMA-262)
Class definitions
Standard Initial definition.
ECMAScript 2017 Draft (ECMA-262)
Class definitions
Draft  

浏览器兼容性

Feature Chrome Firefox (Gecko) Edge Internet Explorer Opera Safari
Basic support 42.0[1]
49.0
45 13 未实现 未实现 9.0
Feature Android Firefox Mobile (Gecko) IE Mobile Opera Mobile Safari Mobile Chrome for Android
Basic support 未实现 45 ? ? 9 42.0[1]
49.0

[1] 要求使用严格模式。非严格模式需要勾选启用实验性的 JavaScript(Enable Experimental JavaScript),其默认不勾选。

相关链接