js 7种继承方式详解

简介: js 7种继承方式详解

目录


前言

原型链继承

构造函数继承

组合继承

原型式继承

寄生式继承

寄生组合继承

类的继承

概念

类继承用extends关键字

es6的类同时拥有__proto__和prototype。


前言



首先学习继承之前,要对原型链有一定程度的了解。


不了解可以去先阅读我另一篇文章,里面对原型链有一个较为详细的说明:js 原型链详解。


不了解call方法的用法的话,提前阅读:js apply、call、bind一篇掌握


如果已经了解请继续。


原型链继承



父类实例作为子类的原型

子类创造的两个实例的隐式原型__proto__指向父类的那个实例

而父类的实例的隐式原型__proto__又指向父类的原型father.prototype

根据原型链的特性,所有子类的实例继承父类原型上的属性。

这张图理解清晰后续我就不再重复解释了。


image.png

    //父类
    function father() {
      this.fatherAttr = ["fatherAttr"];
    }
    //父类的原型上的属性
    father.prototype.checkProto = "checkProto";
    //子类
    function child() {}
    // 将father实例作为child这个构造函数的原型
    child.prototype = new father();
    child.prototype.constructor = child;
    //两个子类实例
    const test1 = new child();
    const test2 = new child();
    console.log("测试1:");
    console.log("test1:", test1);
    console.log("test2:", test2);
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试2:");
    test1.fatherAttr.push("newAttr");
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试3:");
    console.log("test1.checkProto:", test1.checkProto);

image.png

特点:

两个实例对象都没有fatherAttr属性,但是因为父类的实例会拥有fatherAttr属性,且现在父类的实例作为child的原型,根据原型链,他们可以共享到自己的构造函数child的原型上的属性。(测试1)

因为只有一个父类的实例作为他们的原型,所以所有实例共享了一个原型上的属性fatherAttr,当原型上的属性作为引用类型时,此处是数组,test1添加一个新内容会导致test2上的fatherAttr也改变了。(测试2)(缺点)

child构造函数不能传递入参。(缺点)

实例可以访问到父类的原型上的属性,因此可以把可复用方法定义在父类原型上。(测试3)

构造函数继承



将父类上的this绑定到子类,也就是父类上创建的属性会拷贝到子类这个构造函数上,所以实例会继承这些属性。

    //父类
    function father(params) {
      this.fatherAttr = ["fatherAttr"];
      this.params = params;
    }
    //父类的原型上的属性
    father.prototype.checkProto = "checkProto";
    //子类
    function child(params) {
      father.call(this, params);
    }
    //两个子类实例
    const test1 = new child("params1");
    const test2 = new child("params2");
    console.log("测试1:");
    console.log("test1:", test1);
    console.log("test2:", test2);
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试2:");
    test1.fatherAttr.push("newAttr");
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试3:");
    console.log("test1.checkProto:", test1.checkProto);

image.png

特点:

两个实例对象都拥有了拷贝来的fatherAttr属性,所以没有共享属性,创造一个实例就得拷贝一次父类的所有属性,且因为不能继承父类原型,所以方法不能复用,被迫拷贝方法。(测试1)(缺点)

test1添加一个新内容只是改变了test1自己的属性,不会影响到test2。(测试2)

child构造函数可以传递参数,定制自己的属性。(测试1)

实例不能继承父类的原型上的属性。(测试3)(缺点)


组合继承



结合原型链继承和构造函数继承,可以根据两种继承特点进行使用。

    //父类
    function father(params) {
      this.fatherAttr = ["fatherAttr"];
      this.params = params;
    }
    //父类的原型上的属性
    father.prototype.checkProto = "checkProto";
    //子类
    function child(params) {
      //第二次调用了父类构造函数
      father.call(this, params);
    }
    // 将father实例作为child构造函数的原型
    child.prototype = new father();//第一次调用了父类构造函数
    child.prototype.constructor = child;
    //两个实例
    const test1 = new child("params1");//从这里跳转去子类构造函数第二次调用了父类构造函数
    const test2 = new child("params2");
    console.log("测试1:");
    console.log("test1:", test1);
    console.log("test2:", test2);
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试2:");
    test1.fatherAttr.push("newAttr");
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试3:");
    console.log("test1.checkProto:", test1.checkProto);
    console.log("测试4:");
    delete test1.fatherAttr
    console.log("test1:", test1);
    console.log("test1.fatherAttr:", test1.fatherAttr);

image.png

特点:

两个实例对象都拥有了拷贝来的fatherAttr属性,创造一个实例就得拷贝一次父类的所有属性(构造函数继承特点,测试1),但是能访问父类原型,可以把复用方法定义在父类原型上。(原型链继承特点,测试1)

test1添加一个新内容只是改变了test1自己的属性,不会影响到test2。(构造函数继承特点,测试2)

child构造函数可以传递参数,定制自己的属性。(构造函数继承特点,测试1)

实例能继承父类的原型上的属性。(原型链继承特点,测试3)

调用了两次父类的构造函数,生成两份实例,创建子类原型链一次,用子类创建实例时,子类内部里面一次,第二次覆盖了第一次。(缺点)

因为调用两次父类构造函数,如果用delete删除实例上拷贝来的fatherAttr属性,实例仍然拥有隐式原型指向的父类实例上的fatherAttr属性。(原型链继承特点,测试4)(缺点)

原型式继承



就是封装一个方法,在方法中创建一个临时的构造函数,入参为父类实例,将临时构造函数的原型绑定到传入的实例上并返回。

    //父类
    function father() {
      this.fatherAttr = ["fatherAttr"];
    }
    //父类的原型上的属性
    father.prototype.checkProto = "checkProto";
    function create(obj) {
      function child() {}
      child.prototype = obj;
      return new child();
    }
    const fatherObj = new father();
    //两个实例
    const test1 = create(fatherObj);
    const test2 = create(fatherObj);
    console.log("测试1:");
    console.log("test1:", test1);
    console.log("test2:", test2);
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试2:");
    test1.fatherAttr.push("newAttr");
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试3:");
    console.log("test1.checkProto:", test1.checkProto);

image.png

ECMAScript5新增了Object.create()方法规范化了原型式继承,这里的create函数中的代码就是Object.create()的核心部分,Object.create()还提供第二个参数,这里就不展开介绍了。

用Object.create()进行原型式继承:

    //父类
    function father() {
      this.fatherAttr = ["fatherAttr"];
    }
    //父类的原型上的属性
    father.prototype.checkProto = "checkProto";
    // function create(obj) {
    //   function child() {}
    //   child.prototype = obj;
    //   return new child();
    // }
    const fatherObj = new father();
    //两个实例
    const test1 = Object.create(fatherObj);
    const test2 = Object.create(fatherObj);
    console.log("测试1:");
    console.log("test1:", test1);
    console.log("test2:", test2);
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试2:");
    test1.fatherAttr.push("newAttr");
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试3:");
    console.log("test1.checkProto:", test1.checkProto);

image.png

结果一致。

特点:

  • 看测试结果,貌似与原型链方法特点一致。

寄生式继承


在原型式继承的基础上,再多封装一个函数,用于增强对象。

    //父类
    function father() {
      this.fatherAttr = ["fatherAttr"];
    }
    //父类的原型上的属性
    father.prototype.checkProto = "checkProto";
    function create(obj) {
      function child() {}
      child.prototype = obj;
      return new child();
    }
    function afterCreate(obj) {
      const clone = create(obj);
      clone.createAttr = "createAttr";
      return clone;
    }
    const fatherObj = new father();
    //两个实例
    const test1 = afterCreate(fatherObj);
    const test2 = afterCreate(fatherObj);
    console.log("测试1:");
    console.log("test1:", test1);
    console.log("test2:", test2);
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试2:");
    test1.fatherAttr.push("newAttr");
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试3:");
    console.log("test1.checkProto:", test1.checkProto);

image.png

特点:

在原型式继承的基础上,可以拥有afterCreate构造方法内部自带的属性,创建实例时,afterCreate方法自带的属性就自动寄生在实例身上,也就是能让所有调用afterCreate继承于任何不同对象的实例都会拥有这么一个属性。

寄生组合继承


结合寄生式继承和组合式继承,与寄生式继承的区别是:寄生式继承继承的是父类的实例,目的无非是需要父类的原型,那么我们去掉创建父类实例的那一步,直接把父类原型拿去继承,用增强对象的方式去实现将子类的原型指向父类的原型,不再用父类的实例作为子类的原型。

    //父类
    function father(params) {
      this.fatherAttr = ["fatherAttr"];
      this.params = params;
    }
    //父类的原型上的属性
    father.prototype.checkProto = "checkProto";
    function child(params) {
      father.call(this, params);
    }
    function create(obj) {
      function child() {}
      child.prototype = obj;
      return new child();
    }
    function afterCreate(ch, fa) {
      const clone = create(fa.prototype);
      clone.constructor = ch;
      ch.prototype = clone;
    }
    afterCreate(child, father);
    //两个实例
    const test1 = new child("params1");
    const test2 = new child("params2");
    console.log("测试1:");
    console.log("test1:", test1);
    console.log("test2:", test2);
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试2:");
    test1.fatherAttr.push("newAttr");
    console.log("test1.fatherAttr:", test1.fatherAttr);
    console.log("test2.fatherAttr:", test2.fatherAttr);
    console.log("测试3:");
    console.log("test1.checkProto:", test1.checkProto);
    console.log("测试4:");
    delete test1.fatherAttr;
    console.log("test1:", test1);
    console.log("test1.fatherAttr:", test1.fatherAttr);


image.png

特点:

拥有组合式继承的所有优点,且用寄生的方式修复了组合继承会调用两次父类构造函数的问题,也因为修复的这个问题,解决了删除对应实例上的属性,仍然能访问到原型链上属性的问题。

类的继承


概念



这是es6才推出的一个概念,原理也只是语法糖而已,用起来就容易得多了。


father中的constructor就是构造函数,father本身就是原型。


构造函数中的this指向实例。

    class father {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      fatherAttr = "fatherAttr";
    }
    const test1 = new father(1, 2);
    console.log(test1.x);//1
    console.log(test1.y);//2
    console.log(test1.fatherAttr);//"fatherAttr"

类继承用extends关键字


注意:

  1. 子类构造器constructor中必须要有super(),因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工,如果不调用super方法,子类就得不到this对象,会报错。
  2. 如果不写constructor,会默认存在,且调用了super()。
    class father {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      fatherAttr = "fatherAttr";
    }
    class child extends father {
      constructor(x, y, z) {
        super(x, y);
        this.z = z;
      }
      childAttr = "childAttr";
    }
    const test1 = new child(1, 2, 3);
    console.log(test1.x);//1
    console.log(test1.y);//2
    console.log(test1.z);//3
    console.log(test1.fatherAttr);//"fatherAttr"
    console.log(test1.childAttr);//"childAttr"

es6的类同时拥有__proto__和prototype。


    class father {}
    class child extends father {}
    console.log(child.prototype.__proto__ == father.prototype);
    console.log(child.__proto__ == father);

image.png

因为继承实现模式大概是这样:

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