likes
comments
collection
share

一次搞懂原型链和继承😉

作者站长头像
站长
· 阅读数 45

前言

最近正值秋招,陆陆续续投递了一些简历,发现自己很多基础知识都已经忘记,或是之前压根没有看过🤣。为此,复习总结一波关于 JavaScript 的八股合集,一方面是为了帮助自己更好的 拿捏 面试笔试及巩固基础,另外一方面是为帮助到也需要复习关于 JavaScript 的八股的同学😊。

原型链

原型链由来

JavaScript 这门语言就是基于 原型 这种设计模式来设计的,所以 JavaScript 中才会出现 原型链 这个概念。

基于 原型链JavaScript 这门语言实现 面对对象编程 ,让 JavaScript对象 拥有 封装、继承和多态 等众多面对对象特性。

原型链概念

  • 构造函数 (构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。首字母要大写)
  • prototype (每个函数都有一个 prototype 属性,函数的 prototype 属性指向了一个对象, prototype 对象用于放某同一类型实例的共享属性和方法,实质上是为了内存着想。)

一次搞懂原型链和继承😉

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

一次搞懂原型链和继承😉

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

一次搞懂原型链和继承😉

  • 实例、构造函数、原型对象的关系 (构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。)

原型链四道关卡

  • 第一关,什么是原型?

原型就是一个对象,就是 构造函数.prototype 代表的这个对象。

  • 第二关,什么是原型链?

一个个对象通过 __proto__属性 串连起来,形成的 链式结构 就是 原型链

  • 第三关,理解这张原型链图

这张图初看可能会觉得很复杂,感觉各种关系,好多线条~

但是不要怕,首先我们可以发现它是有三列的,第一列是我们使用 new 实例化的对象,第二列是构造函数(首字母大写),第三列是原型对象(通过 实例对象._proto_ 或者 构造函数.prototype ,我们可以拿到这个对象)

然后根据 _proto_属性 来看看这张图有多少条原型链吧,你能找出来嘛?哈哈哈哈哈哈~

  1. f1 -> Foo.prototype -> Object.prototype -> null
  2. o1 -> Object.prototype -> null
  3. Foo -> Function.prototype -> Object.prototype -> null
  4. Object -> Function.prototype -> Object.prototype -> null
  5. Function -> Function.prototype -> Object.prototype -> null

我们可以发现以下几点 关键点 ,帮助我们解决原型链指向问题,妈妈再也不会担心你不会原型链了~

  1. 原型链的起点 要么是 实例对象 (如12条原型链),要么是 构造函数 (如345条原型链)。 原型链的终点 一定是 null
  2. new Object()生成的实例对象 以及 其他原型对象 (例如 Foo.prototype 和 Function.prototype ,除了 Object.prototype ),这两种对象的原型都是 Object.prototype
  3. 构造函数 (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;    // 确保返回值为对象
    }
    

继承

继承由来

为了让代码实现共享,提高代码的重用性,所以有了继承这个概念

继承解决了数据和逻辑的复制。通过继承,对象之间可以共享属性和方法,而不需要手动在每一个对象上添加属性和方法。

原型链继承

  1. 什么是原型链继承?
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.prototypechild1 继承了 Parent 后的变化,与没有继承有什么区别。

console.log(child1.balance); ,如果我们在这句代码前面打印一下 child1

一次搞懂原型链和继承😉

可以发现 child1 这个对象上是没有 balance 这个属性的,通过 原型链即__proto__ 查找,上一级,发现上一级 Parent 上有这个属性,便拿过来了,所以这里的 balance 其实是 Parent 的。

child1.useMoney(6800000); ,同上,我们可以 child1 上是没有 useMoney 这个方法的,我们拿的其实是 child1.__proto__.__proto__ 上的这个方法

  1. 原型链继承的特点

    • 改变子构造函数的原型对象即 Child.prototype = new Parent();

    • 所有实例对象共用同一个原型对象的属性和方法

  2. 原型链继承有哪些问题?

    • 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
    • 实例化的时候,不能向 Parent 传参
    • 子类的原型上多了不需要的父类属性,存在内存上的浪费

盗用构造函数继承

  1. 什么是盗用构造函数继承?
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 上的属性

  1. 盗用构造函数继承的特点

    • 在子构造函数 Child 中执行 Parent.call(this);

    • 避免了引用类型的属性被所有实例共享

    • 可以在 Child 中向 Parent 传参即 Parent.call(this, name, sex, ...);

    • 无法继承 Parent 的方法,需要在 Child 中定义

  2. 盗用构造函数继承有哪些问题?

    • 方法都在构造函数中定义,每次创建实例都会创建一遍方法。

组合继承

  1. 什么是组合继承?
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平的大房子

我们可以发现组合继承就是原型链继承和盗用构造函数继承的组合。代码就不是解释了,就是上面两个的结合,大家一看就能懂~

  1. 组合继承的特点

    • 在子构造函数 Child 中执行 Parent.call(this);

    • 改变子构造函数的原型对象即 Child.prototype = new Parent();

    • 避免了引用类型的属性被所有实例共享,继承 Parent 的方法

  2. 组合继承有哪些问题?

    • 即原型链继承的遗留问题,子类的原型对象上多了不需要的父类属性,存在内存上的浪费
  3. 补充

    我在学习完成第六种寄生式组合继承后,想到个办法解决组合继承这个 多余父类属性的问题 ,就是将 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 的原型就应该是 ParentChild.prototype -> Parent.prototype )。

原型式继承

  1. 什么是原型式继承?
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 的属性和方法,而且不像上面三种继承方法都会带上 ParentChild 的属性或方法。

  1. 原型式继承的特点

    • 改变子构造函数的原型对象即 Child.prototype = Parent;

    • 所有实例对象共用同一个原型对象的属性和方法

    • 将传入的对象作为创建的对象的原型,实例化的 child1、child2... 就只会默认继承 Parent 的属性和方法

    • 适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合

  2. 原型式继承有哪些问题?

    • 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
    • 实例化的时候,不能向 Parent 传参
    • 子类的原型上多了不需要的父类属性,存在内存上的浪费

寄生式继承

  1. 什么是寄生式继承?
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() {} 这个方法

  1. 寄生式继承的特点

    • 改变子构造函数的原型对象即 Child.prototype = Parent;

    • 所有实例对象共用同一个原型对象的属性和方法

    • 可以对 Child 增强,添加属性或者方法

  2. 寄生式继承有哪些问题?

    • 因为所有的实例对象(child1、child2...)用的都是同一个原型的属性,如果这个属性是引用类型,一个实例修改,其他地方都会修改
    • 实例化的时候,不能向 Parent 传参
    • 子类的原型上多了不需要的父类属性,存在内存上的浪费

寄生式组合继承

  1. 什么是寄生式组合继承?
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对象 ,大功告成。

一次搞懂原型链和继承😉

  1. 寄生式组合继承的特点

    • 将父类的原型赋值给了子类的原型,并且将构造函数设置为子类
  2. 寄生式组合继承有哪些问题?

    我暂时发现啥问题hhhhhhhh

    《JavaScript高级程序设计》里说:寄生式组合继承可以算是引用类型继承的最佳模式。

  3. 补充一种写法

借用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继承

  1. 什么是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
  1. Class继承的特点

    • classconstructorextendssuper 等关键字
    • 核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this)
  2. Class继承如何实现的?

    看这里

推荐文章

总结

通过对 原型链继承 这两个概念的深入学习和回顾,我发现自己学的很浅,很多知识还需要去补充完善。学海无涯,大家一起加油~😊😊😊

转载自:https://juejin.cn/post/7151728566688808968
评论
请登录