js--如何实现继承?

前言

  学习过 java 的同学应该都知道,常见的继承有接口继承和实现继承,接口继承只需要继承父类的方法签名,实现继承则继承父类的实际的方法,js 中主要依靠原型链来实现继承,无法做接口继承。

  学习 js 继承之前,我们需要了解原型这一 概念,我们知道 js 中创建对象通过构造函数来创建,而每一个构造函数都有对应的 prototype 的属性,该属性对应的值为一个对象,这个对象也就是所有通过该构造函数创建出来的实例所共享的属性和方法,而创建出来的每一个实例对象都有一个指针指向这些共享的属性和方法,这个指针就是所说的 __proto__(注意这里是双下划线),因此就产生了三种来获取原型的方法,分别是 p.__proto__,p.constructor.prototype,Object.getPrototypeOf( p ),这就是我对原型的了解。

  当我们在访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会在它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是这样一层一层向上找下去,也就产生了原型链的概念。原型链的尽头就是 object.prototype ,所以我们每创建的一个对象都有 toString(),valueOf() 等方法的原因。

  有了上面的基础常识作为铺垫,我们来看下 js 中具体怎么来实现继承。

正文

  js 中实现继承的方法有6种,具体实现如下:

  (1)原型链实现继承

        //定义父类
        function superFun(){
          this.superProperty = "super"//给父类构造函数添加参数属性
        }
        superFun.prototype.getSuperValue = function(){//给父类构造函数添加原型方法
          return this.superProperty
        }
        //定义子类
        function subFun(){
          this.subProperty = "sub"
        }
        subFun.prototype = new superFun()//继承了superFun父类 ,这一点最主要
        subFun.prototype.getSubValue = function(){//在继承父类之后,在原型上添加新的方法或者重写父类的方法
          return this.subProperty
        }
        var sub = new subFun()//实例化一个子类对象
        console.log(sub.superProperty);//super--判断继承父类的属性
        console.log(sub.subProperty);//sub--子类的实例的属性
        console.log(sub.getSuperValue());//super--判断继承父类的方法
        console.log(sub.getSubValue());//sub----子类实例的方法
        console.log(sub instanceof superFun);//true----原型链判断
        console.log(sub instanceof subFun);//true----原型判断

  上面的代码需要注意必须在继承父类语句之后才能在其原型上添加新的方法或者重写父类的方法,同时添加新的方法的时候不能使用字面量的形式添加。

  所有的函数的默认原型都是 object,默认原型都会包含一个内部指针指向 object.prototype ,因此所有自定义的对象都有 toString()方法和 valueOf() 方法。

  确定原型和实例的关系的方法可以使用:instanceof 和 isPrototypeOf。

  优缺点:上面的方法让新实例的原型等于父类的实例实现了原型链的继承,子类的实例能够继承构造函数的属性,构造函数的方法,父类的构造函数的属性以及父类原型上的方法,但是新实例无法向构造函数传参,继承单一,所有的新实例都会共享父类构造函数的属性,因此在父类构造函数种定义一个引用数据类型的时候,每个字类的实例都有拥有该引用类型的属性,当其中一个实例对该属性做了修改,别的实例也会收到影响。例子如下:

       //定义父类
        function superFun(){
          this.superProperty =  {name:"xiaoming",age:20}//给父类构造函数添加参数属性
        }
        superFun.prototype.getSuperValue = function(){//给父类构造函数添加原型方法
          return this.superProperty
        }
        //定义字类
        function subFun(){
          this.subProperty = "sub"
        }
        subFun.prototype = new superFun()//继承了superFun父类 ,这一点最主要
        subFun.prototype.getSubValue = function(){//在继承父类之后,在原型上添加新的方法或者重写父类的方法
          return this.subProperty
        }
        var sub1 = new subFun()
        var sub2 = new subFun()
        console.log(sub2.superProperty.name);//xiaoming
        sub1.superProperty.name = "xiaohong"
        console.log(sub2.superProperty.name);//xiaohong

  (2)借用构造函数实现继承

      //定义父类
      function superFun(superProperty) {
        this.superProperty = superProperty; //给父类构造函数添加参数属性
      }
      superFun.prototype.getSuperValue = function () {
        //给父类构造函数添加原型方法
        return this.superProperty;
      };
      function subFun() {
        superFun.call(this, "super");
        this.subProperty = "sub";
      }
      subFun.prototype.getSubValue = function () {
        return this.subProperty;
      };
      var sub = new subFun();
      console.log(sub.superProperty); //super--判断继承父类的属性
      console.log(sub.subProperty); //sub--子类的实例的属性
      //console.log(sub.getSuperValue()); //报错sub.getSuperValue is not a function--判断继承父类的方法 不能继承
      console.log(sub.getSubValue()); //sub----子类实例的方法
      console.log(sub instanceof superFun); //false----原型链判断
      console.log(sub instanceof subFun); //true----原型判断

  上面的方法借用构造函数实现继承,主要是用 call() 或者apply() 在子类的构造函数内部调用父类的构造函数,就相当于在子类构造函数内部做了父类函数的复制并且自执行。

  优缺点:通过构造函数实现继承,只能继承父类构造函数的属性,不能继承父类原型上面的方法,无法实现构造函数的复用,每次用每次都要重新调用,相当于每个新实例都有父类构造函数的副本,造成臃肿,但是这种方法能够解决原型链不能传参的问题,对父类构造函数种属性为引用数据类型的问题,以及通过多个 call 解决单一继承问题等。

   (3)原型链和构造函数组合实现继承(常用)

      //定义父类
      function superFun(superProperty) {
        this.superProperty = superProperty;
        this.superPropertyList = ["red", "blue", "green"];
      }
      superFun.prototype.getSuperValue = function () {
        return this.superProperty;
      };
      function subFun(property1, property2) {
        superFun.call(this, property1); //继承属性
        this.subProperty = property2;
      }
      subFun.prototype = new superFun(); //继承方法
      subFun.prototype.constructor = superFun;
      //添加字类新方法
      subFun.prototype.getSubValue = function () {
        return this.subProperty;
      };
      var sub1 = new subFun("sub1Tosuper", "sub1Property");
      var sub2 = new subFun("sub2Tosuper", "sub2Property");
      console.log(sub2.superPropertyList); //["red", "blue", "green"]
      sub1.superPropertyList.push("black");
      console.log(sub2.superPropertyList); //["red", "blue", "green"]   父类引用类型数据两者互不干扰
      console.log(sub1.superProperty); //sub1Tosuper--继承父类的属性
      console.log(sub1.getSuperValue()); //sub1Tosuper--继承父类方法
      console.log(sub1.subProperty); //sub1Property--子类的属性
      console.log(sub1.getSubValue()); //sub1Property--子类方法

  上面的代码使用原型链和构造函数组合实现了继承,其中通过原型链实现对原型的属性和方法的继承,通过借用构造函数来实现对实例属性的继承,这样即保证了函数的调用,有实现了每个实例都有自己的属性,解决了实例中属性干扰的问题。

  优缺点:这种方法结合了前两种模式的优点,达到了传参和复用的效果,可以继承父类原型的属性和方法,可以传参,可以复用,同时每个新实例引入的构造函数的属性都是私有的,但是实现需要调用两次父类构造函数,这样就存在内存消耗问题,子类的构造函数会代替原型上的那个父类构造函数。

   (4)原型式实现继承

      //定义父类
      function superFun(superProperty) {
        this.superProperty = superProperty;
      }
      superFun.prototype.getSuperValue = function () {
        return this.superProperty;
      };
      function subFun(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
      }
      var super1 = new superFun("super1");
      var sub = subFun(super1);
      console.log(sub.superProperty);//super1
      console.log(sub.getSuperValue());//super1

  上面的代码重点在于在 subFun() 函数内部创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例,相当于用一个函数包装了一个对象,然后返回这个函数的的调用,这个函数会就编程了可以随意增添属性的实例或者对象, object.create() 就是这个原理。es5中object.create() 接受两个参数,一个参数作为新对象原型的对象,另一个可选参数作为新对象定义额外属性的对象,当两个参数都存在的时候,任何属性都会覆盖原型对象上的同名属性。

  优缺点:这种方法类似于复制一个对象,用函数来包装,其实就是哪一个对象作为继承,然后传入另一个对象,本质就是对传入的对象进行一次浅拷贝,但是所有实例都会继承原型上的属性,且无法实现复用,若包含引用数据类型始终会共享相应的值。

   (5)寄生式实现继承

      //定义父类
      function superFun(superProperty) {
        this.superProperty = superProperty;
      }
      superFun.prototype.getSuperValue = function () {
        return this.superProperty;
      };
      function subFun(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
      }
      var super1 = new superFun("super1");
      function subObject(obj){
        var sub=subFun(obj)
        sub.subPorperty="subPorperty"
        return sub
      }
      var sub = subObject(super1);
      console.log(sub.superProperty); //super1
      console.log(sub.subPorperty);//subPorperty
      console.log(sub.getSuperValue()); //super1

  上面的代码对比原型式继承,其实就是在原型式继承的基础上套了一层壳子,创建了一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回一个对象。

  优缺点:这种方法没有创建自定义类型,因为只是给返回的对象添加了一层壳子,实现了创建的新对象,但是这种方法没有用到原型,无法实现复用。

  (5)寄生组合实现实现继承(常用)

  针对组合实现继承存在的问题进行了优化,前面说到组合继承要调用两次父类构造函数,第一次是在创建子类原型的时候,第二次是在子类构造函数内部 call 调用。对于这两次调用,第一次调用父类是可以避免的,不必为了指定子类型的原型而调用夫类型的构造函数,我们无非是需要一个父类型原型的一个副本而已。

      //定义父类属性
      function superFun(superProperty) {
        this.superProperty = superProperty;
        this.superPropertyList = ["red", "blue", "green"];
      }
      //定义父类原型上的方法
      superFun.prototype.getSuperValue = function () {
        return this.superProperty;
      };
      //使用寄生
      function object(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
      }
      function inheritObject(subFun, superFun) {
        var _prototype = object(superFun.prototype); //创建对象
        _prototype.constructor = subFun; //增强对象
        subFun.prototype = _prototype; //指定对象
      }
      //使用组合
      function subFun(tosuperProperty,subProperty){
        superFun.call(this,tosuperProperty)
        this.subProperty=subProperty
      }
      //子类继承父类
      inheritObject(subFun,superFun)
      //子类原型的方法
      subFun.prototype.getSubValue=function(){
        return this.subProperty
      }
      var sub=new subFun("super","sub")
      console.log(sub.superProperty);//super
      console.log(sub.subProperty);//sub
      console.log(sub.getSuperValue());//super
      console.log(sub.getSubValue());//sub
      console.log(sub instanceof superFun);//true
      console.log(sub instanceof subFun);//true

  上面的方法是 js 中实现继承最常见方法,它完美解决了组合式继承的中两次调用父类原型的bug,通过寄生,在函数内部返回对象然后调用,使用组合,使得函数的原型等于另一个实例,在函数中调用 call 引入另一个构造函数,实现了可以传参的功能,避免了在父类原型上创建不必要的属性,成为最理想的实现继承的方法。需要注意  inheritObject() 函数接受两个参数,分别式子类和父类的两个构造函数。

  优缺点:使用寄生式继承实现了继承父类的原型,然后再将结果指定给子类型的原型。使用组合继承得到传参复用等效果。

  (6)ES5/ES6 的继承除了写法以外还有什么区别

  1.ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this)).
  2.ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必须先调用父类的 super()方法),然后再用子类的构造函数修改 this。
  3.ES5 的继承时通过原型或构造函数机制来实现。
  4.ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。
  5.子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。
  6.注意 super 关键字指代父类的实例,即父类的 this 对象。
  7.注意:在子类构造函数中,调用 super 后,才可使用 this 关键字,否则报错。
  function 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
const bar = new Bar();
// it's ok
function Bar() {
  this.bar = 42;
}
const foo = new Foo();
// ReferenceError: Foo is not defined
class Foo {
  constructor() {
    this.foo = 42;
  }
}
  class 声明内部会启用严格模式。
// 引用一个未声明的变量
function Bar() {
  baz = 42;
  // it's ok
}
const bar = new Bar();
class Foo {
  constructor() {
    fol = 42;
    // ReferenceError: fol is not defined
  }
}
const foo = new Foo();

 

  class 的所有方法(包括静态方法和实例方法)都是不可枚举的
// 引用一个未声明的变量
function Bar() {
  this.bar = 42;
}
Bar.answer = function () {
  return 42;
};
Bar.prototype.print = function () {
  console.log(this.bar);
};
const barKeys = Object.keys(Bar);
// ['answer']
const barProtoKeys = Object.keys(Bar.prototype);
// ['print']
class Foo {
  constructor() {
    this.foo = 42;
  }
  static answer() {
    return 42;
  }
  print() {
    console.log(this.foo);
  }
}
const fooKeys = Object.keys(Foo);
// []
const fooProtoKeys = Object.keys(Foo.prototype);
// []

 

  class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
function Bar() {
  this.bar = 42;
}
Bar.prototype.print = function () {
  console.log(this.bar);
};
const bar = new Bar();
const barPrint = new bar.print();
// it's ok
class Foo {
  constructor() {
    this.foo = 42;
  }
  print() {
    console.log(this.foo);
  }
}
const foo = new Foo();
const fooPrint = new foo.print();
// TypeError: foo.print is not a constructor

 

  必须使用 new 调用 class。
function Bar() {
  this.bar = 42;
}
const bar = Bar();
// it's ok
class Foo {
  constructor() {
    this.foo = 42;
  }
}
const foo = Foo();
// TypeError: Class constructor Foo cannot be invoked without 'new'

 

  class 内部无法重写类名。
function Bar() {
  Bar = "Baz";
  // it's ok
  this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}
class Foo {
  constructor() {
    this.foo = 42;
    Foo = "Fol";
    // TypeError: Assignment to constant variable
  }
}
const foo = new Foo();
Foo = "Fol";
// it's ok

 

总结

  以上就是本文的全部内容,希望给读者带来些许的帮助和进步,方便的话点个关注,小白的成长之路会持续更新一些工作中常见的问题和技术点。

 

posted @ 2021-04-06 15:01  zaisy'Blog  阅读(803)  评论(0编辑  收藏  举报