实现继承的代码显得非常冗长和混乱。为解决这些问题,ECMAScript 6新引入的class关键字具有正式定义类的能力。
    类(class)是ECMAScript中新的基础性语法糖结构
    虽然ECMAScript 6类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
    8.4.1 类定义
    与函数类型相似,定义类也有两种主要方式:类声明和类表达式。
    这两种方式都使用class关键字加大括号:

    1. class Person {} // 类声明
    2. const Animal = class {} // 类表达式

    与函数表达式类似,类表达式在它们被求值前也不能引用。
    与函数定义有两点不同:
    ① 虽然函数声明可以提升,但类定义不能:

    1. // 函数
    2. console.log(FExpression); // undefined
    3. var FExpression = function() {};
    4. console.log(FExpression); // ƒ () {}
    5. console.log(FDeclaration); // ƒ FDeclaration() {}
    6. function FDeclaration() {};
    7. console.log(FDeclaration); // ƒ FDeclaration() {}
    8. // 类
    9. console.log(CExpression); // undefined
    10. var CExpression = class {};
    11. console.log(CExpression); // class {}
    12. console.log(CDeclaration); // Uncaught ReferenceError: Cannot access 'CDeclaration' before initialization
    13. class CDeclaration {};
    14. console.log(CDeclaration); // class CDeclaration {}

    ② 函数受函数作用域限制,而类受块作用域限制:

    1. {
    2. function FDeclaration() {};
    3. class CDeclaration {}
    4. }
    5. console.log(FDeclaration); // ƒ FDeclaration() {}
    6. console.log(CDeclaration); // Uncaught ReferenceError: CDeclaration is not defined

    类的构成
    类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。
    空的类定义照样有效。
    默认情况下,类定义中的代码都在严格模式下执行。
    与函数构造函数一样,多数编程风格都建议类名的首字母要大写,以区别于通过它创建的实例(比如,通过class Foo {}创建实例foo):

    1. class Foo {} // 空类定义,有效
    2. class Bar { // 有构造函数的类,有效
    3. constructor() {}
    4. }
    5. class Baz { // 有获取函数的类,有效
    6. get myBaz() {}
    7. }
    8. class Qux { // 有静态方法的类,有效
    9. static myQux() {}
    10. }

    类表达式的名称是可选的。
    在把类表达式赋值给变量后,可以通过name属性取得类表达式的名称字符串。
    但不能在类表达式作用域外部访问这个标识符。

    1. let Person = class PersonName {
    2. identity() {
    3. console.log(Person.name, PersonName.name)
    4. }
    5. }
    6. let p = new Person();
    7. p.identity(); // PersonName PersonName
    8. console.log(Person.name); // PersonName
    9. console.log(PersonName); // Uncaught ReferenceError: PersonName is not defined

    8.4.2 类构造函数
    constructor关键字用于在类定义块内部创建类的构造函数。
    方法名constructor会告诉解释器,在使用new操作符创建类的新实例时,应该调用这个函数。
    构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。
    1.实例化
    使用new操作符实例化Person的操作,等于使用new调用其构造函数。唯一可感知的不同之处就是,JavaScript解释器知道使用new和类意味着应该使用constructor函数进行实例化。
    使用new调用类的构造函数会执行如下操作。
    (1)在内存中创建一个新对象。
    (2)这个新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性。
    (3)构造函数内部的this被赋值为这个新对象(即this指向新对象)。
    (4)执行构造函数内部的代码(给新对象添加属性)。
    (5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
    来看下面的例子:

    1. class Animal {}
    2. class Person {
    3. constructor() {
    4. console.log('person ctor');
    5. }
    6. }
    7. class Vegetable {
    8. constructor() {
    9. this.color = 'orange';
    10. }
    11. }
    12. let a = new Animal();
    13. let p = new Person(); // person ctor
    14. let v = new Vegetable();
    15. console.log(v.color); // orange

    类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:

    1. class Person {
    2. constructor(name) {
    3. console.log(arguments.length);
    4. this.name = name || null;
    5. }
    6. }
    7. let p1 = new Person(); // 0
    8. console.log(p1.name); // null
    9. let p2 = new Person(); // 0
    10. console.log(p2.name); // null
    11. let p3 = new Person('Amy'); // 1
    12. console.log(p3.name); // Amy

    默认,类构造函数会在执行之后返回this对象。
    构造函数返回的对象会被用作实例化的对象,如果没有什么引用新创建的this对象,那么这个对象会被销毁。
    如果返回的不是this对象,而是其他对象,那么这个对象不会通过instanceof操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改。

    1. class Person {
    2. constructor(override) {
    3. this.foo = 'foo';
    4. if (override) {
    5. return {
    6. bar: 'bar'
    7. }
    8. }
    9. }
    10. }
    11. let p1 = new Person(),
    12. p2 = new Person();
    13. console.log(p1); // Person {foo: "foo"}
    14. console.log(p1 instanceof Person); // true
    15. console.log(p2); // Person {foo: "foo"}
    16. console.log(p2 instanceof Person); // true

    类构造函数与构造函数的主要区别:
    调用类构造函数必须使用new操作符(调用类构造函数时如果忘了使用new则会抛出错误)
    普通构造函数如果不使用new调用,那么就会以全局的this(通常是window)作为内部对象

    1. function Person() {}
    2. class Animal {}
    3. let p = Person(); // 把window作为this来构建实例
    4. let a = Animal(); // 报错: Uncaught TypeError: Class constructor Animal cannot be invoked without 'new'

    类构造函数没有什么特殊之处,实例化之后,它会成为普通的实例方法(但作为类构造函数,仍然要使用new调用)。
    因此,实例化之后可以在实例上引用它:

    1. class Person {}
    2. // 使用类创建一个新实例
    3. let p1 = new Person();
    4. p1.constructor(); // 报错: Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
    5. // 使用对类构造函数的引用创建一个新实例
    6. let p2 = new p1.constructor();

    2.把类当成特殊函数
    ECMAScript中没有正式的类这个类型。从各方面来看,ECMAScript类就是一种特殊函数。声明一个类之后,通过typeof操作符检测类标识符,表明它是一个函数
    类标识符有prototype属性,而这个原型也有一个constructor属性指向类自身:

    1. class Person {}
    2. console.log(Person.prototype); // { constructor: class Person }
    3. console.log(Person === Person.prototype.constructor); // true

    与普通构造函数一样,可以使用instanceof操作符检查构造函数原型是否存在于实例的原型链中
    类本身具有与普通构造函数一样的行为。
    在类的上下文中,类本身在使用new调用时就会被当成构造函数。
    重点在于,类中定义的constructor方法不会被当成构造函数,在对它使用instanceof操作符时会返回false。
    但是,如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么instanceof操作符的返回值会反转
    image.png
    可以像其他对象或函数引用一样把类作为参数传递
    image.png
    与立即调用函数表达式相似,类也可以立即实例化:
    image.png
    8.4.3 实例、原型和类成员
    类的语法可以非常方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员。1.实例成员
    每次通过new调用类标识符时,都会执行类构造函数。
    在这个函数内部,可以为新创建的实例(this)添加“自有”属性(没有限制)。
    在构造函数执行完毕后,仍然可以给实例继续添加新成员。
    每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享

    1. class Person {
    2. constructor() {
    3. this.name = new String('Jack');
    4. this.sayName = () => console.log(this.name);
    5. this.nicknames = ['Jake', 'Jakie'];
    6. }
    7. }
    8. let p1 = new Person(),
    9. p2 = new Person()
    10. p1.sayName(); // String {"Jack"}
    11. p2.sayName(); // String {"Jack"}
    12. console.log(p1.name === p2.name); // false
    13. console.log(p1.sayName === p2.sayName); // false
    14. console.log(p1.nicknames === p2.nicknames); // false
    15. p1.name = p1.nicknames[0];
    16. p2.name = p2.nicknames[1];
    17. p1.sayName(); // Jake
    18. p2.sayName(); // Jakie

    2.原型方法与访问器
    为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法
    可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据
    类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键
    类定义也支持获取和设置访问器。语法与行为跟普通对象一样
    3.静态类方法
    可以在类上定义静态方法。这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。
    与原型成员类似,静态成员每个类上只能有一个。
    静态类成员在类定义中使用static关键字作为前缀。
    在静态成员中,this引用类自身。其他所有约定跟原型成员一样
    4.非函数原型和类成员
    虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加
    5.迭代器与生成器方法
    类定义语法支持在原型和类本身上定义生成器方法
    因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象
    8.4.4 继承
    ECMAScript 6新增特性中最出色的一个就是原生支持了类继承机制。
    虽然类继承使用的是新语法,但背后依旧使用的是原型链。
    1.继承基础
    ES6类支持单继承。使用extends关键字,就可以继承任何拥有[[Construct]]和原型的对象。
    2.构造函数、HomeObject和super()
    派生类的方法可以通过super关键字引用它们的原型。
    这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
    在类构造函数中使用super可以调用父类构造函数
    image.png
    在静态方法中可以通过super调用继承的类上定义的静态方法
    image.png
    注:ES6给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在JavaScript引擎内部访问。super始终会定义为[[HomeObject]]的原型。
    使用super时注意几个问题:
    ❑ super只能在派生类构造函数和静态方法中使用
    ❑ 不能单独引用super关键字,要么用它调用构造函数,要么用它引用静态方法
    ❑ 调用super()会调用父类构造函数,并将返回的实例赋值给this
    ❑ super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
    ❑ 如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数
    ❑ 在类构造函数中,不能在调用super()之前引用this
    ❑ 如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对
    3.抽象基类
    有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。
    虽然ECMAScript没有专门支持这种类的语法,但通过new.target也很容易实现。
    new.target保存通过new关键字调用的类或函数。
    通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化
    image.png
    4.继承内置类型
    ES6类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型
    5.类混入
    把不同类的行为集中到一个类是一种常见的JavaScript模式。
    虽然ES6没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。
    注:Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用Object.assign()就可以了。
    注:很多JavaScript框架(特别是React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。