这一次带你彻底搞懂JS继承

简介: 这一次带你彻底搞懂JS继承

前言

这段时间复习JS从看懂到看开(前端面试题整合)_DieHunter1024的博客-CSDN博客时发现对继承概念又陌生了,平时大多用的都是extends,对底层知识难免会生疏,于是决定分享这篇文章,重新学习一下继承。


起步

JavaScript和面向类的语言不同,它没有类做对象的抽象模式,它能够不通过类直接创建对象,相比其他的面向对象语言,JavaScript才能算是真正的面向 " 对象 " 语言。在面向类的语言中构造函数通常是属于类的,而JavaScript中(在ES6之前),类是属于构造函数的,为什么这么说?因为我们使用的类实际上是用构造函数实现的。下面进入主题让我们聊聊继承。


继承作为面向对象程序设计特征之一,必定有其重要的意义


继承是指:在已存在的类的基础上,拓展出新的类。那么存在的类就是父类,或基类,超类;新的类就是子类,或派生类


其重要意义就是使代码可以复用,子类中也拥有父类的属性和方法,从父类一级一级往下,属性和函数由泛化到细化


那么js中的继承又是怎样的呢?


"new" 究竟发生了什么?

要了解继承,得先了解new,我们在node环境下看看以下案例


JavaScript中的类在es6之前,没有class语法糖时,用的是构造函数实现的,与class不同的是,构造函数既是类,也是函数,既可以使用 "函数名()" 的方式执行,也可以采用 "new 函数名()" 的方式执行,这二者之间的效果却是截然不同,下面的例子中,使用 "函数名()" 的方式执行打印的是小暗,而另一个使用new的却打印了小明(这里我们是在node环境下执行的,function中的this指向的是全局的global,如果是在浏览器控制台执行,就需要把global换成window),由此可以得知 new 实际上是把构造函数原型(prototype)上的属性放在了原型链(__proto__)上,那么当实例化对象取值时就会在原型链上取,而实例化对象上的prototype已经不见了

global.name = "小暗";
function Person() {
  console.log(this.name);
}
Person.prototype = {
  name: "小明",
};
const fnReturn = Person(); // 小暗
const newReturn = new Person(); // 小明
console.log(fnReturn); // undefined
console.log(newReturn.name, newReturn.__proto__, newReturn.prototype); // 小明 { name: '小明' } undefined

我们可以简单理解为 new 实际上是将构造函数的prototype上的属性放在了实例化对象的__proto__上 ,通过实例化对象 . 属性名进行取值


那么new如何实现呢?


来看看下面的代码

exports.newClass = function () {
  const _target = new Object(); // 新增一个容器,用来装载构造函数(目标类)prototype上的所有属性
  const _this = this; //不能直接通过 this() 来运行构造函数,所以用一个变量装载
  _target.__proto__ = _this.prototype; // 核心部分:将构造函数prototype上的所有属性放到新容器中
  _this.apply(_target, arguments); // 执行构造函数,相当于执行class中的constructor
  return _target; //将新的容器返回,此时通过 _target[属性名]就可以访问 this.prototype 中的属性了
};

上述代码将 new 实现了一下,其中最重要的一步就是将构造函数prototype上的所有属性放到新容器中,最后获得的实例化对象的__proto__上就有了构造函数原型中所有属性了,下面我们放在之前的代码中看看效果

const { newClass } = require("./lib/new");
function Person() {
  console.log(this.name);
}
Person.prototype = {
  name: "小明",
};
const newReturn = new Person(); // 小明
const myNew = newClass.call(Person); // 小明
console.log(newReturn.name, newReturn.__proto__, newReturn.prototype); // 小明 { name: '小明' } undefined
console.log(myNew.name, myNew.__proto__, myNew.prototype); // 小明 { name: '小明' } undefined

说了这么多,其实目的是为了让大家知道:实例化一个构造函数,实际上可以简单理解为将类的prototype上的属性转移到实例化对象中,这样有助于理解后续的继承的实现,话不多说,直接开始


类式继承(原型链继承)

结合 new 的原理可以知道: 类式继承实际上是通过 new 将 SuperClass.prototype 绑定到 SuperClass.__proto__ 上,然后赋值给 SubClass.prototype,当实例化 SubClass 时,SubClass.__proto__ 上也会带有 SuperClass 及其原型链上的属性,即 SubClass 实例化对象上有以下属性:SuperClass.prototype 上的属性(实例化对象.__proto__.__proto__),SuperClass 构造函数上的属性(实例化对象.__proto__),SubClass 构造函数上的属性(实例化对象)

function classInheritance(SuperClass, SubClass) {
  SubClass.prototype = new SuperClass();
}
function SuperClass(props) {
  this.state = props;
  this.info = { color: "red" };
}
SuperClass.prototype = {
  name: "Car",
};
classInheritance(SuperClass, SubClass);
function SubClass() {
  this.price = 1000;
}
const BMW = new SubClass();
const BenZ = new SubClass();
console.log(BMW, BMW.__proto__, BMW.__proto__.__proto__); // { price: 1000 } { state: undefined, info: { color: 'red' } } { name: 'Car' }
console.log(BenZ.name, BenZ.info); // Car { color: 'red' }
BMW.info.color = "blue";
console.log(BenZ.name, BenZ.info); // Car { color: 'blue' }
console.log(BMW instanceof SubClass) // true
console.log(BMW instanceof SuperClass) // true

优点:简洁方便,子类拥有父类及父类 prototype 上属性


缺点:


子类通过prototype继承父类,只能父类单向传递属性给子类,无法向父类传递参数。为什么要向父类传递参数?如果父类中的某属性对参数有依赖关系,此时子类继承父类就需要在 new SuperClass() 时传参

当父类原型上的引用属性改变时,所有子类实例相对应的引用属性都会对应改变,即继承的引用类型属性都有引用关系

子类只能继承一个父类(因为继承方式是直接修改子类的prototype,如果再次修改,会将其覆盖)

继承语句前不能修改子类的 prototype 因为此类继承会覆盖子类原型

构造函数继承

在 SubClass 构造函数中使用 SuperClass.call 直接运行 SuperClass 构造函数,然而直接执行构造函数和使用 new 实例化构造函数二者是完全不同的:


前者(直接执行构造函数)在下方代码中会将 SuperClass 构造函数里初始化的属性带到 SubClass 中,而 SuperClass.prototype 中的 name 属性并未带到 SubClass 中;


而后者(使用 new 实例化构造函数)则会将 SuperClass.prototype 中的属性带到 SuperClass 实例化对象的 __proto__ 上

function SuperClass(props) {
  this.state = props;
  this.info = { color: "red" };
}
SuperClass.prototype = {
  name: "Car",
};
function SuperClass2() {
  this.size = 'small';
}
// 注意:构造函数使用 call 会重写子类同名属性,要写在子类的最开始
function SubClass() {
  SuperClass.call(this, ...arguments);
  SuperClass2.call(this, ...arguments);
  this.price = 1000;
}
SubClass.prototype.name = "Small Car";
const BMW = new SubClass(true);
const BenZ = new SubClass(false);
console.log(BMW, BMW.__proto__, BMW.__proto__.__proto__); 
// SubClass {
//   state: true,
//   info: { color: 'red' },
//   size: 'small',
//   price: 1000
// } SubClass { name: 'Small Car' } {}
console.log(BenZ.name, BenZ.info, BenZ.state); // Small Car { color: 'red' } false
BMW.info.color = "blue";
console.log(BenZ.name, BenZ.info); // Small Car { color: 'red' }
console.log(BMW instanceof SubClass) // true
console.log(BMW instanceof SuperClass) // false

所以其优点是:


可以在 SuperClass 执行时传参数


可以继承多个父类


继承同一个父类的子类的属性之间不会有引用关系(因为父类构造函数的执行是在每个子类中call(this)了,从而在父类构造函数执行时,this分别代表着每个子类)


缺点是:父类 prototype 上的属性无法继承,只能继承父类构造函数的属性,正是因为这点,父类的函数无法复用(指无法复用父类 prototype 中的函数,只能通过父类构造函数将函数放在子类中)


针对父类的函数无法复用的理解:


父类 SuperClass 每次在子类 SubClass 中执行都会在每个子类重新初始化 this.属性 或 this.函数,这些属性是属于每个子类单独的,这样既增加了性能负担又使父类原型中的公共属性无法复用;


而倘若这些函数或者属性在 SuperClass 的 prototype 上,并且子类能继承父类,则所有子类用公共属性的都是父类的,此时就达到了复用效果,而类式继承却能够实现这个效果,于是就有了下面的组合继承


组合继承

构造函数继承不能继承父类原型上的属性,而类式继承无法传参给父类,组合继承正好将两者规避了


然而组合继承在实例化父类和执行父类构造函数时执行了两次 SuperClass ,实际上类式继承是为了解决构造函数继承上的父类的 prototype 无法被子类继承的问题,看代码可以得知,new SuperClass() 确实会将父类的 prototype 继承到子类中,但是也会将 SuperClass 构造函数中的操作又执行一遍(具体可看 console.log(++count) 执行了3次),而且类式继承是将子类的原型直接替换掉,所以无法继承多个父类的问题也被延续下来了(但是可以在父类上多加一次继承,使多个类形成原型链关系,达到多继承的目的,即A,B,C三个类,A要继承B和C,那么让A继承B再继承C)

function classInheritance(superClass, subClass) {
  subClass.prototype = new superClass();
}
let count = 0;
function SuperClass(props) {
  this.state = props;
  this.info = { color: "red" };
  console.log(++count);// 打印 1 2 3 
}
SuperClass.prototype = {
  name: "Car",
};
classInheritance(SuperClass, SubClass);
function SubClass() {
  SuperClass.call(this, ...arguments);
  this.price = 1000;
}
SubClass.prototype.name = "Small Car";
const BMW = new SubClass(true);
const BenZ = new SubClass(false);
console.log(BMW, BMW.__proto__, BMW.__proto__.__proto__);
// { state: true, info: { color: 'red' }, price: 1000 } { state: undefined, info: { color: 'red' }, name: 'Small Car' } { name: 'Car' }
console.log(BenZ.name, BenZ.info, BenZ.state);// Small Car { color: 'red' } false
BMW.info.color = "blue";
console.log(BenZ.name, BenZ.info);// Small Car { color: 'red' }
console.log(BMW instanceof SubClass) // true
console.log(BMW instanceof SuperClass) // true


优点:解决类式继承和构造函数继承的主要问题


缺点:父类构造函数执行两遍,性能损耗


原型式继承

原型式继承是基于类式继承的封装,特点和类式继承一样,继承的引用类型属性都有引用关系


原型式继承的过渡对象F实际上就是类式继承中的子类构造函数,这么做相比类式继承的特点:减少性能开销(子类是空白的构造函数,没有任何内容),对应的,无法在子类构造函数中初始化属性


是不是觉得原型式继承和 Object.create( ) 很像? create 函数的原理就是生成一个新对象,这个新对象的 __proto__ 等于传入的对象。让我们回忆一下前面讲到的 new 的原理,new 实际上就是将 prototype 放在实例化对象的 __proto__ 上,不难理解,下面代码中 F.prototype = superClass 和 new F() 做的就是这一步

function prototypeInheritance(superClass) {
  function F() {}
  F.prototype = superClass;
  return new F();
}
function SuperClass(props) {
  this.state = props;
  this.info = { color: "red" };
}
SuperClass.prototype = {
  name: "Car",
};
const superClass = new SuperClass(true);
const BenZ = prototypeInheritance(superClass);
const BMW = prototypeInheritance(superClass);
BMW.price = 2000;
console.log(BMW, BMW.__proto__, BMW.__proto__.__proto__); // { price: 2000 } { state: true, info: { color: 'red' } } { name: 'Car' }
console.log(BenZ, BenZ.name, BenZ.info, BenZ.state); // {} Car { color: 'red' } true
BMW.info.color = "blue";
console.log(BenZ.name, BenZ.info); // Car { color: 'blue' }
console.log(BMW instanceof SuperClass); // true

类式继承是如何转换成原型式继承?看以下代码是不是清晰了一点,所以原型式继承也可以写成const subClass=Object.create(superClass)

function prototypeInheritance(SuperClass) {
  function SubClass() {}
  SubClass.prototype = new SuperClass();
  return new SubClass();
}

优点:无子类构造函数开销,相当于实现了对象的浅复制


缺点:


继承时无法向父类传参

和类式继承一样,继承父类的引用类型属性都有引用关系

寄生式继承

寄生式继承实际上是在上面的原型式继承的基础上做了二次封装,可以看成工厂模式+原型式继承,将继承步骤放在新的函数中,此时便可以在子类构造函数上添加子类独有的函数和属性,由此叫做寄生式继承,就好像子类独有的属性方法寄生在下面的 parasiticInheritance 函数中一样。使用这种继承在新建子类时,每个子类中的属性都不一样,违背了代码复用的效果

function prototypeInheritance(superClass) {
  function F() {}
  F.prototype = superClass;
  return new F();
}
function SuperClass(props) {
  this.state = props;
  this.info = { color: "red" };
}
SuperClass.prototype = {
  name: "Car",
};
function parasiticInheritance(superClass) {
  const subClass = prototypeInheritance(superClass);
  subClass.type = { electricity: true, gasoline: false };
  return subClass;
}
const superClass = new SuperClass(true);
const BenZ = parasiticInheritance(superClass);
const BMW = parasiticInheritance(superClass);
console.log(BenZ.type === BMW.type); // false  说明每个子类的属性都不一样
console.log(BenZ, BenZ.__proto__, BenZ.__proto__.__proto__);
// { type: { electricity: true, gasoline: false } } { state: true, info: { color: 'red' } } { name: 'Car' }
console.log(BMW, BMW.name, BMW.info, BMW.state); // { type: { electricity: true, gasoline: false } } Car { color: 'red' } true
BMW.info.color = "blue";
console.log(BMW.name, BMW.info); // Car { color: 'blue' }
console.log(BMW instanceof SuperClass); // true
console.log(BenZ instanceof SuperClass); // true


优点:


无子类构造函数开销


继承父类所有属性


子类拥有自己的属性


缺点:


继承时无法向父类传参


和类式继承一样,继承父类的引用类型属性都有引用关系


子类公共属性无法在原型上定义,导致无法复用


针对代码无法复用缺点的理解:让我们回忆一下上面的构造函数继承对代码复用的理解,子类构造函数中直接执行父类构造函数并改变 this 指向从而达到将父类属性初始化到子类中。而寄生式继承则是每次生成的子类都是新的构造函数 F ,所以在继承时单独给 subClass 增加属性实际上是操作不同的子类构造函数,而如果这个做法能在子类 prototype 中进行,那么子类的函数及属性可以复用。


寄生组合式继承

实际上上述继承方式都是实现最终继承方式的猜想和尝试,在ES6的class语法糖出现之前,寄生组合式继承是最理想的继承方式,下面让我们来看看


顾名思义寄生组合式继承就是寄生式继承和组合式继承的结合,个人认为叫它寄生组合式继承倒不如称其为原型组合式继承,因为他的写法就是原型式继承+组合式继承


作为ES6之前最理想的继承,我们当然是要深入分析一下,这么做到底好在哪?


我们按照标题寄生组合式继承实现一下这种继承的写法

// 之前写的原型式继承
function prototypeInheritance(superClass) {
  function F() {}
  F.prototype = superClass;
  return new F();
}
function parasiticCombinatorialInheritance(SuperClass, SubClass) {
  // 核心代码
  SubClass.prototype = prototypeInheritance(SuperClass.prototype);
  SubClass.prototype.superClass = SuperClass;
}
// 父类
function SuperClass(props) {}
// 子类
function SubClass() {
  this.superClass.call(this, ...arguments);
}
parasiticCombinatorialInheritance(SuperClass, SubClass);

乍一看,这种写法和组合式继承属实有点像,但是有一点不同:

prototypeInheritance 函数会生成一个只包含父类原型上属性而没有执行父类构造函数的 “纯净” 的新对象(即不执行父类构造函数)。

这句话怎么理解?

让我们结合一下 new 的原理,回忆一下类式继承或组合式继承是如何实现的:SubClass.prototype = new SuperClass() 这样会导致子类 prototype 中既执行了父类构造函数,也有父类原型上的属性。而实际上我们是暂时不需要执行父类构造函数的,因为在组合式继承中还有一步:在子类中执行 SuperClass.call(this, ...arguments) ,这一步会将父类构造函数再执行一次,将其二者结合,于是我们就得到了组合式继承的升级版:寄生组合式继承


我们将prototypeInheritance简写成Object.create,得到以下示例

function parasiticCombinatorialInheritance(SuperClass, SubClass) {
  SubClass.prototype = Object.create(SuperClass.prototype);
  SubClass.prototype.superClass = SuperClass;
}
function SuperClass(props) {
  this.state = props;
  this.info = { color: "red" };
}
SuperClass.prototype = {
  name: "Car",
};
function SubClass() {
  this.superClass.call(this, ...arguments); //调用一下父类构造函数,将父类的属性放在子类中
}
parasiticCombinatorialInheritance(SuperClass, SubClass);
SubClass.prototype.name = "small car";//修改prototype值写在继承后面
const BMW = new SubClass(true);
const BenZ = new SubClass(false);
console.log(BenZ, BenZ.__proto__, BenZ.__proto__.__proto__); // { state: false, info: { color: 'red' } } { superClass: [Function: SuperClass], name: 'small car' } { name: 'Car' }
console.log(BMW.info); // { color: 'red' }
BenZ.info.color = "blue";
console.log(BenZ.name,BenZ.info); // small car { color: 'blue' }
console.log(BenZ.name,BMW.info); // small car { color: 'red' }
console.log(BMW instanceof SuperClass); // true
console.log(BenZ instanceof SuperClass); // true


最后总结一下寄生组合式继承的优缺点


优点:解决了组合式继承的父类构造函数调用两次的问题,只创建了一次父类属性,并且子类拥有父类原型上的属性


缺点:多继承问题和子类prototype被修改(个人感觉后者可以适当调整赋值位置解决,而多继承问题可以考虑使用mixin进行优化)


看到这里,不知道你是否对JS继承有感触,觉得它和深复制有点像


不错,JS继承的类被继承时,其属性和行为也会被复制到子类中,JavaScript中没有类只有对象,而我们所说的类的继承,实际上是基于对象的深复制


想了解深复制和寄生组合式继承的同学可以跳到这篇文章代码


总结

以上就是JS继承的实现与使用,感谢你看到了最后,如果这篇文章有帮助到你,请支持一下作者,你的支持是我创作的动力


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