一次搞懂原型链和继承😉
前言
最近正值秋招,陆陆续续投递了一些简历,发现自己很多基础知识都已经忘记,或是之前压根没有看过🤣。为此,复习总结一波关于 JavaScript 的八股合集,一方面是为了帮助自己更好的 拿捏 面试笔试及巩固基础,另外一方面是为帮助到也需要复习关于 JavaScript 的八股的同学😊。
原型链
原型链由来
JavaScript 这门语言就是基于 原型 这种设计模式来设计的,所以 JavaScript 中才会出现 原型链 这个概念。
基于 原型链 , JavaScript 这门语言实现 面对对象编程 ,让 JavaScript对象 拥有 封装、继承和多态 等众多面对对象特性。
原型链概念
构造函数(构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。首字母要大写)prototype(每个函数都有一个prototype属性,函数的prototype属性指向了一个对象,prototype 对象用于放某同一类型实例的共享属性和方法,实质上是为了内存着想。)

_proto_(每一个JavaScript对象(除了null)都具有的一个属性,叫__proto__,这个属性会指向该对象的原型)

constructor(每个原型对象都有一个constructor属性指向关联的构造函数)

实例、构造函数、原型对象的关系(构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。)
原型链四道关卡
- 第一关,什么是原型?
 
原型就是一个对象,就是 构造函数.prototype 代表的这个对象。
- 第二关,什么是原型链?
 
一个个对象通过 __proto__属性 串连起来,形成的 链式结构 就是 原型链 。
- 第三关,理解这张原型链图
 
这张图初看可能会觉得很复杂,感觉各种关系,好多线条~
但是不要怕,首先我们可以发现它是有三列的,第一列是我们使用 new 实例化的对象,第二列是构造函数(首字母大写),第三列是原型对象(通过 实例对象._proto_ 或者 构造函数.prototype ,我们可以拿到这个对象)
然后根据 _proto_属性 来看看这张图有多少条原型链吧,你能找出来嘛?哈哈哈哈哈哈~
- f1 -> Foo.prototype -> Object.prototype -> null
 - o1 -> Object.prototype -> null
 - Foo -> Function.prototype -> Object.prototype -> null
 - Object -> Function.prototype -> Object.prototype -> null
 - Function -> Function.prototype -> Object.prototype -> null
 
我们可以发现以下几点 关键点 ,帮助我们解决原型链指向问题,妈妈再也不会担心你不会原型链了~
原型链的起点要么是实例对象(如12条原型链),要么是构造函数(如345条原型链)。原型链的终点一定是null。new Object()生成的实例对象以及其他原型对象(例如 Foo.prototype 和 Function.prototype ,除了 Object.prototype ),这两种对象的原型都是Object.prototype。构造函数(Foo、Object、Function)的原型都是Function.prototype。

- 第四关,常见面试题,你会了嘛?
- 如何理解原型及原型链
 - 如何重写原型
 - 原型链指向
 - 如何手写实现一个 new
 
function create() { let Con = [].shift.call(arguments); // 获取构造函数 // obj.__proto__ = Con.prototype; 这种写法obj.__proto__ 已弃用,影响性能,可见mdn let obj = Object.create(Con.prototype) // 创建一个对象,并设置该对象的原型 let result = Con.apply(obj, arguments); // 绑定 `this` 并执行构造函数 return result instanceof Object ? result : obj; // 确保返回值为对象 } 
继承
继承由来
为了让代码实现共享,提高代码的重用性,所以有了继承这个概念
继承解决了数据和逻辑的复制。通过继承,对象之间可以共享属性和方法,而不需要手动在每一个对象上添加属性和方法。
原型链继承
- 什么是原型链继承?
 
function Parent () {
    this.balance = 100000000;    // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) {    // 你爹花钱的本领
    this.balance -= number;
    console.log(`花了${number}块`);
    console.log(`余额${this.balance}块`);
}
function Child () {    // 上帝之手造好了生产你的构造函数
}
Child.prototype = new Parent();    // 上帝之手把你和你爹强行绑定血缘关系,基因遗传,将Child的原型对象换成new Parent()生成的对象
var child1 = new Child();    // 你出生了,并通过原型链,继承了你爹的余额以及你爹花钱的本领
console.log(child1.balance);  // 你也有可以获取到balance这个属性
child1.useMoney(6800000);    // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
上面代码就是原型链继承,有个 Parent 构造函数,有个 balance 余额属性,Parent.prototype 上有个 useMoney 方法,然后有个 Child 构造函数。
然后我们重点来看最后四句代码,下面将一句一句解析
Child.prototype = new Parent(); 这句代码将

换成了

注意:[[Prototype]就是实例对象上的__proto__属性
即 Child.prototype 这个属性,原来存放的是 { constructor: ƒ Child(), [[Prototype]]: Object } ,现在变成了 { [[Prototype]]: Parent }
var child1 = new Child(); 实例化一个 child1 对象,打印它

如果我们不继承 Parent 应该是这样

即 child1.__proto__ 这个属性等价与 Child.prototype (如果不理解这句,请看知识点一:原型链), child1.__proto__ 原来存放的是 { constructor: ƒ Child(), [[Prototype]]: Object } ,现在变成了 { [[Prototype]]: Parent }
至此,我们清楚了 Child.prototype 和 child1 继承了 Parent 后的变化,与没有继承有什么区别。
console.log(child1.balance); ,如果我们在这句代码前面打印一下 child1

可以发现 child1 这个对象上是没有 balance 这个属性的,通过 原型链即__proto__ 查找,上一级,发现上一级 Parent 上有这个属性,便拿过来了,所以这里的 balance 其实是 Parent 的。
child1.useMoney(6800000); ,同上,我们可以 child1 上是没有 useMoney 这个方法的,我们拿的其实是 child1.__proto__.__proto__ 上的这个方法
- 
原型链继承的特点
- 
改变子构造函数的原型对象即
Child.prototype = new Parent(); - 
所有实例对象共用同一个原型对象的属性和方法
 
 - 
 - 
原型链继承有哪些问题?
- 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
 - 实例化的时候,不能向 
Parent传参 - 子类的原型上多了不需要的父类属性,存在内存上的浪费
 
 
盗用构造函数继承
- 什么是盗用构造函数继承?
 
function Parent () {
    this.balance = 100000000;    // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) {    // 你爹花钱的本领
    this.balance -= number;
    console.log(`花了${number}块`);
    console.log(`余额${this.balance}块`);
}
function Child () {    // 上帝之手造好了生产你的构造函数
    Parent.call(this);    // 执行了 Parent 这个构造函数,call替换上下文环境为Child
}
var child1 = new Child();    // 你出生了,继承了你爹的余额
console.log(child1.balance);  // 你也有balance这个属性
child1.useMoney(6800000);    // 报错,你没有继承这个能力呜呜呜
上面代码就是盗用构造函数继承,与原型链继承的区别,我们一眼就可以看出来,改变原型对象那句代码没有了。我们在 Child 这个构造函数里面加了要一句这个代码:
Parent.call(this);
这句代码在 new Child(); 时执行,就是执行了 Parent 这个构造函数,但是通过 .call(this) ,将执行 Parent 的上下文换成了 Child 的上下文,相当于在 new Child(); 生成的这个新的对象里面执行了一句 this.balance = 100000000; ,然后我们把我这个 new 出来的对象赋给了 child1 ,所以 child1 这个对象就会有 Parent 的属性,就像继承过来一样~这样就解决了上面原型链继承共享属性的问题了。
但是, child1.useMoney(6800000);报错了

对的,我们没有继承到 Parent 上的方法,我们只是继承了 Parent 上的属性
- 
盗用构造函数继承的特点
- 
在子构造函数
Child中执行Parent.call(this); - 
避免了引用类型的属性被所有实例共享
 - 
可以在
Child中向Parent传参即Parent.call(this, name, sex, ...); - 
无法继承
Parent的方法,需要在Child中定义 
 - 
 - 
盗用构造函数继承有哪些问题?
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
 
 
组合继承
- 什么是组合继承?
 
function Parent () {
    this.balance = 100000000;    // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) {    // 你爹花钱的本领
    this.balance -= number;
    console.log(`花了${number}块`);
    console.log(`余额${this.balance}块`);
}
function Child () {    // 上帝之手造好了生产你的构造函数
    Parent.call(this);    // 执行了 Parent 这个构造函数,call替换上下文环境为Child
}
Child.prototype = new Parent();    // 上帝之手把你和你爹强行绑定血缘关系,基因遗传,将Child的原型对象换成new Parent()生成的对象
Child.prototype.constructor = Child;    // 不加上这句,Child.prototype.constructor就是new Parent()这个实例对象的原型对象上的constructor了,即ƒ Parent (name) {...}
var child1 = new Child();    // 你出生了,继承了你爹的余额
console.log(child1.balance);  // 你也有balance这个属性
child1.useMoney(6800000);    // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
我们可以发现组合继承就是原型链继承和盗用构造函数继承的组合。代码就不是解释了,就是上面两个的结合,大家一看就能懂~
- 
组合继承的特点
- 
在子构造函数
Child中执行Parent.call(this); - 
改变子构造函数的原型对象即
Child.prototype = new Parent(); - 
避免了引用类型的属性被所有实例共享,继承
Parent的方法 
 - 
 - 
组合继承有哪些问题?
- 即原型链继承的遗留问题,子类的原型对象上多了不需要的父类属性,存在内存上的浪费
 
 - 
补充
我在学习完成第六种寄生式组合继承后,想到个办法解决组合继承这个
多余父类属性的问题,就是将Child.prototype = new Parent();这句代码换成Child.prototype = Parent.prototype;,这样就可以解决这个问题,如下图:child1 -> Child.prototype -> Object.prototype -> null

而且这样还比寄生式组合继承少套了一层原型,大家可以对比一下,下图是寄生式组合继承
child1 -> Child.prototype -> Parent.prototype -> Object.prototype -> null

虽然解决了这个
多余父类属性的问题,但是破坏了原型链,本来Child继承了Parent,那么Child的原型就应该是Parent(Child.prototype -> Parent.prototype)。 
原型式继承
- 什么是原型式继承?
 
function Parent () {
    this.balance = 100000000;    // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) {    // 你爹花钱的本领
    this.balance -= number;
    console.log(`花了${number}块`);
    console.log(`余额${this.balance}块`);
}
var parent = new Parent();
function createObj(Parent) {    // 接受的式一个实例对象,即Parent的实例对象parent
    function Child(){}
    Child.prototype = Parent;
    return new Child();
}
var child1 = createObj(parent)
我们可以发现这不就是 Object.create(只传入一个参数的情况) ,而且很像原型链继承,感觉就是一个 变种 。把继承的操作都封装在 createObj 这个函数中,并直接返回一个 Child 的实例对象, 我们就不能去定义 Child 的特有属性和方法了,实例化的 child1、child2... 就只会默认继承 Parent 的属性和方法,而且不像上面三种继承方法都会带上 Parent 和 Child 的属性或方法。
- 
原型式继承的特点
- 
改变子构造函数的原型对象即
Child.prototype = Parent; - 
所有实例对象共用同一个原型对象的属性和方法
 - 
将传入的对象作为创建的对象的原型,实例化的
child1、child2...就只会默认继承Parent的属性和方法 - 
适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合
 
 - 
 - 
原型式继承有哪些问题?
- 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
 - 实例化的时候,不能向 
Parent传参 - 子类的原型上多了不需要的父类属性,存在内存上的浪费
 
 
寄生式继承
- 什么是寄生式继承?
 
function object(Parent) {
    function Child(){}
    Child.prototype = Parent;
    return new Child();
}
function createAnother(Parent){
    let Child = object(Parent);    // 通过调用函数创建一个新对象
    Child.sayHi = function() {    // 以某种方式增强这个对象
        console.log("hi");
    };
    return Child;    // 返回这个对象
}
这怎么还调用了上面的原型式继承?确实,这感觉就是原型式的增强版。我们在原型式继承的时候不是发现 把继承的操作都封装在 object 这个函数中,并直接返回一个 Child 的实例对象, 我们就不能去定义 Child 的特有属性和方法了 ,然后寄生式继承,就是在原型式外面再套一个函数,然后以某种方式增强这个对象,比如添加 Child.sayHi = function() {} 这个方法
- 
寄生式继承的特点
- 
改变子构造函数的原型对象即
Child.prototype = Parent; - 
所有实例对象共用同一个原型对象的属性和方法
 - 
可以对
Child增强,添加属性或者方法 
 - 
 - 
寄生式继承有哪些问题?
- 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
 - 实例化的时候,不能向 
Parent传参 - 子类的原型上多了不需要的父类属性,存在内存上的浪费
 
 
寄生式组合继承
- 什么是寄生式组合继承?
 
function Parent () {
    this.balance = 100000000;    // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) {    // 你爹花钱的本领
    this.balance -= number;
    console.log(`花了${number}块`);
    console.log(`余额${this.balance}块`);
}
function Child () {    // 上帝之手造好了生产你的构造函数
    Parent.call(this);    // 执行了 Parent 这个构造函数,call替换上下文环境为Child
}       
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
function inheritPrototype(Child, Parent) {
    var prototype = object(Parent.prototype);    // 创建一个prototype对象,prototype对象的原型对象为Parent.prototype
    prototype.constructor = Child;    // prototype对象的constructor指向Child
    Child.prototype = prototype;    // 将Child的原型对象替换成我们创建好的新的prototype对象
}
inheritPrototype(Child, Parent);    // 继承
var child1 = new Child();    // 你出生了,继承了你爹的余额以及你爹花钱的本领
console.log(child1.balance);    // 你也有balance这个属性
child1.useMoney(6800000);    // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
我们的组合继承已经很好了,但是他有个问题,组合继承会调用两次父构造函数,一次是设置子类型实例的原型的时候  Child.prototype = new Parent(); 一次是在创建子类型实例的时候, new Child 时回执行  Parent.call(this, name); ,最后造成子实例对象的原型对象上多了不需要的父类属性,存在内存上的浪费。如下图所示:

所以就有了寄生式组合继承,就是为了将两次变成一次,依然达到继承父构造函数的属性的原型上的方法。
我们优化的是 Child.prototype = new Parent(); 这句,原来这句代码确实让我可以拿到 Parent 原型对象上的方法,但是这样是直接 new 了一个 Parent 实例,挂到 Child.prototype 上,这里就又把Parent 的属性创建了一遍。
所以我们首先var prototype = object(Parent.prototype); 通过原型式创建出来 prototype 这个对象,这个对象的原型是 Parent.prototype ,自然我们可以通过 prototype 这个对象获取到Parent.prototype 上的方法了。 prototype.constructor = Child; 这一步将 prototype对象 的 constructor 指向 Child ,解决了默认 constructor 丢失的问题,Child.prototype = prototype; , 将 Child 的原型对象替换成我们创建好的新的 prototype对象 ,大功告成。

- 
寄生式组合继承的特点
- 将父类的原型赋值给了子类的原型,并且将构造函数设置为子类
 
 - 
寄生式组合继承有哪些问题?
我暂时发现啥问题hhhhhhhh
《JavaScript高级程序设计》里说:寄生式组合继承可以算是引用类型继承的最佳模式。
 - 
补充一种写法
 
借用Object.create来写
function Parent () {
    this.balance = 100000000;    // 你爹的余额,一个小目标
}
Parent.prototype.useMoney = function (number) {    // 你爹花钱的本领
    this.balance -= number;
    console.log(`花了${number}块`);
    console.log(`余额${this.balance}块`);
}
function Child () {    // 上帝之手造好了生产你的构造函数
    Parent.call(this);    // 执行了 Parent 这个构造函数,call替换上下文环境为Child
}       
Child.prototype = Object.create(Parent.prototype, { 
    constructor: {
        value: Child, 
        enumerable: false,
        writable: true,
        configurable: true
    } 
})
var child1 = new Child();    // 你出生了,继承了你爹的余额以及你爹花钱的本领
console.log(child1.balance);    // 你也有balance这个属性
child1.useMoney(6800000);    // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
Class继承
- 什么是Class继承?
 
class Parent {
    constructor() {
    this.balance = 100000000;
    }
    useMoney(number) {    // 你爹花钱的本领
        this.balance -= number;
        console.log(`花了${number}块`);
        console.log(`余额${this.balance}块`);
    }
}
class Child extends Parent {
    constructor() {
        super()
    }
}
let child1 = new Child()
console.log(child1.balance);    // 你也有balance这个属性
child1.useMoney(6800000);    // 你花680w在上海浦东新区北蔡镇买了套100平的大房子
child instanceof Parent // true
- 
Class继承的特点
class、constructor、extends、super等关键字- 核心在于使用 
extends表明继承自哪个父类,并且在子类构造函数中必须调用super,因为这段代码可以看成Parent.call(this) 
 - 
Class继承如何实现的?
 
推荐文章
- 冴羽的JavaScript深入之从原型到原型链
 - 为什么是
person1.prototype.constructor而不是person1.constructor指向构造函数?如果你这些概念很混乱,这篇文章,清晰好懂,能清楚这些东西为什么要这样设计,虽然这样设计不好懂,但是里面自有他的学问 - JavaScript 高级程序设计第 4 版PDF版
 - 冴羽的JavaScript深入之继承的多种方式和优缺点
 
总结
通过对 原型链 和 继承 这两个概念的深入学习和回顾,我发现自己学的很浅,很多知识还需要去补充完善。学海无涯,大家一起加油~😊😊😊
转载自:https://juejin.cn/post/7151728566688808968