这一次带你彻底搞懂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继承的实现与使用,感谢你看到了最后,如果这篇文章有帮助到你,请支持一下作者,你的支持是我创作的动力


相关文章
|
2月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承通过原型链实现共享,节省内存,但不支持私有属性。
27 0
|
1月前
|
设计模式 JavaScript 前端开发
在JavaScript中,继承是一个重要的概念,它允许我们基于现有的类(或构造函数)创建新的类
【6月更文挑战第15天】JavaScript继承促进代码复用与扩展,创建类层次结构,但过深的继承链导致复杂性增加,紧密耦合增加维护成本,单继承限制灵活性,方法覆盖可能隐藏父类功能,且可能影响性能。设计时需谨慎权衡并考虑使用组合等替代方案。
35 7
|
1月前
|
JavaScript 前端开发
在 JavaScript 中,实现继承的方法有多种
【6月更文挑战第15天】JavaScript 继承常见方法包括:1) 原型链继承,利用原型查找,实例共享原型属性;2) 借用构造函数,避免共享,但方法不在原型上复用;3) 组合继承,结合两者优点,常用但有额外开销;4) ES6 的 class,语法糖,仍基于原型链,提供直观的面向对象编程。
25 7
|
26天前
|
设计模式 JavaScript 前端开发
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
JavaScript的继承机制基于原型链,它定义了对象属性和方法的查找规则。每个对象都有一个原型,通过原型链,对象能访问到构造函数原型上的方法。例如`Animal.prototype`上的`speak`方法可被`Animal`实例访问。原型链的尽头是`Object.prototype`,其`[[Prototype]]`为`null`。继承方式包括原型链继承(通过`Object.create`)、构造函数继承(使用`call`或`apply`)和组合继承(结合两者)。ES6的`class`语法是语法糖,但底层仍基于原型。继承选择应根据需求,理解原型链原理对JavaScript面向对象编程至关重要
33 7
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
|
27天前
|
JavaScript 前端开发
JavaScript进阶-原型链与继承
【6月更文挑战第18天】JavaScript的原型链和继承是其面向对象编程的核心。每个对象都有一个指向原型的对象链,当查找属性时会沿着此链搜索。原型链可能导致污染、效率下降及构造函数与原型混淆的问题,应谨慎扩展原生原型、保持原型结构简洁并使用`Object.create`或ES6的`class`。继承方式包括原型链、构造函数、组合继承和ES6的Class继承,需避免循环引用、方法覆盖和不当的构造函数使用。通过代码示例展示了这两种继承形式,理解并有效利用这些机制能提升代码质量。
|
1月前
|
JavaScript 前端开发
JavaScript 继承的方式和优缺点
JavaScript 继承的方式和优缺点
|
21天前
|
JavaScript 前端开发
JavaScript 中,可以使用类继承来创建子类
JavaScript 中,可以使用类继承来创建子类
12 0
|
24天前
|
JavaScript 前端开发
JS的6种继承方式
JS的6种继承方式
15 0
|
1月前
|
JavaScript 前端开发
深入解析JavaScript中的面向对象编程,包括对象的基本概念、创建对象的方法、继承机制以及面向对象编程的优势
【6月更文挑战第12天】本文探讨JavaScript中的面向对象编程,解释了对象的基本概念,如属性和方法,以及基于原型的结构。介绍了创建对象的四种方法:字面量、构造函数、Object.create()和ES6的class关键字。还阐述了继承机制,包括原型链和ES6的class继承,并强调了面向对象编程的代码复用和模块化优势。
27 0
|
2月前
|
设计模式 JavaScript 前端开发
在JavaScript中,继承是一个重要的概念
【5月更文挑战第9天】JavaScript继承有优点和缺点。优点包括代码复用、扩展性和层次结构清晰。缺点涉及深继承导致的复杂性、紧耦合、单一继承限制、隐藏父类方法以及可能的性能问题。在使用时需谨慎,并考虑其他设计模式。
25 2