Jex
03-JS 继承

03-JS 继承

梳理一下 JS 的几种实现继承的方式

1. 原型链继承

我们从上一章知道,在 new 运算符操作的时候,会将实例原型(Child.prototype)指向构造函数的原型(Parent.prototype),这样 child1 就可以访问到构造函数原型中的属性,实现了继承。

缺点:

  1. 多个实例对引用类型的操作会被篡改。这个理解一下原型链就明白了,就不费篇幅写例子了。
  2. 在创建 Child 的实例时,不能向Parent传参。
function Parent () {
    this.name = 'name';
}

Parent.prototype.getName = function () {
    console.log(this.name);
}

function Child () {}

Child.prototype = new Parent();

const child1 = new Child();

console.log(child1.getName()) // name

我们可以打印出来验证一下:
image.png

2. 借用构造函数

使用父类的构造函数来实现继承,等同于复制父类的实例给子类(不使用原型)。但在创建子类实例时,会调用 Perent 构造函数,所以 Child 的每个实例都会将 Parent 中的属性复制一份

优点:

  1. 避免了引用类型的属性被所有实例共享
  2. 可以在 Child 中向 Parent 传参

缺点:

  1. 方法都在构造函数中定义,每次创建实例都会创建一遍方法,影响性能
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
function Parent (name) {
  this.name = name;
}

function Child (name) {
  // 继承于 Parent
  Parent.call(this, name);
}

var child1 = new Child("kevin");
console.log(child1.name); // kevin

var child2 = new Child("yayu");
console.log(child2.name); // yayu

3. 组合继承

原型链继承和经典继承双剑合璧,用原型链实现对原型属性和方法的继承,用借用构造函数的方法来实现实例属性的继承。

优点:

  1. 融合原型链继承和构造函数的优点。

缺点:

  1. 在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
  // 继承属性,第二次调用 Parent
  Parent.call(this, name);
  this.age = age;
}

// 继承方法,构建原型链,第一次调用 Parent
Child.prototype = new Parent();
// 重写 Child.prototype 的 constructor 属性,指向自己的构造函数 Child
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');
console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

同样,我们可以把 Child 的实例打印出来看看:
image.png

4. 原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

缺点:

  1. 包含引用类型的属性值始终都会共享相应的值,跟原型链继承一样。
  2. 无法传递参数
function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

Object.create() 对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。

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

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

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

console.log(person.friends);   // "Shelby,Court,Van,Rob,Barbie"

5. 寄生式继承

在原型式继承的基础上,增强对象,返回构造函数。

缺点(同原型式继承)

function createObj (o) {
  var clone = Object.create(o);
  // 函数的主要作用是为构造函数新增属性和方法,以增强函数
  clone.sayName = function () {
    console.log('hi');
  }
  return clone;
}

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var p1 = createObj(person);
p1.sayHi(); // "hi"

6. 寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承,这种方式的高效率体现它只调用了一次 SuperType 构造函数,并且因此避免了在 SuperType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

function inheritPrototype(subType, superType){
  // 创建对象,创建父类原型的一个副本
  var prototype = Object.create(superType.prototype); 
  // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  prototype.constructor = subType;
  // 指定对象,将新创建的对象赋值给子类的原型
  subType.prototype = prototype;                      
}

// 父类初始化实例属性和原型属性
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  console.log(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function(){
  console.log(this.age);
}

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]