目录

前言

起步

"new" 究竟发生了什么?

类式继承(原型链继承)

构造函数继承

组合继承

原型式继承

寄生式继承

寄生组合式继承

总结


前言

这段时间复习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上的所有属性放到新容器中const result = _this.apply(_target, arguments); // 执行构造函数,相当于执行class中的constructorreturn result && typeof result === "object" ? result : _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继承的实现与使用,感谢你看到了最后,如果这篇文章有帮助到你,请支持一下作者,你的支持是我创作的动力

有需要源码的小伙伴可以看这里(有点乱):myCode: 一些小案例 - Gitee.com

这一次带你彻底搞懂JS继承相关推荐

  1. 怎么用Python写出随时间变化的字_面试必备 | 带你彻底搞懂 Python 生成器

    文章转载地址:面试必备 | 带你彻底搞懂 Python 生成器. 写在之前 Python 的高级语言特性一直是我们学习 Python 的一个难点,大部分人并没有做到熟练的掌握,甚至去学习它都感觉很困难 ...

  2. proe常用c语言语句,带你轻松搞懂Proe条件语句

    原标题:带你轻松搞懂Proe条件语句 本文通过几个简单的例子介绍Proe中的条件语句,希望对你能有所帮助.Proe中使用的IF条件语句和C语言中的IF语句原理是一样的,其结构稍有差别.首先我们了解一下 ...

  3. 10分钟带你彻底搞懂微内核架构

    文章目录 十分钟搞懂系列 什么是微内核架构? 如何实现微内核架构? 总结 十分钟搞懂系列 序号 标题 链接 1 10分钟带你彻底搞懂企业服务总线 https://blog.csdn.net/belon ...

  4. 10分钟带你彻底搞懂服务限流和服务降级

    文章目录 十分钟搞懂系列 服务限流 计数器法 滑动窗口法 漏桶算法 令牌桶算法 服务降级 十分钟搞懂系列 序号 标题 链接 1 10分钟带你彻底搞懂企业服务总线 https://blog.csdn.n ...

  5. 一文带你彻底搞懂i++和++i的区别,谁的效率更高?

    作者简介:Codebowl靓仔,学妹的工具人,C++开发误入数据开发,梦想30岁退休的靓仔就是我啦. i++和++i对于初学者来说,一直是一个特别容易搞混的内容,相信很多人现在也没有完全搞清(作者初学 ...

  6. lambda表达式java项目常用_一文带你彻底搞懂Lambda表达式

    1. 为什么使用Lambda表达式 Lambda是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递).可以写出更简洁.更灵活的代码.作为一种更紧凑的代码风 ...

  7. 10分钟带你彻底搞懂负载均衡

    文章目录 十分钟搞懂系列 负载均衡是如何保证软件系统的生产部署的? 负载均衡分发策略 请求由谁来分发? 服务器端负载均衡器 客户端负载均衡 请求分发到哪去? 静态负载均衡算法 动态负载均衡算法 十分钟 ...

  8. 8分钟带你深入浅出搞懂Nginx

    8分钟带你深入浅出搞懂Nginx Nginx是一款轻量级的Web服务器.反向代理服务器,由于它的内存占用少,启动极快,高并发能力强,在互联网项目中广泛应用. 架构图 上图基本上说明了当下流行的技术架构 ...

  9. 彻底搞懂 JS 中 this 机制

    彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...

最新文章

  1. 前嗅ForeSpider教程:采集图片/视频/资源文件的链接地址
  2. 剑指offer 算法 (抽象建模能力)
  3. Repo lesson
  4. 三十岁以前不必在乎的29件事
  5. React Native官方DEMO
  6. java中抽象类的定义_Java中抽象类的定义和使用
  7. axure通用元件库 Pc、Web端原型图组件库高保真UI rp源文件
  8. Java实现指数运算
  9. java 获得文本框文本_Java获取和删除Word文本框中的表格
  10. 2018年深圳,武汉房价走势分析
  11. linux更换steam目录,如何在Linux上备份Steam游戏数据 | MOS86
  12. android ui web,AndroidUI4Web:最适合Android开发者的WebApp框架
  13. word制作招聘启示文档教学
  14. 如何解决百度网盘下载速度慢的问题
  15. 手把手教你如何在Facebook上找客户——实战分享
  16. 【java日常知识3.2】多线程
  17. 简单自定义安全键盘(只能输入字母,数字,部分符号)
  18. Java并发_8.1 并发容器
  19. numactl mysql_mysqld_multi多实例及numactl资源绑定
  20. Unity人工智能编程精粹学习笔记 寻找最短路径并避开障碍物——A*寻路

热门文章

  1. HCL动态抓包设置流程教学
  2. Cisco 2960X交换机支持多少条静态路由
  3. 【JVM】JVM基础知识:垃圾回收、JVM调优
  4. Adobe Reader与Foxi Reader
  5. 计算机毕业设计(附源码)python医院门诊分诊系统
  6. Python-Django毕业设计租车信息管理系统(程序+Lw)
  7. Verilog——将单脉冲宽度信号变为双脉冲宽度信号(扩宽脉冲宽度)
  8. 解读汽车车身设计中的流体力学奥秘
  9. 简直太牛了!这个微信插件神器开源,怒冲GitHub排名榜第一,网友:太好用了!...
  10. 微信朋友圈python广告投了多少钱_朋友圈广告的评论区,当代人的灌水BBS