JavaScript实现继承的几种方式

230 阅读6分钟

JavaScript实现继承的几种方式

JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一

首先定义一个父类

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

原型链继承

核心: 将父类的实例作为子类的原型

function Dog(name) {
  // 属性
  this.name = name || 'Dog';
}
Dog.prototype = new Person();
var dog = new Dog();
console.log(dog.name); // Dog
console.log(dog.eat('bone'));//Dog正在吃:bone
console.log(dog.sleep());//Dog正在睡觉
console.log(dog instanceof Animal); //true 
console.log(dog instanceof Dog); //true

优点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原型属性,子类都能访问到
  3. 简单,易于实现 缺点:
  4. 创建子类实例无法向父类构造函数传参
  5. 继承单一, 无法实现多继承
  6. 所有子类的实例都会共享父类实例的属性。(原型对象的所有属性被所有实例共享,当其中一个实例修改原型属性时, 其他实例的原型属性也会被改变)。

构造函数继承

核心:使用.call()和.apply()将父类的构造函数用来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name, age){
  Animal.call(this, name); //复制父类实例属性给子类
  this.age = age || 5;
}

var cat = new Cat("Tom", 18);
console.log(cat.name); // Tom
console.log(cat.age); // 18
console.log(cat.sleep()); // Tom正在睡觉
console.log(cat.eat("fish")); // Uncaught TypeError: cat.eat is not a function
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

优点:

  1. 只继承了父类构造函数的属性,方法。没有继承父类原型上的属性和方法。
  2. 解决了原型链继承的缺点
  3. 可以实现多继承(也就是可以继承多个构造函数属性,在子类构造函数中call多个构造函数)
  4. 在子实例对象中可以向父实例对象传参 缺点:
  5. 实例并不是父类的实例,只是子类的实例。
  6. 只能继承父类的实例属性和方法,不能继承原型属性/方法。
  7. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

组合继承(原型链继承+构造函数继承) (常用)

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Rabbit(name, age) {
  Animal.call(this, name); // 构造函数继承, 第二次调用
  this.age = age || 5;
}
Rabbit.prototype = new Animal(); //原型继承 第一次调用
Cat.prototype.constructor = Cat; //重点 组合继承也是需要修复构造函数指向的。


var rabbit = new Rabbit("jine");
console.log(rabbit.name); // jine
console.log(rabbit.age); // 5
console.log(rabbit.sleep()); // jine正在睡觉
console.log(rabbit.eat("carrot")); // jine正在吃:carrot
console.log(rabbit instanceof Animal); // true
console.log(rabbit instanceof Rabbit); // false

优点:

  1. 弥补了构造函数的缺陷。可以继承实例属性/方法,也可以继承原型属性/方法
  2. 可传参
  3. 函数可复用
  4. 每个子类实例引入的构造函数属性是私有的。不存在引用属性共享问题
  5. 既是子类的实例,也是父类的实例 缺点:
  6. 调用了两次父类构造函数,生成了两份实例(损耗内存), 子类的构造函数会代替原型上的父类构造函数。

原型式继承

核心:用一个函数包装一个对象, 然后返回这个函数的调用。这个函数变成一个可以随时添加属性和方法的实例或者对象。Object.create()就是这个原理。 实质上,CreateObject() 对传入其中的对象执行了一次浅复制

function CreateObject(obj) {
    var Fun = function() {};
    Fun.prototype = obj; // obj.__proto__===Object.prototype true; Fun.prototype.constructor === Object true
    return new Fun();
}

var person = {
    name: "xiaoming",
    friend: ["lily", "xiaohong"]
}
var person1 = CreateObject(person);
var person2 = CreateObject(person);
//person1.name改变后,person2.name并未改变, 是因为person.name这个属性是基础类型,不是引用类型。
person1.name = "xiaohe"; 
console.log(person2.name); //"xiaoming"
console.log(person.name); //"xiaoming"
//由于person.friend是一个引用类型,person1.friend修改的时候,会同时改变当前引用类型地址存放的值,导致了person和person2的值改变。
person1.friend.push("xiaozhang"); 
console.log(person2.friend); // ["lily", "xiaohong", "xiaozhang"]
console.log(person.friend);// ["lily", "xiaohong", "xiaozhang"]
person1.friend = ["xiaowang"]; //将person1的friend属性指向另一个引用地址。
console.log(person2.friend); // ["lily", "xiaohong", "xiaozhang"]
console.log(person.friend);// ["lily", "xiaohong", "xiaozhang"]
//person1.friend引用指向的内存地址和person,person2的不同,所以person2和person的friend属性不会改变。
person1.friend.push("xiaoli");
console.log(person1.friend); // ["xiaowang", "xiaoli"]
console.log(person2.friend); // ["lily", "xiaohong", "xiaozhang"]
console.log(person.friend);// ["lily", "xiaohong", "xiaozhang"]

优点: 类型与一个对象的复制,用函数包装了一层而已

缺点:

  1. 包含引用类型值的属性始终都会共享相应的值
  2. 无法实现复用。(新实例属性都是后面添加的)

寄生继承

核心:就是给原型式继承外面套了一个壳子, 可以在这个壳子上添加属性和方法

//这个构造函数与Object.create()的效果是一样的
function CreateObject(obj) {
    var Fun = function() {};
    Fun.prototype = obj; 
    return new Fun();
}

var person = {
    name: "xiaoming",
    friend: ["lily", "xiaohong"]
}

function CreatChild(obj) {
    //创建对象,或者用var newObj = Object.create(obj);
    var newObj = CreateObject(obj); 
    newObj.sayName = function() { //增强对象
        console.log(this.name);
    }
    return newObj;
}

var child1 = CreatChild(obj);
child1.sayName(); // xiaoming
child1.name="xiaohua";
child1.sayName(); // xiaohua

优点:没有创建自定义类型, 因为只是套了个壳子返回对象。可以增强对象。 缺点: 没有用到原型, 无法复用属性及方法。

寄生组合继承

核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

// 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型
function Person(name){
     this.name = name;
     this.colors = ["red","green","blue"];
}
Person.prototype.sayName = function(){
      console.log(this.name);
};
function Child(name,age){
     Person.call(this,name); //构造函数继承
     this.age = age;
}
(function(){
    // 创建超类型原型的一个副本
    var anotherPrototype = Object.create(Person.prototype);
    // 重设因重写原型而失去的默认的 constructor 属性
    anotherPrototype.constructor = Child;
    // 将新创建的对象赋值给子类型的原型
    Child.prototype = anotherPrototype;
})();

Child.prototype.sayAge = function(){
     console.log(this.age);
};
var child1 = new Child("luochen",22);
child1.colors.push("purple");
console.log(child1.colors);      // "red,green,blue,purple"
child1.sayName();
child1.sayAge();

var child2 = new SubType("tom",34);
console.log(child2.colors);      // "red,green,blue"
child2.sayName();
child2.sayAge();

重点: 修复了组合继承的问题。

实例继承

核心:为父类实例添加新特性,作为子类实例返回

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

var cat = new Cat();
console.log(cat.name);  // Tom
console.log(cat.sleep()); // Tom正在睡觉
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

缺点:

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承

拷贝继承

核心:在子类构造函数中拷贝父类的属性到子类构造函数的原型对象上。

function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  this.name = name || 'Tom';
}

var cat = new Cat();
console.log(cat.name); // "Tom"
console.log(cat.sleep()); // Tom正在睡觉
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

优点:支持多继承

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性)
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

ES6 Class语法糖继承

class Person {
    constructor(name) {
        this.name = name||"default name";
    }
    reName(name) {
        this.name = name;
    }
}
class Child extends Person {
    constructor(name) {
        super(name);
    }
}
var child1 = new Child();
console.log(child1.name); // default name
child1.reName("Tom");
console.log(child1.name); // Tom