Class 基本语法

“class” 语法

  1. class User {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. sayHi() {
  6. alert(this.name);
  7. }
  8. }
  9. // 用法:
  10. let user = new User("John");
  11. user.sayHi();

当 new User(“John”) 被调用:

  1. 一个新对象被创建。
  2. constructor 使用给定的参数运行,并为其分配 this.name。

类的方法之间没有逗号

什么是 class?

在 JavaScript 中,类是一种函数。

  1. class User {
  2. constructor(name) { this.name = name; }
  3. sayHi() { alert(this.name); }
  4. }
  5. // 佐证:User 是一个函数
  6. alert(typeof User); // function

Class 字段

例如,让我们在 class User 中添加一个 name 属性:

  1. class User {
  2. name = "John";
  3. sayHi() {
  4. alert(`Hello, ${this.name}!`);
  5. }
  6. }
  7. new User().sayHi(); // Hello, John!

类字段重要的不同之处在于,它们会在每个独立对象中被设好,而不是设在 User.prototype:

  1. class User {
  2. name = "John";
  3. }
  4. let user = new User();
  5. alert(user.name); // John
  6. alert(User.prototype.name); // undefined

使用类字段制作绑定方法

正如 函数绑定 一章中所讲的,JavaScript 中的函数具有动态的 this。它取决于调用上下文。
因此,如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则 this 将不再是对其对象的引用。
例如,此代码将显示 undefined:

  1. class Button {
  2. constructor(value) {
  3. this.value = value;
  4. }
  5. click() {
  6. alert(this.value);
  7. }
  8. }
  9. let button = new Button("hello");
  10. setTimeout(button.click, 1000); // undefined

这个问题被称为“丢失 this”
我们在 函数绑定 一章中讲过,有两种可以修复它的方式:

  1. 传递一个包装函数,例如 setTimeout(() => button.click(), 1000)。
  2. 将方法绑定到对象,例如在 constructor 中。

类字段提供了另一种非常优雅的语法:

  1. class Button {
  2. constructor(value) {
  3. this.value = value;
  4. }
  5. click = () => { //优雅
  6. alert(this.value);
  7. }
  8. }
  9. let button = new Button("hello");
  10. setTimeout(button.click, 1000); // hello

类字段 click = () => {…} 是基于每一个对象被创建的,在这里对于每一个 Button 对象都有一个独立的方法,在内部都有一个指向此对象的 this。我们可以把 button.click 传递到任何地方,而且 this 的值总是正确的。

类继承

类继承是一个类扩展另一个类的一种方式。

“extends” 关键字

在内部,关键字 extends 使用了很好的旧的原型机制进行工作。它将 Rabbit.prototype.[[Prototype]] 设置为 Animal.prototype。
所以,如果在 Rabbit.prototype 中找不到一个方法,JavaScript 就会从 Animal.prototype 中获取该方法。

让我们创建一个继承自 Animal 的 class Rabbit:

  1. class Rabbit extends Animal {
  2. hide() {
  3. alert(`${this.name} hides!`);
  4. }
  5. }
  6. let rabbit = new Rabbit("White Rabbit");
  7. rabbit.run(5); // White Rabbit runs with speed 5.
  8. rabbit.hide(); // White Rabbit hides!

在 extends 后允许任意表达式

  1. function f(phrase) {
  2. return class {
  3. sayHi() { alert(phrase); }
  4. };
  5. }
  6. class User extends f("Hello") {}
  7. new User().sayHi(); // Hello

这里 class User 继承自 f(“Hello”) 的结果。

重写方法

但是通常来说,我们不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能。我们在我们的方法中做一些事儿,但是在它之前或之后或在过程中会调用父类方法。
Class 为此提供了 “super” 关键字。

  • 执行 super.method(…) 来调用一个父类方法。
  • 执行 super(…) 来调用一个父类 constructor(只能在我们的 constructor 中)。

例如,让我们的 rabbit 在停下来的时候自动 hide:

  1. class Animal {
  2. constructor(name) {
  3. this.speed = 0;
  4. this.name = name;
  5. }
  6. run(speed) {
  7. this.speed = speed;
  8. alert(`${this.name} runs with speed ${this.speed}.`);
  9. }
  10. stop() {
  11. this.speed = 0;
  12. alert(`${this.name} stands still.`);
  13. }
  14. }
  15. class Rabbit extends Animal {
  16. hide() {
  17. alert(`${this.name} hides!`);
  18. }
  19. stop() {
  20. super.stop(); // 调用父类的 stop
  21. this.hide(); // 然后 hide
  22. }
  23. }
  24. let rabbit = new Rabbit("White Rabbit");
  25. rabbit.run(5); // White Rabbit 以速度 5 奔跑
  26. rabbit.stop(); // White Rabbit 停止了。White rabbit hide 了!

箭头函数没有 super
如果被访问,它会从外部函数获取。例如:

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // 1 秒后调用父类的 stop
  }
}

重写 constructor

根据 规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor:

class Rabbit extends Animal {
  // 为没有自己的 constructor 的扩展类生成的
  constructor(...args) {
    super(...args);
  }
}

继承类的 constructor 必须调用 super(…),并且 (!) 一定要在使用 this 之前调用。
在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [[ConstructorKind]]:”derived”。这是一个特殊的内部标签。
为了让 Rabbit 的 constructor 可以工作,它需要在使用 this 之前调用 super(),就像下面这样

class Animal {

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

  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    super(name);   // 需要在使用 this 之前调用 super()
    this.earLength = earLength;
  }

  // ...
}

// 现在可以了
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10

重写类字段: 一个棘手的注意要点

我们不仅可以重写方法,还可以重写类字段。

class Animal {
  name = 'animal';

  constructor() {
    alert(this.name); // (*)
  }
}

class Rabbit extends Animal {
  name = 'rabbit';
}

new Animal(); // animal
new Rabbit(); // animal

因为 Rabbit 中没有自己的构造器,所以 Animal 的构造器被调用了。有趣的是在这两种情况下:new Animal() 和 new Rabbit(),在 () 行的 alert 都打印了 animal。
*换句话说, 父类构造器总是会使用它自己字段的值,而不是被重写的那一个。

再看看 重写方法的例子:

class Animal {
  showName() {  // 而不是 this.name = 'animal'
    alert('animal');
  }

  constructor() {
    this.showName(); // 而不是 alert(this.name);
  }
}

class Rabbit extends Animal {
  showName() {
    alert('rabbit');
  }
}

new Animal(); // animal
new Rabbit(); // rabbit

当父类构造器在派生的类中被调用时,它会使用被重写的方法。
……但对于类字段并非如此。
这里为什么会有这样的区别呢?
实际上,原因在于字段初始化的顺序。类字段是这样初始化的

  • 对于基类(还未继承任何东西的那种),在构造函数调用前初始化。
  • 对于派生类,在 super() 后立刻初始化。

    深入:内部探究和 [[HomeObject]]

    这是关于继承和 super 背后的内部机制。
    先跳过。。。。

    静态属性和静态方法

    静态方法被用于实现属于整个类的功能。它与具体的类实例无关。

    私有的和受保护的属性和方法

    内部接口和外部接口

    就面向对象编程(OOP)而言,内部接口与外部接口的划分被称为 封装)。
    在面向对象的编程中,属性和方法分为两组:

  • 内部接口 —— 可以通过该类的其他方法访问,但不能从外部访问的方法和属性。

  • 外部接口 —— 也可以从类的外部访问的方法和属性。

在 JavaScript 中,有两种类型的对象字段(属性和方法):

  • 公共的:可从任何地方访问。它们构成了外部接口。到目前为止,我们只使用了公共的属性和方法。
  • 私有的:只能从类的内部访问。这些用于内部接口。

    受保护的 “waterAmount”

    受保护的属性通常以下划线 _ 作为前缀。这不是在语言级别强制实施的,但是程序员之间有一个众所周知的约定,即不应该从外部访问此类型的属性和方法。
    所以我们的属性将被命名为 _waterAmount

    只读的 “power”

    只需要设置 getter,而不设置 setter
    受保护的字段是可以被继承的

    私有的 “#waterLimit”

    This is a recent addition to the language. Not supported in JavaScript engines, or supported partially yet, requires polyfilling.
    私有属性和方法应该以 # 开头。它们只在类的内部可被访问。
    私有字段不能通过 this[name] 访问

    扩展内建类

    内建的类,例如 Array,Map 等也都是可以扩展的(extendable)。

    类检查:”instanceof”

    instanceof 操作符用于检查一个对象是否属于某个特定的 class。同时,它还考虑了继承。

    instanceof 操作符

    如果 obj 隶属于 Class 类(或 Class 类的衍生类),则返回 true。
    obj instanceof Class
    
    ```javascript class Rabbit {} let rabbit = new Rabbit();

// rabbit 是 Rabbit class 的对象吗? alert( rabbit instanceof Rabbit ); // true

通常,instanceof 在检查中会将原型链考虑在内。此外,我们还可以在静态方法 Symbol.hasInstance 中设置自定义逻辑。
<a name="RQ1Ec"></a>
## 福利:使用 Object.prototype.toString 方法来揭示类型
内建的 toString 方法可以被从对象中提取出来,并在任何其他值的上下文中执行。其结果取决于该值。

- 对于 number 类型,结果是 [object Number]
- 对于 boolean 类型,结果是 [object Boolean]
- 对于 null:[object Null]
- 对于 undefined:[object Undefined]
- 对于数组:[object Array]
- ……等(可自定义)

{}.toString 是一种“更高级的” typeof
```javascript
let s = Object.prototype.toString;

alert( s.call(123) ); // [object Number]
alert( s.call(null) ); // [object Null]
alert( s.call(alert) ); // [object Function]

Symbol.toStringTag

Mixin 模式

在 JavaScript 中,我们只能继承单个对象。每个对象只能有一个 [[Prototype]]。并且每个类只可以扩展另外一个类
换句话说,mixin 提供了实现特定行为的方法,但是我们不单独使用它,而是使用它来将这些行为添加到其他类中。

一个 Mixin 实例

在 JavaScript 中构造一个 mixin 最简单的方式就是构造一个拥有实用方法的对象,以便我们可以轻松地将这些实用的方法合并到任何类的原型中。

// mixin
let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  }
};

// 用法:
class User {
  constructor(name) {
    this.name = name;
  }
}

// 拷贝方法
Object.assign(User.prototype, sayHiMixin);  // 这里没有继承,只有一个简单的方法拷贝。

// 现在 User 可以打招呼了
new User("Dude").sayHi(); // Hello Dude!

所以 User 可以从另一个类继承,还可以包括 mixin 来 “mix-in“ 其它方法,就像这样:

class User extends Person {
  // ...
}

Object.assign(User.prototype, sayHiMixin);