(JavaScript)中的类继承(extends、重写方法、重写constructor、重写类字段、[[HomeObject]])

"extends"关键字

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} stands still.`);
  }
}

let animal = new Animal("My animal");


class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!
  • extendsRabbit.prototype.[[Prototype]] 设置为 Animal.prototype

在extends后允许任意表达式

function f(phrase) {
  return class {
    sayHi() { alert(phrase); }
  };
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

重写方法

class Rabbit extends Animal {
  stop() {
    // ……现在这个将会被用作 rabbit.stop()
    // 而不是来自于 class Animal 的 stop()
  }
}

  • 在父类的基础上进行调整或扩展其功能,可以使用super关键字
  • 执行 super.method(...) 来调用一个父类方法
  • 执行 super(...) 来调用一个父类 constructor(只能在我们的 constructor 中)。
class Animal {

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

  run(speed) {
    this.speed = speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stands still.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }

  stop() {
    super.stop(); // 调用父类的 stop
    this.hide(); // 然后 hide
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit 以速度 5 奔跑
rabbit.stop(); // White Rabbit 停止了。White rabbit hide 了!

箭头函数没有super

//箭头函数没有super,如果被访问,它会从外部函数获取
class Rabbit extends Animal {
  stop() {
    //箭头函数的super和stop的是一样的
    setTimeout(() => super.stop(), 1000); // 1 秒后调用父类(Animal)的 stop
  }
}
class Rabbit extends Animal {
  stop() {
    //如果这里是普通的函数,将会抛出错误
    setTimeout(function() { super.stop() }, 1000);
  }
}

重写constructor

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

class Rabbit extends Animal {
  // 为没有自己的 constructor 的扩展类生成的
  constructor(...args) {
    //调用了父类的 constructor,并传递了所有的参数
    super(...args);
  }
}
  • 继承类的 constructor 必须调用 super(...),并且一定要在使用 this 之前调用。

  • 在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived"

  • 这是一个特殊的内部标签。该标签会影响它的 new 行为:

  1. 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this
  2. 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。
  3. 因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错
class Animal {

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

  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    super(name);
    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';
}

/*
* 为什么这两个会输出相同的结果呢?
* 因为Rabbit没有自己的构造器,去调用了Animal的构造器
* 而父类构造器(Animal)总是会使用它自己字段的值,而不是重写的那一个
**/
new Animal(); // animal
new Rabbit(); // 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() 后立刻初始化。

在上面的AnimalRabbit的例子当中,Animal是基类,所以在构造函数调用之前就已经初始化了。Rabbit是派生类,new Rabbit() 先去调用了 super(),执行了父类构造器。父类构造器中有this.name,但是此时Rabbit还没有自己的类字段,就使用了Animal的类字段。Rabbitsuper()调用之后,才会初始化自己的类字段。


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

super的运行机制

当一个对象方法执行时,它会将当前对象作为this
如果调用super.method(),引擎需要从当前对象的原型中获取method(this.__proto__.method
但是,这个方法是行不通的。

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {
    // 这就是 super.eat() 可以大概工作的方式
    /*
    *.call(this) 在这里非常重要
    *因为简单的调用 this.__proto__.eat() 将在原型的上下文中执行 eat,而非当前对象。
    **/
    this.__proto__.eat.call(this); 
  }
};

rabbit.eat(); // Rabbit eats.

但是,如果我们在原型链上再添加一个对象,将会发生变化

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  eat() {
    //2.因为刚才传递过来的this = longEar
    //所以this.__proto__.eat.call(this)变成了longEar.__proto__.eat.call(this)
    //又是rabbit.eat,所以rabbit.eat在不停地循环调用自己,因此它无法进一步的提升
    this.__proto__.eat.call(this); // (*)
  }
};

let longEar = {
  __proto__: rabbit,
  eat() {
    //1.在调用原型(rabbit)中的call方法,提供了调用对象this = longEar
    this.__proto__.eat.call(this); // (**)
  }
};

longEar.eat(); // Error: Maximum call stack size exceeded

[[HomeObject]]

  • 当一个函数被定义为类或者对象方法时,它的 [[HomeObject]] 属性就成为了该对象。
  • 然后 super 使用它来解析(resolve)父原型及其方法。
let animal = {
  name: "Animal",
  eat() {         // animal.eat.[[HomeObject]] == animal
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {         // rabbit.eat.[[HomeObject]] == rabbit
    super.eat();
  }
};

let longEar = {
  __proto__: rabbit,
  name: "Long Ear",
  eat() {         // longEar.eat.[[HomeObject]] == longEar
    super.eat();
  }
};

// 正确执行
longEar.eat();  // Long Ear eats.
  • 它基于 [[HomeObject]] 运行机制按照预期执行。一个方法,例如 longEar.eat,知道其 [[HomeObject]] 并且从其原型中获取父方法。并没有使用 this。

方法并不是自由的
  • 函数通常都是自由的,它并没有绑定Javascript中的对象。正因如此,他们可以在对象之间复制,并用另一个this调用它
  • [[HomeObject]] 的存在违反了这个原则,因为方法记住了它们的对象。[[HomeObject]] 不能被更改,所以这个绑定是永久的。
  • 在 JavaScript 语言中 [[HomeObject]] 仅被用于 super。所以,如果一个方法不使用 super,那么我们仍然可以视它为自由的并且可在对象之间复制。但是用了 super 再这样做可能就会出错。
let animal = {
  sayHi() {   //3.animal和这个sayHi绑定在一起(它的[[HomeObject]]是animal),只要调用这个sayHi,就一定是animal调用
    alert(`I'm an animal`);
  }
};

// rabbit 继承自 animal
let rabbit = {
  __proto__: animal,
  sayHi() {   //2.rabbit和这个sayHi绑定在一起(它的[[HomeObject]]是rabbit),只要调用这个sayHi,就一定是rabbit调用
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    alert("I'm a plant");
  }
};

// tree 继承自 plant
let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi // 1.tree.sayHi 方法是从 rabbit 复制而来
};

tree.sayHi();  // I'm an animal (?!?)
方法,不是函数属性
  • [[HomeObject]] 是为类和普通对象中的方法定义的。但是对于对象而言,方法必须确切指定为 method(),而不是 “method: function()”。
let animal = {
  eat: function() { // 这里是故意这样写的,而不是 eat() {...
    // ...
  }
};

let rabbit = {
  __proto__: animal,
  eat: function() {   //[[HomeObject]]属性是对于方法来说的,而不是属性
    super.eat();
  }
};

rabbit.eat();  // 错误调用 super(因为这里没有 [[HomeObject]])
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值