JavaScript的7种继承方式

275 阅读10分钟

方式一:原型链的继承

// 父类
function Person() {}

// 子类
function Student(){}

// 继承
Student.prototype = new Person()

我们只要把子类的prototype设置为父类的实例,就完成了继承,怎么样? 是不是超级简单? 有没有比Java,C#的清晰?
事实上,以上就是js里面的原型链继承

当然,通过以上代码,我们的Student只是继承了一个空壳的Person,这样视乎是毫无意义的,我们使用继承的目的,
就是要通过继承获取父类的内容,那我们先给父类加上一点点简单的内容(新增的地方标记 '// 新增的代码'):

// 父类
function Person(name,age) {
  this.name = name || 'unknow'     // 新增的代码
  this.age = age || 0              // 新增的代码
}

// 子类
function Student(name){
  this.name = name                 // 新增的代码
  this.score = 80                  // 新增的代码
}

// 继承
Student.prototype = new Person()

使用

var stu = new Student('lucy')

console.log(stu.name)  // lucy    --子类覆盖父类的属性
console.log(stu.age)   // 0       --父类的属性
console.log(stu.score) // 80      --子类自己的属性

这里为了降低复杂度,我们只演示了普通属性的继承,没有演示方法的继承,事实上,方法的继承也很简单,
我们再来稍微修改一下代码,基于上面的代码,给父类和子类分别加一个方法

  // 父类
  function Person(name, age) {
    this.name = name || "unknow";
    this.age = age || 0;
  }

  // 为父类新曾一个方法
  Person.prototype.say = function () {
    console.log("I am a person");
  };

  // 子类
  function Student(name) {
    this.name = name;
    this.score = 80;
  }

  // 使用父类的实例对象来重写子类的原型对象
  // 继承 注意,继承必须要写在子类方法定义的前面
  Student.prototype = new Person("zgc", 18);
  // 修复子类原型对象上constructor 的指向
  Student.prototype.constructor = Student;

  // 为子类新增一个方法(在继承之后,否则会被重写的原型覆盖)
  Student.prototype.study = function () {
    console.log("I am studing");
  };

使用

  var stu = new Student("lucy");

  console.log(stu.name); // lucy               --子类覆盖父类的属性
  console.log(stu.age); // 18                 --父类的属性
  console.log(stu.score); // 80                 --子类自己的属性
  stu.say(); // I am a person      --继承自父类的方法
  stu.study(); // I am studing       --子类自己的方法

这样,看起来我们好像已经完成了一个完整的继承了,这个就是原型链继承,怎么样,是不是很好理解?
但是,原型链继承有一个缺点,来自原型对象引用属性会被所有实例共享,请看以下代码

   // 父类
    function Person(name, age) {
        this.name = name || 'unknow'
        this.age = age || 0
        this.hobbies = ['music', 'reading']
    }
    // 子类
    function Student(name) {
        this.name = name
        this.score = 80
    }
    // 继承
    Student.prototype = new Person()

    var stu1 = new Student('zgc')
   //修改stu1的name属性,stu2的name不会改变
    var stu2 = new Student('wf')

    stu1.hobbies.push('basketball')
    console.log(stu1.hobbies)   // music,reading,basketball
    console.log(stu2.hobbies)   // music,reading,basketball
    console.log(stu1.name === stu2.name); //true or false 不传参相等,传参根据参数判断

我们可以看到,当我们改变stu1的引用类型的属性时,stu2对应的属性,也会跟着更改,这就是原型链继承缺点 --来自原型链引用属性会被所有实例共享

特点

  • 共享了父类构造函数的方法和属性
  • 简单,易于实现

缺点

  • 继承的属性会被多个实例共享, 如果某个属性是引用类型, 那么这个属性在多个实例中互相影响
  • 创建子类实例时,无法向父类构造函数传参
  • 要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中
  • 不可以实现多继承

例题:

   function Parent(name) {
        this.name = name || '⽗亲'; // 实例基本属性 (该属性,强调私有,不共享)
        this.arr = [1]; // (该属性,强调私有)
    }
    Parent.prototype.say = function () { // -- 将需要复⽤、共享的⽅法定义在⽗类原型上
        console.log('hello')
    }
    function Child(like) {
        this.like = like;
    }
    Child.prototype = new Parent() // 核⼼,但此时Child.prototype.constructor==Parent
    Child.prototype.constructor = Child // 修正constructor指向
    let boy1 = new Child()
    let boy2 = new Child()
    // 优点:共享了⽗类构造函数的say⽅法
    console.log(boy1.say(), boy2.say(), boy1.say === boy2.say); // hello , hello , true
    // 缺点1:不能向⽗类构造函数传参
    console.log(boy1.name, boy2.name, boy1.name === boy2.name); // ⽗亲,⽗亲,true
    // 缺点2: ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性
    boy1.arr.push(2);
    // 修改了boy1的arr属性,boy2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了⽗类构造函数的实例属性arr;
    所以只要修改了boy1.arr,boy2.arr的属性也会变化。
    console.log(boy2.arr); // [1,2]
    // 注意1:修改boy1的name属性,是不会影响到boy2.name。因为设置boy1.name相当于在⼦类实例新增了name属性。
    // 注意2:
    console.log(boy1.constructor); // Parent 你会发现实例的构造函数居然是Parent。
    // ⽽实际上,我们希望⼦类实例的构造函数是Child, 所以要记得修复构造函数指向。
    // 修复方法如下:Child.prototype.constructor = Child;

方式二: 借用构造函数继承

这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数

  function Person(name, age) {
    this.name = name;
    this.age = age;
    this.setName = function () {};
  }

  Person.prototype.setAge = function () {};

  function Student(name, age, price) {
    Person.call(this, name, age);
    /* 等于下面两行代码
        this.name = name
        this.age = age
    */
    this.price = price;
  }
  var s1 = new Student("Tom", 20, 15000);

image.png 这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。

console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function

特点

  • 解决了原型链继承中子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承父类原型属性和方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

例题:

 function Parent(name) {
            this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
            this.arr = [1]; // (该属性,强调私有)
            this.say = function () { // 实例引⽤属性 (该属性,强调复⽤,需要共享)
                console.log('hello')
            }
        }
        function Child(name, like) {
            Parent.call(this, name); // 核⼼ 拷⻉了⽗类的实例属性和⽅法
            this.like = like;
        }
        let boy1 = new Child('⼩红', 'apple');
        let boy2 = new Child('⼩明', 'orange ');
        // 优点1:可向⽗类构造函数传参
        console.log(boy1.name, boy2.name); // ⼩红, ⼩明
        // 优点2:不共享⽗类构造函数的引⽤属性
        boy1.arr.push(2);
        console.log(boy1.arr, boy2.arr);// [1,2] [1]

        // 缺点1:⽅法不能复⽤
        console.log(boy1.say === boy2.say) // false (说明,boy1和boy2的say⽅法是独⽴,不是共享的)
        // 缺点2:不能继承⽗类原型上的⽅法
        Parent.prototype.walk = function () { // 在⽗类的原型对象上定义⼀个walk⽅法。
            console.log('我会⾛路')
        }
        boy1.walk; // undefined (说明实例,不能获得⽗类原型上的⽅法)

方式三: 原型链+借用构造函数的组合继承

这种方式关键在于:

  • 普通属性 使用 借用构造函数继承(继承的属性和方法都在实例自身),
  • 方法函数 使用 原型链继承(继承的属性和方法都在实例的原型里面),
  • 通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.setAge = function () {};
  }

  Person.prototype.sayBy = function () {
    console.log("111");
  };

  function Student(name, age, price) {
    Person.call(this, name, age); //核心代码
    this.price = price;
    this.setScore = function () {};
  }

  Student.prototype = new Person(); // 核心代码
  Student.prototype.constructor = Student; //组合继承也是需要修复构造函数指向的

  Student.prototype.sayHello = function () {};

  var s1 = new Student("Tom", 20, 15000);
  var s2 = new Student("Jack", 22, 14000);

  console.log(s1);
  console.log(s1.constructor); //Student

image.png

这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点

  • 就是无论在什么情况下,都会调用两次父类构造函数:
    • 一次是在创建子类型原型的时候调用,
    • 一次是在子类型构造函数的内部调用,

优点

  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用

缺点

  • 调用了两次父类构造函数,子类型实例上会有两份父类型的属性和方法
  • 一份在子类实例自身, 一份在子类实例的原型对象中

例题:

  function Parent(name) {
            this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
            this.arr = [1]; // (该属性,强调私有)
        }
        Parent.prototype.say = function () { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
            console.log('hello')
        }
        function Child(name, like) {
            Parent.call(this, name) // 核⼼ 第⼆次
            this.like = like;
        }
        Child.prototype = new Parent() // 核⼼ 第⼀次
        Child.prototype.constructor = Child // 修正constructor指向
        let boy1 = new Child('⼩红', 'apple')
        let boy2 = new Child('⼩明', 'orange')
        // 优点1:可以向⽗类构造函数传参数
        console.log(boy1.name, boy1.like); // ⼩红,apple
        // 优点2:可复⽤⽗类原型上的⽅法
        console.log(boy1.say === boy2.say) // true
        // 优点3:不共享⽗类的引⽤属性,如arr属性
        boy1.arr.push(2)
        console.log(boy1.arr, boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。
      // 缺点1:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性

方式四: 原型式继承

核心:原型式继承的Object.create方法本质上是对参数对象的一个浅复制。

 var person = {
    name: "Nicholas",
    age: 18,
    friends: ["Shelby", "Court", "Van"],
  };
  person.__proto__.sayHello = function () {
    console.log("Hello");
  };

  var anotherPerson = Object.create(person);

  anotherPerson.name = "Greg";
  anotherPerson.age = 22;
  anotherPerson.friends.push("Rob");

  var yetAnotherPerson = Object.create(person);
  yetAnotherPerson.name = "Linda";
  yetAnotherPerson.friends.push("Barbie");

  console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
  console.log(person); // {name: 'Nicholas', age: 18, friends: Array(5)}
  anotherPerson.sayHello(); // Hello
  console.log(yetAnotherPerson);

优点:

  • 子类可以继承父类和父类原型上的属性和方法

缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

拓展: 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__


  var obj = {
    name: "zgc",
    age: 18,
  };

  // 1.
  var info1 = {};
  info1.__proto__ = obj;

  // 2.
  function createObject(o) {
    const F = function () {};
    F.prototype = o;
    return new F();
  }
  var info2 = createObject(obj);

  // 3.
  Object.create(obj);

方式五: 寄生式继承

核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

  function createAnother(original, name) {
    var clone = Object.create(original); //通过调用函数创建一个新对象
    clone.name = name
    clone.sayHi = function () {
      //以某种方式来增强这个对象
      alert("hi");
    };
    return clone; //返回这个对象
  }

  var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"],
  };

  var anotherPerson = createAnother(person, 'zgc);
  anotherPerson.sayHi(); //"hi"

函数的主要作用是为构造函数新增属性和方法,以增强函数

缺点(同原型式继承):

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

方式六: 寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承

 function Person(name, age) {
    this.name = name;
    this.age = age;
    this.say = function () {};
  }

  Person.prototype.setAge = function () {
    console.log("111");
  };
  // Person.prototype.msg = "Hello";

  function Student(name, age, price) {
    Person.call(this, name, age);
    // 子类继承父类自身的属性和方法
    this.price = price;
    this.setScore = function () {};
  }

  function inheritPrototype(subType, superType) {
    subType.prototype = Object.create(superType.prototype); //核心代码
    subType.prototype.constructor = subType; //核心代码
  }
  // Object.create(obj); 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
  // subType.prototype.constructor = subType; 将子类原型对象的构造器指向自身构造函数

  inheritPrototype(Student, Person);

  var s1 = new Student("Tom", 20, 15000);
  console.log(s1 instanceof Student, s1 instanceof Person); // true true
  console.log(s1.constructor); //Student
  console.log(s1);

  // 完美写法: inheritPrototype函数封装
  // function createObject(o) {
  //   const F = function () {};
  //   F.prototype = o;
  //   return new F();
  // }

  // function inheritPrototype(subType, superType) {
  //   subType.prototype = createObject(superType.prototype); //核心代码
  //   Object.defineProperty(subType.prototype, "constructor", {
  //     value: subType,
  //     enumerable: false,
  //     writable: true,
  //     configurable: true,
  //   });
  // }

同样的,Student继承了所有的Person原型对象的属性和方法。目前来说,最完美的继承方法!

image.png

方式七:ES6中class 的继承

ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的

       class Person {
            //调用类的构造方法
            constructor(name, age) {
                this.name = name
                this.age = age
            }
            //定义一般的方法
            showName() {
                console.log("调用父类的方法")
                console.log(this.name, this.age);
            }
        }
        let p1 = new  Person('kobe', 39)
        console.log(p1)
        //定义一个子类
        class Student extends Person {
            constructor(name, age, salary) {
                super(name, age)//通过super调用父类的构造方法
                this.salary = salary
            }
            showName() {//在子类自身定义方法
                console.log("调用子类的方法")
                console.log(this.name, this.age, this.salary);
            }
        }
        let s1 = new Student('wade', 38, 1000000000)
        console.log(s1)
        s1.showName()

优点

  • 语法简单易懂,操作更方便

缺点

  • 并不是所有的浏览器都支持class关键字