JavaScript专题-继承

简介: avaScript专题-继承此专题系列将对JavaScript重难点进行梳理,希望能融会贯通,加深印象,更进一步...本章需要你比较熟悉原型链相关的知识,如果你还不熟悉或者略有忘记,可以看看我的往期文章(JavaScript专题-原型链

JavaScript专题-继承

此专题系列将对JavaScript重难点进行梳理,希望能融会贯通,加深印象,更进一步...

本章需要你比较熟悉原型链相关的知识,如果你还不熟悉或者略有忘记,可以看看我的往期文章(JavaScript专题-原型链

各种方法整体认识


我们首先梳理一下各种继承实现的方法的进化史,这样更方便我们的记忆,从上往下都是上面有一定的缺点不能忍受,由此产生了对应下方的继承实现,最终寄生组合式继承结合上述优点成为最优的一种继承实现,包括后续官方ES6的继承extends也仅仅是这种实现的语法糖;

1.原型链方式


继承实现

基本思想就是通过原型链继承多个引用类型的属性和方法,关键语句就是将父类型作为子类型的原型:

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
};
function SubType() {
  this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true


这样SuperType实例中可以访问的所有属性和方法也会存在于SubType.prototype

判断继承关系

方式一:instanceof

console.log(subType instanceof SuperType) // true

方式二:isPrototypeOf()

console.log(SuperType.prototype.isPrototypeOf(subType))  // true


缺点

  • 主要问题出现在原型中包含引用值的时候,原型中包含的引用值会在所有实例间共享,即通过该方式实现的继承,如果父类包含引用值,该引用值就会在子类的所有实例中共享。
  • 子类在实例化时不能给父类型的构造函数传参

2.盗用构造函数


继承实现

基本思路就是在子类构造函数中调用父类构造函数:

function SuperType() {
  this.colors = ["red", "blue", "green"];
}
function SubType() {
  //继承SuperType
  SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red, blue, green, black"
let instance2 = new SubType();
console.log(instance2.colors); // "red, blue, green"


相当于新的SubType对象上运行了SuperType()函数中的所有初始化代码,结果就是子类的每个实例上都包含父类的属性和方法。在这里就是每个子实例都会拥有属于自己的colors属性,注意这与原型链实现的不同,原型链的方式是所有实例共享一个,而这里是为每个实例都新建了一个。

并且还有一个优点就是可以在子类构造函数中向父类构造函数传参

缺点

必须在构造函数中定义方法,因此函数不能重用,就是原型链的实现方式会导致我们不想要共享的属性(比如引用值)也跟着共享了,而盗用构造函数的实现方式会导致我们想要共享的通用方法也跟着都初始化了一次,就是实例化一次对象就初始化一次该方法。

3.组合式继承


继承实现

其实根据上述的缺点我们隐约就知道接下来的实现方式是什么样的了,就是组合式继承,上述两种继承实现方式明显就是相互补充的,所以这里结合他们从实现目的:我们可以根据需求做到有些属性或方法共享,而有些属性和方法不共享,具体哪些方法可以由我们自己决定。

这里的组合式继承就可以实现这样的效果:既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};
function SubType(name, age){
  // 继承属性
  SuperType.call(this, name);
  this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
  console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);   // "red, blue, green, black"
instance1.sayName();               // "Nicholas";
instance1.sayAge();                // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors);   // "red, blue, green"
instance2.sayName();               // "Greg";
instance2.sayAge();                // 27


缺点

该实现方式基本达到了我们想要的目的,但还有一个致命的缺点就是:

调用了两次父类的构造函数:

  1. SuperType.call(this, name);
  2. let instance1 = new SubType("Nicholas", 29);

我们用白话描述一下这个过程:首先父类需要将方法写在原型上而不是作为自身的属性。然后通过盗用构造函数将所有的属性继承下来,最后通过原型链继承的方式将父类作为子类的原型,注意这里是将整个父类作为了子类的原型,并不是直接复制父类的原型。所以原型里面包含了父类的属性,即和子类的属性重复了,只不过这里是在原型,会被同名属性遮蔽而已,但也浪费了存储空间,增加了初始化的消耗

关键就是我们通过原型链方式继承的时候使用的是整个父类的实例,导致子类的实例的原型上拥有了我们不需要共享的属性,这里其实就能想到一个基本的思路就是使用父类的原型赋值到子类的原型上,接下来详细讲一下这个继承实现方式。

4.原型式继承


继承实现

你可以简单地将这种方式理解为1.原型链继承的一个封装,下面这个函数就是这种方式的关键思想:

function object(o) {
  function F() {};
  F.prototype = o;
  return new F();
}


比如在1.原型链方式中,我们是这样实现继承的:

SubType.prototype = new SuperType();
let instance = new SubType();


而有了这个函数,我们就可以这样实现继承:

let instance = object(new SuperType())


这种方式将原型赋值隐藏在了函数内部,方便开发者更加灵活地操作,其中关键的操作就是object(o)中的o参数不仅仅可以传入一个父对象的实例,还可以传入任何一个对象,比如我们可以传入父对象的原型对象,这样我们就可以非常方便地复制父对象地原型对象,这也是我们后面解决组合式继承缺点的关键一步

扩展

ES5通过增加Object.create()方法将原型式继承的概念规范化了,这个函数可以传入两个参数,当只传入一个参数的时候,功能就和前面给的object(o)的代码效果相同了;


let instance = Object.create(new SuperType())

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合

缺点

因为关键也是使用1.原型链方式实现的,所以缺点也是一样的,主要就是属性中包含的引用值始终会在相关对象间共享。

5.寄生式继承


继承实现

基本思路就是:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

就是比如我们使用前面的let instance = object(new SuperType())复制了父对象中的属性和方法,然后我们在此基础上增加一些我们需要自定义的一些新的属性和方法在instance上,这个操作就叫做增强这个对象,然后整个这种方式就是寄生式继承;

function createAnother(original){
  let clone = object(original);   // 通过调用函数创建一个新对象
  clone.sayHi = function() {      // 以某种方式增强这个对象
  console.log("hi");
  };
  return clone;              // 返回这个对象
}
// 使用该函数
let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi();   // "hi"


缺点

注意我们在createAnother()函数中有一些自定义的方法,而这些方法在我们每次调用createAnother()函数创建一个新的实例的时候都会重新初始化一次,在这里就是上方代码中的第三行。

我们可以这样理解,通过寄生式继承这种方式的缺点其实和2.盗用构造函数的方式差不多,都是我们想要共享的方法也会跟着每次创建实例的时候都初始化一次。其实这里的createAnother()的效果和盗用构造函数是基本一致的

注意这里我们是在克隆对象本身上进行增强(添加方法),其实根据原型的知识,我们只需要在克隆的原型上添加我们想要共享的方法就可以了,当然具体实现稍微复杂一点,就是我们接下来要讲的终极解决方法6.寄生式组合继承

6.寄生式组合继承


在组合式继承中基本能达到我们预期中继承实现效果的目标,但是组合式继承存在一定的效率问题:就是父类构造函数始终会调用两次,一次是在创建子类原型时调用,另外一次就是在子类构造函数中调用,而现在的寄生式组合继承就是结合上面寄生式继承的思想来解决这个问题的。

基本思路就是:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。

function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype); // 创建父类原型副本
  prototype.constructor = subType; // 增强对象,解决由于重写原型导致默认constructor丢失的问题 
  subType.prototype = prototype; // 将父类原型的副本赋值给子类原型
}


接下来,我们再稍微修改一下组合式继承的方式(结合上面寄生式的思想)就可以解决组合式继承的缺点了:

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
  console.log(this.age);
};


这里就没有将整个父类赋值给子类的原型了,而仅仅是赋值了父类的原型给子类,最终这里只调用了一次SuperType构造函数,避免了SubType.prototype上不必要的属性。


目录
相关文章
|
11天前
|
JavaScript 前端开发
如何在 JavaScript 中使用 __proto__ 实现对象的继承?
使用`__proto__`实现对象继承时需要注意原型链的完整性和属性方法的正确继承,避免出现意外的行为和错误。同时,在现代JavaScript中,也可以使用`class`和`extends`关键字来实现更简洁和直观的继承语法,但理解基于`__proto__`的继承方式对于深入理解JavaScript的面向对象编程和原型链机制仍然具有重要意义。
|
20天前
|
JavaScript 前端开发
Javascript如何实现继承?
【10月更文挑战第24天】JavaScript 中实现继承的方式有很多种,每种方式都有其优缺点和适用场景。在实际开发中,我们需要根据具体的需求和情况选择合适的继承方式,以实现代码的复用和扩展。
|
14天前
|
JavaScript 前端开发
如何使用原型链继承实现 JavaScript 继承?
【10月更文挑战第22天】使用原型链继承可以实现JavaScript中的继承关系,但需要注意其共享性、查找效率以及参数传递等问题,根据具体的应用场景合理地选择和使用继承方式,以满足代码的复用性和可维护性要求。
|
14天前
|
JavaScript 前端开发 开发者
js实现继承怎么实现
【10月更文挑战第26天】每种方式都有其优缺点和适用场景,开发者可以根据具体的需求和项目情况选择合适的继承方式来实现代码的复用和扩展。
30 1
|
5月前
|
设计模式 JavaScript 前端开发
在JavaScript中,继承是一个重要的概念,它允许我们基于现有的类(或构造函数)创建新的类
【6月更文挑战第15天】JavaScript继承促进代码复用与扩展,创建类层次结构,但过深的继承链导致复杂性增加,紧密耦合增加维护成本,单继承限制灵活性,方法覆盖可能隐藏父类功能,且可能影响性能。设计时需谨慎权衡并考虑使用组合等替代方案。
46 7
|
5月前
|
JavaScript 前端开发
在 JavaScript 中,实现继承的方法有多种
【6月更文挑战第15天】JavaScript 继承常见方法包括:1) 原型链继承,利用原型查找,实例共享原型属性;2) 借用构造函数,避免共享,但方法不在原型上复用;3) 组合继承,结合两者优点,常用但有额外开销;4) ES6 的 class,语法糖,仍基于原型链,提供直观的面向对象编程。
37 7
|
2月前
|
自然语言处理 JavaScript 前端开发
一文梳理JavaScript中常见的七大继承方案
该文章系统地概述了JavaScript中七种常见的继承模式,包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承等,并探讨了每种模式的实现方式及其优缺点。
一文梳理JavaScript中常见的七大继承方案
|
2月前
|
JavaScript 前端开发
js之class继承|27
js之class继承|27
|
2月前
|
JSON JavaScript 前端开发
js原型继承|26
js原型继承|26
|
2月前
|
JavaScript 前端开发 开发者
JavaScript 类继承
JavaScript 类继承
19 1