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);
相关文章
|
1月前
|
JavaScript 前端开发
JavaScript 继承的方式和优缺点
JavaScript 继承的方式和优缺点
13 0
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(三)
深入JS面向对象(原型-继承)
31 0
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
32 0
|
2月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承利用原型链查找属性,节省内存但不支持私有成员。类继承通过ES6的class和extends实现,支持私有成员但占用更多内存。两者各有优势,适用于不同场景。
21 0
|
4月前
|
JavaScript
|
4月前
|
JavaScript 前端开发
原型继承在 JavaScript 中是如何工作
原型继承在 JavaScript 中是如何工作
21 0
|
3天前
|
JavaScript 前端开发
JavaScript 中最常用的继承方式
JavaScript中的继承有多种实现方式:1) 原型链继承利用原型查找,但属性共享可能引发问题;2) 借用构造函数避免共享,但方法复用不佳;3) 组合继承结合两者优点,是最常用的方式;4) ES6的class继承,是语法糖,仍基于原型链,提供更直观的面向对象编程体验。
8 1
|
3天前
|
设计模式 JavaScript 前端开发
在JavaScript中,继承是一个重要的概念
JavaScript继承有优点和缺点。优点包括代码复用、扩展性和层次结构清晰。缺点涉及深继承导致的复杂性、紧耦合、单一继承限制、隐藏父类方法以及可能的性能问题。在使用时需谨慎,并考虑其他设计模式。
8 2
|
3天前
|
JavaScript 前端开发 开发者
JavaScript 继承的方式和优缺点
JavaScript 继承的方式和优缺点
9 0
|
12天前
|
JavaScript 前端开发
JavaScript 继承的方式和优缺点
JavaScript 继承的方式和优缺点