JS继承详细解(带图说话)

250 阅读5分钟

回顾

上篇文章我们从原型对象、构造函数、实例的三个属性及三者之间的关系讲述了原型链,如果还不是很清楚的同学可以先看一下上篇文章不要再说你不懂原型链了---从原型、构造函数、实例开始理解原型链

那么原型链是用来做什么的呢?答案就是用来继承的。你可以试一下在控制台输出下面这段代码

image.png 我们只是输入了一个简单的对象,在他的[[protoType]]属性上有这么多的方法,根据原型图,我们可以知道这些属性是从Object原型对象上继承过来的。

原型图

就是这条链路 o1 -> o1.__proto__(等价于Object.prototype) 继承过来的。

继承

什么是继承?每个对象都有一个__proto__的属性,指向它构造函数的原型对象,该原型对象也会有自己的原型对象,从实例出发,连接每个原型对象之间的线条就是继承的链路(这句话可以从下面的图解能很好的理解)。

new 过程图解

image.png

new过程的实现思路再来一遍 new及object.create的手写过程

new Foo

  • 创建一个空对象 f = Object.create()
  • 将空对象的原型指向构造函数的原型对象(这一步用来继承构造函数的原型对象) f.__proto__ = Foo.prototype
  • 将this指向于这个空对象并执行 Foo.call(obj)
  • 返回obj

在创建空对象的时候,将空对象f.prototype 重新指向一个新的原型(看绿色线),这时候会导致F.prototype.constructor 指向丢失,这里需要重新指定constructor,所以有F.prototype.constructor = F

原型链继承

原型链继承的基本思想就是通过原型继承多个引用类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有 一个属性指回构造函数,而实例有一个内部指针指向原型。 来上代码

function SuperType() { 
    this.property = true;
}

SuperType.prototype.getSuperValue = function() { 
    return this.property;
};

function SubType() { 
    this.subproperty = false;
}

// 继承SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
    return this.subproperty; 
};

let instance = new SubType(); 
console.log(instance.getSuperValue()); // true

image.png

原型链继承最关键的一步就是 SubType.prototype = new SuperType(); SubType.prototype 替换成SuperType的实例,这意味着 SuperType 实例可以访问的所有属性和方法也会存在于 SubType.prototype。 接着又给SubType.prototype增加一个getSubValue的方法,创建instance实例的时候,会继承SubType.prototype上的方法。

从instance出发,有两条虚线__proto__,这里就是继承的链条,实例上的属性都是从这个链条上继承过来的,每个属性继承过程都是有迹可循的。

缺点

  • 原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。
function SuperType() {
    this.colors = ["red", "blue", "green"];
}
function SubType() {}

// 继承SuperType\
SubType.prototype = new SuperType();

let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black"

let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green,black"
  • 子类型实例化的时候,无法动态的给所有父类型传参

借用构造函数

为了解决原型包含引用值导致的继承问题。基本思路很简单:在子类构造函数中调用父类构造函数。

function SuperType() {
    this.colors = ["red", "blue", "green"];
}

function SubType() {
    SuperType.call(this);
}

let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black"

let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green"

image.png

通过使用 call()(或 apply())方法,SuperType 构造函数在为 SubType 的实例创建的新对象的上下文中执行了。这相当于新的 SubType 对象上运行了 SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的 colors 属性。 缺点 子类也不能访问父类原型上定义的方法,这里只是继承了父类构造函数上的属性

组合继承

基本的思路是使用原型链继承原型上的属性和方法,而通过借用构造函数继承实例属性。

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; 
}

// 继承方法
SubType.prototype = new SuperType();

SubType.prototype.sayAge = function() { console.log(this.age);

};

let instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" instance1.sayName(); // "Nicholas"; instance1.sayAge(); // 29

let instance2 = new SubType("Greg", 27); console.log(instance2.colors); // "red,blue,green" instance2.sayName(); // "Greg"; instance2.sayAge(); // 27

image.png

组合继承 = 原型链继承 + 借用构造函数继承

组合继承可以解决原型链继承及借用构造函数继承的缺点,如果不是追求高性能是满足目前使用。

缺点

细心的同学可以看出,SuperType的构造函数执行了两次,一次是借用构造函数继承时,一次是原型链继承的时候,new SuperType new运算符源码帮我们执行了一次。

原型式继承

原型式继承的本质就是借用一个空的构造函数,继承原型对象,这个构造函数就有一个指定的原型。这里是不是有种熟悉的感觉,没错!就是Object.create的原理,创建一个指定原型的对象。

function create(prototype) {
    // 1、创建一个空函数
    function F() {};
    // 2、指定空函数的原型
    F.prototype = prototype;
    // 3、实例化这个空函数
    return new F()
}

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

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

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

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

image.png

缺点

原型式继承只是继承了原型对象,构造函数上的属性并没有继承过来。

寄生式继承

寄生式继承与原型式继承是几乎一样的,在原型式继承的基础上,增强了原型对象,再将这个原型对象返回出来。

function createAnother(original){
    // 通过调用函数创建一个新对象
    let clone = object(original); 
     clone.sayHi = function() { 
     // 以某种方式增强这个对象
        console.log("hi");
    };
    // 返回这个对象 
    return clone; 
}

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

let anotherPerson = createAnother(person); anotherPerson.sayHi(); // "hi"

寄生组合继承

在组合继承中,我们知道的组合继承的缺点就是会将父类的构造函数执行了两次,通过上面,我们学习了寄生式继承(原型式继承的增强版),同时结合借用构造函数的优点。

function inheritPrototype(subType, superType) {
    let prototype = object(superType.prototype); // 创建对象 
    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);
};

image.png

后续

因为最近比较忙,ES6部分的解析需要等到后面才能补上