基本的类声明

类声明以 class 关键字开始,其后是类的名称;剩余部分的语法看起来就像对象字面量中的
方法简写,并且在方法之间不需要使用逗号。

class PersonClass {
    // 等价于 PersonType 构造器
    constructor(name) {
        this.name = name;
    }
    // 等价于 PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
}
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

为何要使用类的语法?

类与自定义类型之间有相似性较高,但也要记住一些重要的区别:

  • 类声明不会被提升,这与函数定义不同。类声明的行为与 let 相似,因此在程序的执行
    到达声明处之前,类会存在于暂时性死区内。
  • 类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。
  • 类的所有方法都是不可枚举的,这是对于自定义类型的显著变化,后者必须用
    Object.defineProperty() 才能将方法改变为不可枚举。
  • 类的所有方法内部都没有 [[Construct]] ,因此使用 new 来调用它们会抛出错误。
  • 调用类构造器时不使用 new ,会抛出错误。
  • 试图在类的方法内部重写类名,会抛出错误。

类表达式

类与函数有相似之处,即它们都有两种形式:声明与表达式。函数声明与类声明都以适当的
关键词为起始(分别是 function 与 class ),随后是标识符(即函数名或类名)。函数具
有一种表达式形式,无须在 function 后面使用标识符;类似的,类也有不需要标识符的表
达式形式。类表达式被设计用于变量声明,或可作为参数传递给函数。

基本的类表达式

let PersonClass = class {
    // 等价于 PersonType 构造器
    constructor(name) {
        this.name = name;
    }
    // 等价于 PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
};
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

类表达式不需要在 class 关键字后使用标识符。除了语法差异,类表达式的功能等价于类声明。

具名类表达式

在class 关键字后添加标识符,即具名类表达式

作为一级公民的类

在编程中,能被当作值来使用的就称为一级公民( first-class citizen ),意味着它能作为参
数传给函数、能作为函数返回值、能用来给变量赋值。

ES6 延续了传统,让类同样成为一级公民。这就使得类可以被多种方式所使用。例如,它能
作为参数传入函数:

function createObject(classDef) {
    return new classDef();
}
let obj = createObject(class {
    sayHi() {
        console.log("Hi!");
    }
});
obj.sayHi(); // "H

访问器属性

自有属性需要在类构造器中创建,而类还允许你在原型上定义访问器属性。为了创建一个
getter ,要使用 get 关键字,并要与后方标识符之间留出空格;创建 setter 用相同方式,只
是要换用 set 关键字。例如:

class CustomHTMLElement {
    constructor(element) {
        this.element = element;
    }
    get html() {
        return this.element.innerHTML;
    }
    set html(value) {
        this.element.innerHTML = value;
    }
}
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false

需计算的成员名

对象字面量与类之间的相似点还不仅前面那些。类方法与类访问器属性也都能使用需计算的
名称。语法相同于对象字面量中的需计算名称:无须使用标识符,而是用方括号来包裹一个
表达式。例如:

let methodName = "sayName";
class PersonClass {
    constructor(name) {
        this.name = name;
    }
    [methodName]() {
        console.log(this.name);
    }
}
let me = new PersonClass("Nicholas");
me.sayName();

生成器方法

同函数生成器一样

静态成员

只要在方法与访问器属性的名称前添加正式的 static 标注。即构成类的静态方法

class PersonClass {
    constructor(name) {
        this.name = name;
    }
    // 等价于 PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
    // 等价于 PersonType.create
    static create(name) {
        return new PersonClass(name);
    }
}
let person = PersonClass.create("Nicholas");

静态成员不能用实例来访问,你始终需要直接用类自身来访问它们。

继承

使用派生类进行继承

使用extends 关键字来指定当前类所需要继承的函数即可实现类的继承。而如果想要访问继承基类的构造器则需要调用super()

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    getArea() {
        return this.length * this.width;
    }
}
class Square extends Rectangle {
    constructor(length) {
        // 与 Rectangle.call(this, length, length) 相同
        super(length, length);
    }
}
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true

继承了其他类的类被称为派生类( derived classes )。如果派生类指定了构造器,就需要
使用 super() ,否则会造成错误。若你选择不使用构造器, super() 方法会被自动调用,
并会使用创建新实例时提供的所有参数。

使用 super() 时需牢记以下几点:

  1. 你只能在派生类中使用 super() 。若尝试在非派生的类(即:没有使用 extends
    关键字的类)或函数中使用它,就会抛出错误。
  2. 在构造器中,你必须在访问 this 之前调用 super() 。由于 super() 负责初始化
    this ,因此试图先访问 this 自然就会造成错误。
  3. 唯一能避免调用 super() 的办法,是从类构造器中返回一个对象。

从表达式中派生类

在 ES6 中派生类的最强大能力,或许就是能够从表达式中派生类。只要一个表达式能够返回
一个具有 [[Construct]] 属性以及原型的函数,你就可以对其使用 extends 实现继承。

继承内置对象

在 ES6 基于类的继承中, this 的值会先被基类( Array )创建,随后才被派生类的构造
器( MyArray )所修改。结果是 this 初始就拥有作为基类的内置对象的所有功能,并能正
确接收与之关联的所有功能。

class MyArray extends Array {
// 空代码块
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined

MyArray 直接继承了 Array ,因此工作方式与正规数组一致。与数值索引属性的互动更新
了 length 属性,而操纵 length 属性也能更新索引属性。这意味着你既能适当地继承
Array 来创建你自己的派生数组类,也同样能继承其他的内置对象。伴随着这些附加功能,
ES6 与派生类型有效解决了从内置类型进行派生这最后的特殊情况,不过这种情况仍然值得
继续探索。

Symbol.species 属性

继承内置对象一个有趣的方面是:任意能返回内置对象实例的方法,在派生类上却会自动返
回派生类的实例。因此,若你拥有一个继承了 Array 的派生类 MyArray ,诸如 slice() 之
类的方法都会返回 MyArray 的实例。例如:

class MyArray extends Array {
// 空代码块
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof MyArray); // true

在此代码中, slice() 方法返回了 MyArray 的一个实例。 slice() 方法是从 Array 上继
承的,原本应当返回 Array 的一个实例。而 Symbol.species 属性在后台造成了这种变化。

Symbol.species 知名符号被用于定义一个能返回函数的静态访问器属性。每当类实例的方法
(构造器除外)必须创建一个实例时,前面返回的函数就被用为新实例的构造器。下列内置
类型都定义了 Symbol.species :

  • Array
  • ArrayBuffer
  • Map
  • Promise
  • RegExp
  • Set
  • 类型化数组

以上每个类型都拥有默认的 Symbol.species 属性,其返回值为 this ,意味着该属性总是
会返回自身的构造器函数。若你准备在一个自定义类上实现此功能,代码就像这样:

// 几个内置类型使用 species 的方式类似于此
class MyClass {
    static get [Symbol.species]() {
        return this;
    }
    constructor(value) {
        this.value = value;
    }
    clone() {
        return new this.constructor[Symbol.species](this.value);
    }
}

在类构造器中使用 new.target

与判断函数是被如何被调用的一样。

class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width;
    }
}
class Square extends Rectangle {
    constructor(length) {
        super(length, length)
    }
}
// new.target 就是 Square
var obj = new Square(3); // 输出 fals

总结

ES6 的类让 JS 中的继承变得更简单,因此对于你已从其他语言学习到的类知识,你无须将其
丢弃。 ES6 的类起初是作为 ES5 传统继承模型的语法糖,但添加了许多特性来减少错误。

ES6 的类配合原型继承来工作,在类的原型上定义了非静态的方法,而静态的方法最终则被
绑定在类构造器自身上。类的所有方法初始都是不可枚举的,这更契合了内置对象的行为,
后者的方法默认情况下通常都不可枚举。此外,类构造器被调用时不能缺少 new ,确保了不
能意外地将类作为函数来调用。

基于类的继承允许你从另一个类、函数或表达式上派生新的类。这种能力意味着你可以调用
一个函数来判断需要继承的正确基类,也允许你使用混入或其他不同的组合模式来创建一个
新类。新的继承方式让继承内置对象(例如数组)也变为可能,并且其工作符合预期。

你可以在类构造器内部使用 new.target ,以便根据类如何被调用来做出不同的行为。最常
用的就是创建一个抽象基类,直接实例化它会抛出错误,但它仍然允许被其他类所继承。

总之,类是 JS 的一项新特性,它提供了更简洁的语法与更好的功能,通过安全一致的方式来
自定义一个对象类型。