学习链接
Class
在 JavaScript 中,类是一种函数。
class MyClass {prop = value; // 属性constructor(...) { // 构造器// ...}method(...) {} // methodget something(...) {} // getter 方法set something(...) {} // setter 方法[Symbol.iterator]() {} // 有计算名称(computed name)的方法(此处为 symbol)// ...}// MyClass 是一个函数alert(typeof MyClass); // function// 更确切地说,是 constructor 方法alert(MyClass === MyClass.prototype.constructor); // true
- 技术上来说,
MyClass是一个函数(即constructor方法,若不编写则假定为空) - 而 methods、getters 和 settors 都被写入了
MyClass.prototype - 类字段(即属性)会在实例中被设定,而不属于
MyClass.prototype
不仅仅是语法糖
class 通常被视为一种定义构造器及其原型方法的语法糖。
事实上,它们之间存在着重大差异:
首先,通过
class创建的函数具有特殊的内部属性标记[[IsClassConstructor]]: true。因此,它与手动创建并不完全相同。
编程语言会在许多地方检查该属性。例如,与普通函数不同,必须使用new来调用它。
Table 33: Internal Slots of ECMAScript Function Objects
| Internal Slot | Type | Description | | —- | —- | —- | | [[IsClassConstructor]] | a Boolean | Indicates whether the function is a class constructor.
(If true, invoking the function’s [[Call]] will immediately throw a TypeError exception.) |类方法不可枚举。 类定义将
"prototype"中的所有方法的enumerable标志设置为false。
这很好,因为如果我们对一个对象调用for..in方法,我们通常不希望 class 方法出现。- 类总是使用
use strict。 在类中的所有代码都将自动进入严格模式。 类不存在变量提升(hoist),这一点与 ES5 完全不同。
new Foo(); // ReferenceErrorclass Foo {}
因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与继承有关,必须保证子类在父类之后定义。默认有两条继承链,即构造函数的继承和原型对象的继承。
- 派生类的
new行为不同,即[[ConstructorKind]]:"derived"的构造函数- ES5 中的继承,实例在前,继承在后
先创建子类实例,后继承父类特性,添加自身特性 - ES6 中的继承,继承在前,实例在后
先创建父类实例,再添加子类特性
- ES5 中的继承,实例在前,继承在后
this 的指向
优雅解法
类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class Button {constructor(value) {this.value = value;}click() {console.log(this); // Windowconsole.log(this.value); // undefined}}const button = new Button("hello");setTimeout(button.click, 0);
解决方案
- 传递一个包装函数,例如
setTimeout(() => button.click(), 1000)。 - 将方法绑定到对象,例如在
constructor中this.click = this.click.bind(this) - 使用类字段搭配箭头函数,此时的
this指向实例对象。click = () => {console.log(this); // buttonconsole.log(this.value); // hello}
类继承 extends

- 子类的
[[Prototype]]属性指向父类,表示构造函数的继承。 - 子类
prototype属性的[[Prototype]]属性指向父类prototype属性,表示原型对象的继承(方法的继承)。
super 关键字
必须显式指定作为函数还是对象使用。
作为函数使用
- 代表父类构造函数,返回子类实例
super()内部的 this 指向子类实例super()只能用在子类的构造函数中
let x = null;class A {constructor() { x = this; }}class B extends A {constructor() { super(); }}const instance = new B();instance === x // true
继承类的 constructor 必须调用 **super(...)**,并且 (!) 一定要在使用 **this** 之前调用。
在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。
派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived"。这是一个特殊的内部标签。
该标签会影响它的 new 行为:
- 当通过
new执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给this。 - 但是当继承类的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。
因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错。
类字段的初始化顺序:
- 对于基类(还未继承任何东西的那种),在构造函数调用前初始化
- 对于派生类,在
**super()**后立刻初始化
也就是说,可以对派生类的 new 的行为步骤做如下的理解:
- 先创建(new)一个父类实例,而不是空对象
- 然后将这个实例的
[[Prototype]]指向子类的prototype属性 - 将这个实例赋值给派生类构造函数的
this - 执行派生类构造函数的代码
- 返回这个实例对象
这也就是下面的例子都输出 animal 的原因:
class Animal {name = 'animal';constructor() {alert(this.name); // (*)}}class Rabbit extends Animal {name = 'rabbit';}new Animal(); // animalnew Rabbit(); // animal, super() 会先 new Animal() (从表现出的行为来看)
作为对象使用
普通方法中
super指向父类的prototype即父类的原型对象- 定义在父类实例上的方法或属性,无法通过
**super**调用 super调用父类方法时,方法内部的this指向当前的子类实例 ```javascript class A { constructor() { this.x = 1; } print() { console.log(this.x); } }
class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); } }
let b = new B(); b.m() // 2
- 通过 `super` 对属性赋值,此时 `super` 就是 `this`,指向子类实例```javascriptclass A {constructor() { this.x = 1; }}class B extends A {constructor() {super();this.x = 2;super.x = 3; // 等价于 this.super = 3;console.log(super.x); // undefined (等价于获取 A.prototype.x)console.log(this.x); // 3}}let b = new B();
猜测
在 Object.prototype 上有一个最原始且特殊的 getter/setter,正常的对象定义属性的时候,都会沿着原型链去获取这个方法,然后设置和读取自身的属性,
这一操作可在定义时直接完成,例如 let obj = { a: 1},
也可在定义后额外添加属性,例如 obj.b = 2。
但是,如果在自身或者原型链上的某个对象中,定义了对应名称的 getter/setter,
则会拦截或者说劫持对象的设置和读取属性操作,
也就是说,阻止了对象去 Object.prototype 中获取对应的最原始且特殊的 getter/setter 。
仅作猜想,未经证实。
静态方法中
super 指向父类。
super 调用父类方法时,方法内部的 this 指向当前子类
class A {constructor() { this.x = 1; }static print() { console.log(this.x); }}class B extends A {constructor() {super();this.x = 2;}static m() {super.print(); // this 指向 子类 B}}B.m() // 2
内建类没有静态方法继承
内建对象有它们自己的静态方法,例如 Object.keys,Array.isArray 等。
如我们所知道的,原生的类互相扩展。例如,Array 扩展自 Object。
通常,当一个类扩展另一个类时,静态方法和非静态方法都会被继承。
但内建类却是一个例外。它们相互间不继承静态方法。
例如,Array 和 Date 都继承自 Object,所以它们的实例都有来自 Object.prototype 的方法。但 Array.[[Prototype]] 并不指向 Object,所以它们没有例如 Array.keys()(或 Date.keys())这些静态方法。
这里有一张 Date 和 Object 的结构关系图:

正如你所看到的,Date 和 Object 之间没有连结。它们是独立的,只有 Date.prototype 继承自 Object.prototype,仅此而已。
与我们所了解的通过 extends 获得的继承相比,这是内建对象之间继承的一个重要区别。
补充
[[HomeObject]]
问题演示
首先要说的是,从我们迄今为止学到的知识来看,super 是不可能运行的。
当一个对象方法执行时,它会将当前对象作为 this。随后如果我们调用 super.method(),那么引擎需要从当前对象的原型中获取 method。但这是怎么做到的?
这个任务看起来是挺容易的,但其实并不简单。引擎知道当前对象的 this,所以它可以获取父 method 作为 this.__proto__.method。不幸的是,这个“天真”的解决方法是行不通的。
- 确定或者说固定当前对象的
this - 获取父类的原型中的方法,使其内部的
this为第一步中调用者的this- 如此获取父类的原型中的方法
this.__proto__.method, - 如此使其内部的
this称为第一步中调用者的thisthis.__proto__.method.call(this)
- 如此获取父类的原型中的方法
- 事实上,这是存在漏洞的,
但如果仅有父类和子类这两层的调用关系,并未将问题暴露出来 - 正如 问题演示 中的例子所示,当加到三层的时候,问题就暴露出来了
- 问题就出在
this.__proto__.method.call(this)这一段代码中- 使用第一个
this的目的是通过它去获取父类的原型中的方法 - 使用第二个
this的目的是为了传入调用者的所在的对象 - 两层调用的时候,第一个
this和第二个this都是调用者本身 - 三层调用的时候
- 第一层的两个
this都是调用者本身 - 注意,此时第二层的两个
this也都成了第一层的调用者 - 第二层的第二个
this的取值是我们所期望的,传递了第一层的调用者 - 然而,第二层的第一个
this我们所期望的取值是第二层本身,而非第一层的调用者,因为我们原本需要通过它来获取到第三层信息,但现在被第一层传入的this给覆盖了,导致无法取到第三层的信息
- 第一层的两个
- 或许我们会想到将示例中第二层的
eat()方法删除,让第一层的调用者通过原型链去直接获取第三层的方法,也就是this.__proto__.__proto__.method.call(this),这样一来就可以既获取到方法,又传递了自身的this。- 在这里我们不用对象简化表示,而是用类来还原原本的例子
``javascript class Animal { name = 'Animal' eat() { alert(${this.name} eats.`); } }
- 在这里我们不用对象简化表示,而是用类来还原原本的例子
- 使用第一个
class Rabbit extends Animal { name = ‘Rabbit’ eat() { super.eat(); } }
class LongEar extends Rabbit { name = ‘Long Ear’ eat() { super.eat(); } }
(new LongEar).eat(); // Long Ear eats.
<br />也就是说,我们单用 `this` 并没有能够模拟出 `super` 获取父类方法的效果,重点难以做到在于传入调用者的 `this` 的同时,也不丢失自身的 `this` 去用以获取父类的方法。<a name="de842a6c-1"></a>### 解决方案为了提供解决方法,JavaScript 为函数添加了一个特殊的内部属性:`[[HomeObject]]`。**当一个函数被定义为类或者对象方法时,它的 **`**[[HomeObject]]**`** 属性就成为了该对象。**然后 `super` 使用它来解析(resolve)父原型及其方法。它基于 `[[HomeObject]]` 运行机制按照预期执行。一个方法,例如 `longEar.eat`,知道其 `[[HomeObject]]` 即为 `longEar`,并且**从其原型中获取父方法。并没有使用 **`**this**`。也就是说,利用 `[[HomeObject]]` 来取代了第一个 `this` 的作用,使其能够正确的获取父类原型中的方法,而不会覆盖第二个 `this`。> **红宝书**:ES6 给类构造函数和静态方法添加了内部特性 `[[HomeObject]]`,这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。`**super**`** 始终会定义为 **`**[[HomeObject]]**`** 的原型****注意**:- 在 JavaScript 语言中 `[[HomeObject]]` 仅被用于 `super`。- `[[HomeObject]]` 是为类和普通对象中的方法定义的。但是对于对象而言,方法必须确切指定为 `method()`,而不是 `"method: function()"`。- `[[HomeObject]]` 不能被更改,这个绑定是永久的,该方法对于对象的绑定是永久的,不同于 `this`。```javascriptlet animal = {sayHi() {alert(`I'm an animal`);}};// rabbit 继承自 animallet rabbit = {__proto__: animal,sayHi() {super.sayHi();}};let plant = {sayHi() {alert("I'm a plant");}};// tree 继承自 plantlet tree = {__proto__: plant,sayHi: rabbit.sayHi // (*) [[HomeObject]] 依旧指向 rabbit 而非 tree};tree.sayHi(); // I'm an animal
