JS面试题之你知道几种实现继承的方法?

130 阅读2分钟

先了解原型和原型链的知识,有助于你理解本文的代码。

先来看一些不常用的继承方式(因为都有各自的缺点):

1. 原型链继承

    // 父类
    function Animal(name){
      this.name = name
      this.hobbies = ['music', 'dance']
    }
    Animal.prototype.say = function(){
        console.log('hello,my name is', this.name);
      } 
    // 子类
    function Dog(name){
      this.name = name
    }
    // 继承
    Dog.prototype = new Animal() // 重点代码
    let dog1 = new Dog('小黑')
    let dog2 = new Dog('小白')
    console.log(dog1.name, dog2.name); // 小白 小黑
    dog1.hobbies.push('eat')
    console.log(dog1.hobbies, dog2.hobbies); // ['music','dance', 'eat']  ['music','dance', 'eat']

通过例子可以发现,该继承方法最大的缺点就是 --- 引用类型的属性会被所有实例共享(因为属性值指向同一个内存地址)。

2. 构造函数继承

   // 父类
    function Animal(name){
      this.name = name
      this.hobbies = ['music', 'dance']
      this.say = function(){
        console.log('hello,my name is ', this.name);
      } 
    }
    // 子类
    function Dog(name){
      Animal.call(this, name) // 重点  相当于把父类代码复制一份放到这
    }
   
    let dog1 = new Dog('小黑')
    let dog2 = new Dog('小白')
    console.log(dog1, dog2); // 小白 小黑
    dog1.hobbies.push('eat')
    console.log(dog1.hobbies, dog2.hobbies); // ['music','dance', 'eat']  ['music','dance']
    console.log(dog1.say === dog2.say); // false
    console.log(dog1 instanceof Animal); // false

在子类函数中复制了父类函数,并没有实现子类和父类的继承 所以dog1 instanceof Animal的值为false, 这种方法可以解决原型链继承带来的引用类型属性共享的问题,但是也正因为解决了引用类型属性共享,导致父类构造函数中的方法也不共享了,也就是说没创建一个实例,就会往这个实例身上添加一个方法,尽管每个实例身上的方法作用都是完全一样的。

看到上面两种各带缺点的继承方法,大家可能会想到,将两者方法组合在一起,不就可以解决两者的缺点了吗? 没错, 将两者结合,就叫做组合继承,确实可以很好的实现继承,但是每次创建一个实例都需要调用两次父类构造函数,是比较消耗内存的。

还有一些其它的继承方法,也都各有优缺点,不再介绍。 下面直接讲讲最常用的一些实现继承的方法吧

圣杯模式

// 雅虎公司提出的写法,利用了闭包
var inherit = (function () {
  var Temp = function () {};
  return function (father, son) {
    Temp.prototype = father.prototype;    
    son.prototype = new Temp();
    son.prototype.constructor = son;
    son.prototype.uber = father.prototype;
  };
})();

可以使用es5提供Object.create() 来实现继承。

   function inheritES5(father, son) {
       son.prototype = Object.create(father);
       son.prototype.constructor = son;
       son.prototype.uber = father.prototype;
   }

还可以使用es6新增的Object api -- Object.setPrototypeOf()

    function inheritES6(father, son) {
        Object.setPrototypeOf(son.prototype, father.prototype)
    }

最后,就是利用es6提出extends配合super实现继承

    class Animal {
      constructor(type, name,age) {
        if(new.target === Animal) { // 一般父类不允许直接创建实例
          throw new TypeError('不能直接创建Animal的对象,应该通过子类创建')
        }
        this.type = type;
        this.name = name;
        this.age = age;
      }
      print() {
        console.log(`【种类】: ${this.type}`);
        console.log(`【名字】:${this.name}`);
        console.log(`【年龄】:${this.age}`);

      }
    }

    class Dog extends Animal {
      constructor(name, age) {
        super('犬类', name, age); // 调用父类的构造函数
        this.love = '吃';
      }
      print(){ // 覆盖父类的同名方法
        super.print(); // 先调用父类的方法
        console.log(`【爱好】:${this.love}`); // 执行自己特有的操作
      }
    }

    const d = new Dog('小黑', 3);
    d.print();