Skip to main content
 首页 » 编程设计

Javascript面向对象之类继承

2022年07月19日144langtianya

Javascript面向对象(九)——类继承

类可以继承另一个类。有漂亮的语法,技术上基于原型继承。

为了从另一个类继承,我们应该指定“extends”关键字,并且把父类写在括号{...}之前。

下面示例代码中Rabbit类继承自Animal类:

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} stopped.`); 
  } 
 
} 
 
// Inherit from 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! 

extends关键字准确地增加一个原型引用从Rabbit.prototypeAnimal.prototype,正如你期望的一样,下面图示我们之前也见过:

所以现在rabbit对象能访问自己的方法,也可以访问来自父类Animal的方法。

任何表达式都可以在extends之后

继承语法允许继承不仅是类,在extens之后可以为任意表达式。
示例,一个函数调用生成父类:

function f(phrase) { 
  return class { 
    sayHi() { alert(phrase) } 
  } 
} 
 
class User extends f("Hello") {} 
 
new User().sayHi(); // Hello 

这里类User 继承自f(“hello”)函数的调用结果。
这可能在一些高级编程模型中非常有用,我们使用函数依赖很多条件生成类,然后可以从它们继承。

方法覆盖

现在让我们继续学习方法覆盖。截至目前,Rabbit继承了Animal类的stop方法,并设置this.speed = 0
如果我们在Rabbit中指定我们自己的方法stop,那么则会覆盖父类方法并使用。

class Rabbit extends Animal { 
  stop() { 
    // ...this will be used for rabbit.stop() 
  } 
} 

但通常我们并不想完全替代父类的方法,而是构建在其之上,稍微调整或扩展其功能。我们实现在实现方法中增加一些功能,但这过程中或前后调用父类的方法。Javascript提供了关键自“super”可以实现。

  • super.method(…) 调用父类方法
  • super(…) 调用父类构造函数(只能在构造函数内部调用)

举例,让rabbit对象调用stop方式,自动隐藏。

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} stopped.`); 
  } 
 
} 
 
class Rabbit extends Animal { 
  hide() { 
    alert(`${this.name} hides!`); 
  } 
 
  stop() { 
    super.stop(); // call parent stop 
    hide(); // and then hide 
  } 
} 
 
let rabbit = new Rabbit("White Rabbit"); 
 
rabbit.run(5); // White Rabbit runs with speed 5. 
rabbit.stop(); // White Rabbit stopped. White rabbit hides! 

现在,Rabbit有stop方法,其首先调用了父类的方法,super.stop() .

箭头函数没有super

箭头函数不能有super,如果使用,实际是调用外部函数,示例:

class Rabbit extends Animal { 
  stop() { 
    setTimeout(() => super.stop(), 1000); // call parent stop after 1sec 
  } 
} 

这里super在箭头函数中,和在stop()方法是一样的,所以如我们期望的一样。
如果我们在一般的函数中使用,会抛出错误:

// Unexpected super 
setTimeout(function() { super.stop() }, 1000); 

覆盖构造函数

构造函数覆盖有点棘手。
到目前为止,Rabbit类没有自己的构造函数。根据规范,如果一个类继承自另一个类,没有提供构造函数,那么自动生成一个构造函数:

class Rabbit extends Animal { 
  // generated for extending classes without own constructors 
  constructor(...args) { 
    super(...args); 
  } 
} 

我们看到,基本上是调用父类构造函数,并传入所有参数。因为如果我们不写构造函数,就会这样。
现在,我们增加自己的构造函数,在name的基础上指定earLength属性。

class Animal { 
  constructor(name) { 
    this.speed = 0; 
    this.name = name; 
  } 
  // ... 
} 
 
class Rabbit extends Animal { 
 
  constructor(name, earLength) { 
    this.speed = 0; 
    this.name = name; 
    this.earLength = earLength; 
  } 
 
  // ... 
} 
 
// Doesn't work! 
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined. 

啊!居然出错了,我们不能创建rabbit对象,怎么出错了呢?

简短的答案是:继承类的构造函数必须调用super(…),而其之前不能使用this关键字。

但是为什么?这时怎么回事?事实上,这个需求有点奇怪。

当然,有个合理的解释,让我们进入细节,这样你能真正理解发生了什么?

在Javascript中,继承类的构造函数与一般构造函数有区别。
在继承类中,相应的构造函数用一个特定的内部属性[[ConstructorKind]]:"derived"标记(派生的)。区别是:

  • 当正常的构造函数运行时,它创建一个空对象赋值给this,然后继续。
  • 但派生类构造函数运行时,它没有这么做,它期望父类构造函数做这个工作。

所以我们创建自己的构造函数,必须调用super,否则this引用的对象没有被创建,导致错误。
让Rabbit正常,在使用this之前需要调用super(),代码如下:

class Animal { 
 
  constructor(name) { 
    this.speed = 0; 
    this.name = name; 
  } 
 
  // ... 
} 
 
class Rabbit extends Animal { 
 
  constructor(name, earLength) { 
    super(name); 
    this.earLength = earLength; 
  } 
 
  // ... 
} 
 
// now fine 
let rabbit = new Rabbit("White Rabbit", 10); 
alert(rabbit.name); // White Rabbit 
alert(rabbit.earLength); // 10 

Super:内部的,[[HomeObject]]
让我们深入super的底层,顺便可以看到一些有趣的事情。

首先要说的,从我们已经学习的内容,super不可能实现。

是的,确实是的,让我们问自己,技术上super是如何工作的?当对象方法执行是,系统获得当前对象作为this。
如果我们调用super.method()时,如何返回方法?换句话说,我们需要从当前对象的父原型中获得方法,那么Javascript引擎在技术上是如何实现的?

可能我们获得this的[[prototype]],然后通过this.__proto__.method?不幸的是,这样不行。

让我们试着去做,不使用类,使用普通对象纯粹为了简化。
这里,rabbit.eat()应该调用animal.eat(),父对象的方法:

let animal = { 
  name: "Animal", 
  eat() { 
    alert(this.name + " eats."); 
  } 
}; 
 
let rabbit = { 
  __proto__: animal, 
  name: "Rabbit", 
  eat() { 
    this.__proto__.eat.call(this); // (*) 
  } 
}; 
 
rabbit.eat(); // Rabbit eats. 

在型号行,我们从原型(animal)中获得eat方法,并在当前对象的上下文中调用它。请注意.call(this)是很重要的,因为简单的this.__proto__.eat()执行原型上下文中的父对象的eat方法,不是当前对象。这里一切如愿。

现在让我们增加更多的链,我们将发现意外:

let animal = { 
  name: "Animal", 
  eat() { 
    alert(this.name + " eats."); 
  } 
}; 
 
let rabbit = { 
  __proto__: animal, 
  eat() { 
    // ...bounce around rabbit-style and call parent (animal) method 
    this.__proto__.eat.call(this); // (*) 
  } 
}; 
 
let longEar = { 
  __proto__: rabbit, 
  eat() { 
    // ...do something with long ears and call parent (rabbit) method 
    this.__proto__.eat.call(this); // (**) 
  } 
}; 
 
longEar.eat(); // Error: Maximum call stack size exceeded 

这代码不再工作,当我们调用longEar.eat()时出现错误。

可能不直观,但如果我们跟踪longEar.eat()调用,可以看到原因。有星号的两行,this的值是当前对象(longEar)。
这是基本的:所有对象方法获得当前对象作为this,不是原型或其他。

所以,两行带星号中的this.proto显然是相同的:rabbit,他们都调用rabbit.eat,没有向上寻找原型链。

换句话说:

1、在longEar.eat()里,我们经过向上调用rabbit.eat,给它相同的this=longEar 。

// inside longEar.eat() we have this = longEar 
this.__proto__.eat.call(this) // (**) 
// becomes 
longEar.__proto__.eat.call(this) 
// or 
rabbit.eat.call(this); 

2、在 rabbit.eat中,我们想通过调用原型链中更高层的方法,但是this=longEar,所以this.proto.eat是rabbit.eat !

// inside rabbit.eat() we also have this = longEar 
this.__proto__.eat.call(this) // (*) 
// becomes 
longEar.__proto__.eat.call(this) 
// or (again) 
rabbit.eat.call(this); 

3、所以rabbit.eat调用自身,进入死循环,因为它不能进一步向上执行。

这个问题是无法解决,因为this必须总是调用对象自身,无论那个父对象调用。所以它的原型总是直接父对象,我们不能向上获得原型链。

[[HomeObject]]

为了提供解决方案,Javascript给函数增加一个特殊内部属性:[[HomeObject]].

当一个函数被指定作为类或对象方法时,它的[[HomeObject]]属性为那个对象。

这实际上违背了函数“不绑定”的思想,因为方法记住对应的对象,[[HomeObject]]不能被改变,所以这绑定是永久的,所以这是Javascript语言很重要的改变。

但这个改变是安全的,[[HomeObject]]仅用于通过super调用父类方法中,为了解决通过原型无法实现功能,所以没有打破兼容性。

让我们再看super如何工作的,仍使用普通对象:

let animal = { 
  name: "Animal", 
  eat() { // [[HomeObject]] == animal 
    alert(this.name + " eats."); 
  } 
}; 
 
let rabbit = { 
  __proto__: animal, 
  name: "Rabbit", 
  eat() { // [[HomeObject]] == rabbit 
    super.eat(); 
  } 
}; 
 
let longEar = { 
  __proto__: rabbit, 
  name: "Long Ear", 
  eat() { // [[HomeObject]] == longEar 
    super.eat(); 
  } 
}; 
 
longEar.eat();  // Long Ear eats. 

每个方法记住对应对象在内部[[HomeObject]]属性中,然后super用它获得父原型。

[[HomeObject]]被定义在类或对象的方法中,对于对象,方法必须通过如method()方式指定,不能是method: function()方式。

在下面示例中,使用非方法语法用于比较,[[HomeObject]]属性没有被设置,继承不会起作用:

let animal = { 
  eat: function() { // should be the short syntax: eat() {...} 
    // ... 
  } 
}; 
 
let rabbit = { 
  __proto__: animal, 
  eat: function() { 
    super.eat(); 
  } 
}; 
 
rabbit.eat();  // Error calling super (because there's no [[HomeObject]]) 

静态方法和继承

类的语法也支持静态成员继承。举例:

class Animal { 
 
  constructor(name, speed) { 
    this.speed = speed; 
    this.name = name; 
  } 
 
  run(speed = 0) { 
    this.speed += speed; 
    alert(`${this.name} runs with speed ${this.speed}.`); 
  } 
 
  static compare(animalA, animalB) { 
    return animalA.speed - animalB.speed; 
  } 
 
} 
 
// Inherit from Animal 
class Rabbit extends Animal { 
  hide() { 
    alert(`${this.name} hides!`); 
  } 
} 
 
let rabbits = [ 
  new Rabbit("White Rabbit", 10), 
  new Rabbit("Black Rabbit", 5) 
]; 
 
rabbits.sort(Rabbit.compare); 
 
rabbits[0].run(); // Black Rabbit runs with speed 5. 

现在我们能调用Rabbit.compare,假设其继承自Animal.
如何工作的呢?使用原型,你可能已经猜到了。扩展也给Rabbit的属性[[Prototype]]引用Animal。

所以,Rabbit函数现在继承自Animal函数,Animal函数正常有[[Prototype]]属性,引用Function.prototype,因为它没有扩展任何其他对象。
我们检查看看:

class Animal {} 
class Rabbit extends Animal {} 
 
// for static propertites and methods 
alert(Rabbit.__proto__ == Animal); // true 
 
// and the next step is Function.prototype 
alert(Animal.__proto__ == Function.prototype); // true 
 
// that's in addition to the "normal" prototype chain for object methods 
alert(Rabbit.prototype.__proto__ === Animal.prototype); 

这样Rabbit能访问所有Animal类静态的方法。

请注意内置类没有这样静态[[Prototype]]引用。如Object有Object.defineProperty,Object.keys等,但是Array,Date等没有继承它们。
这里是Date和Object的图示结构:

注意,Date和Object之间没有链接,两者之间独立存在。Date.prototype继承自Object.prototype,但这也就是全部了。

由于历史原因存在这样差异:在Javascript开始阶段,没有考虑类语法和继承静态方法。

扩展内置类

内置的一些类,如Array,Map等也可以扩展。举例,PowerArray继承自内置类Array。

// add one more method to it (can do more) 
class PowerArray extends Array { 
  isEmpty() { 
    return this.length == 0; 
  } 
} 
 
let arr = new PowerArray(1, 2, 5, 10, 50); 
alert(arr.isEmpty()); // false 
 
let filteredArr = arr.filter(item => item >= 10); 
alert(filteredArr); // 10, 50 
alert(filteredArr.isEmpty()); // false 

注意一个有趣的事情,内置方法如fliter,map等返回正是继承类型(子类)的对象,依赖构造器属性实现。
上面示例中:
arr.constructor === PowerArray

所以当arr.filter()被调用时,内部正是通过new PowerArray创建新数组,我们可以使用原型链中更深层的方法。
而且,我们可以自定义行为,静态getter Symbol.species,如果存在,返回使用的构造器。

举例,这里由于Symbol.species,内置方法如map,fliter将返回正常数组:

class PowerArray extends Array { 
  isEmpty() { 
    return this.length == 0; 
  } 
 
  // built-in methods will use this as the constructor 
  static get [Symbol.species]() { 
    return Array; 
  } 
} 
 
let arr = new PowerArray(1, 2, 5, 10, 50); 
alert(arr.isEmpty()); // false 
 
// filter creates new array using arr.constructor[Symbol.species] as constructor 
let filteredArr = arr.filter(item => item >= 10); 
 
// filteredArr is not PowerArray, but Array 
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function 

我们能使用在更高级的关键点,如果我们不需要,可以去除一些扩展功能,或许进一步扩展。


本文参考链接:https://blog.csdn.net/neweastsun/article/details/71374659