Skip to content

原型链与继承

一、基础概念

显式原型

每个函数都有一个属性pototype,被称为显式原型

构造函数的显式原型默认有一个 constructor 属性,指向构造函数本身,该属性可以被实例继承,从而查询到构造函数

js
function Person() {}
let p1 = new Person();
console.log(Person.prototype.constructor === Person); // true
console.log(p1.constructor === Person); // true

隐式原型

每个对象都有一个私有属性 __proto__ ,被称为隐式原型

普通情况下实例对象的隐式原型指向构造函数的显式原型,如下所示

js
function Person() {}
let p1 = new Person();
console.log(p1.__proto__ === Person.prototype); // true

通过 Object.create 创建的对象是例外,该方法相当于手动修改原型指向

js
let a1 = { a: 1 };
let a2 = Object.create(a1);
console.log(a2.constructor === Object); // true
console.log(a2.__proto__ === a2.constructor.prototype); // false
console.log(a2.__proto__ === a1); // true

原型链

先展示一段代码,如下所示,我们需要查找到 toString 这个属性

js
function Person(name) {
    this.name = name;
}
let tom = new Person('tom');
console.log(tom.toString); // [Function: toString]

具体查找属性 toString 的过程如下所示

  • 首先查找自身, tom 没有 toString 这个属性
  • 接着往上查原型,Person.prototype 也没有找到
  • 依次再往上查找,Object.prototype 中找到了需要的属性,于是立即返回对应的值
  • 如果Object.prototype还没找到属性就会返回 undifined,因为Object.prototype.__proto__ === null,没有上一级

这种检索的轨迹像一条长链,又因prototype原型充当链接的作用,于是把这种实例与原型的链条称作 原型链null是原型链的顶点


关系断言

  • instanceof(推荐):实例与原型的关系,判断后者是否在前者的原型链中

    js
    function Person() {}
    let tom = new Person();
    console.log(tom instanceof Person); // true
    console.log(tom instanceof Object); // true
  • isPrototypeOf:原型与实例的关系,判断前者是否出现在后者的原型链中

    js
    console.log(Person.prototype.isPrototypeOf(tom)); // true
    console.log(Object.prototype.isPrototypeOf(tom)); // true

    安全建议:不应这样直接调用原型中的方法,应当只使用实例方法

二、继承

原型链继承

prototype__proto__ 都是可以修改的,通过修改 prototype 可以形成继承关系

js
function Father(like) {
    this.like = like;
}
Father.prototype.height = 175;

function Son(name) {
    this.name = name;
}
Son.prototype = new Father(['Apple', 'Pear']);

let tom = new Son('tom');
console.log(tom.like); // [ 'Apple', 'Pear' ]
console.log(tom.height); // 175

特点:

  • 完整继承了父类整条原型链中的属性

缺点:

  • 创建实例时无法向父类构造函数传参

  • 当原型链中包含引用类型值的原型时,会被所有实例共享,做修改会影响到所有实例,如下所示

    js
    let jak = new Son('jak');
    tom.like.push('Peach');
    console.log(jak.like); // [ 'Apple', 'Pear', 'Peach' ]

借用构造函数继承

使用 call 或者 apply 这类可以更改 this 指向的方法来继承属性

js
function Father(like) {
    this.like = like;
}
Father.prototype.height = 175;

function Son(name) {
    Father.call(this, ['apple', 'Pear']);
    this.name = name;
}

let tom = new Son('tom');
console.log(tom); // Son { like: [ 'apple', 'Pear' ], name: 'tom' }
console.log(tom.height); // undefined

let jak = new Son('jak');
tom.like.push('Peach');
console.log(jak.like); // [ 'apple', 'Pear' ]

特点

  • 实例继承的属性是私有的,不会影响其他实例

  • 可多次使用 call 方法,从而继承多个构造函数的属性

  • 子实例中也可给父实例传参

缺点

  • 只能继承父构造函数本身的属性,无法向上继承原型的属性
  • 调用多次父类构造函数(耗内存

组合继承

组合继承融合了原型链继承和借用构造函数继承的特点,是 JavaScript 中最常用的继承模式

javascript
function Father(like = []) {
    this.like = like;
}
Father.prototype.height = 175;

function Son(name) {
    Father.call(this, ['apple', 'Pear']);
    this.name = name;
}
Son.prototype = new Father();

let tom = new Son('tom');
console.log(tom); // Father { like: [ 'apple', 'Pear' ], name: 'tom' }
console.log(tom.height); // 175

let jak = new Son('jak');
tom.like.push('Peach');
console.log(jak.like); // [ 'apple', 'Pear' ]

上述例子中,共调用了两次父类构造函数,一次是继承构造函数本身属性,一次继承父类原型的属性。

特点

  • 完整继承了父类整条原型链中的属性
  • 实例引入的构造函数属性是私有的,不会影响其他实例

缺点

  • 调用多次父类构造函数(耗内存
  • 父级构造函数的属性被继承了两次,有冗余属性,例如上例中,实例同时拥有私有属性 like 和原型链属性 like,只是由于访问时默认是私有属性,所以这个缺陷没有显现

为了解决组合继承的缺陷,出现了寄生组合式继承,为了便于理解,先将其中部分逻辑拆解为原型式继承寄生式继承

原型式继承

首先定义一个函数,传入原型对象,返回一个空的实例对象

js
function createInstance(obj) {
    function TempConstructor() {} // 定义临时的构造函数
    TempConstructor.prototype = obj; // 将传入的对象座位构造函数的原型
    return new TempConstructor(); // 返回实例
}

该方法内的逻辑等价于 return Object.create(obj)

该继承模式与原型链继承的效果类似,如下所示

javascript
function Father(like = []) {
    this.like = like;
}
Father.prototype.height = 175;
let fatherInstance = new Father(['apple', 'Pear']);

let tom = createInstance(fatherInstance);
console.log(tom.like); // [ 'Apple', 'Pear' ]
console.log(tom.height); // 175

let jak = createInstance(fatherInstance);
tom.like.push('Peach');
console.log(jak.like); // [ 'Apple', 'Pear', 'Peach' ]

特点与缺点:

  • 与原型链继承的效果类似

寄生式继承

原型式继承创建的实例本身没有属性,每个实例都需要重复地手动添加,因此可以再包裹一层函数,在创建实例后自动添加属性

js
function createInstance(obj) {
    return Object.create(obj);
}

function createSon(obj) {
    let clone = createInstance(obj);
    clone.sex = 'man';
    return clone;
}

继承效果与优缺点同上

寄生组合式继承

javascript
function Father(like = []) {
    this.like = like;
}
Father.prototype.height = 175;

function Son(name) {
    Father.call(this); // 借用构造函数继承父类的构造函数属性
    this.name = name;
}

// 原型式继承与原型链继承,Son 继承了 Father 的原型上的属性
Son.prototype = Object.create(Father.prototype);
Son.prototype.constructor = Son; // 修复 Son 的构造函数指向

let tom = new Son('tom');
console.log(tom); // Son { like: [], name: 'tom' }
console.log(tom.height); // 175

let jak = new Son('jak');
tom.like.push('Peach');
console.log(tom.like); // [ 'Peach' ]
console.log(jak.like); // []

寄生:

  • 在函数内返回对象然后调用

组合:

  • 函数的原型等于另一个实例
  • 在函数中用 apply 或者 call 引入另一个构造函数,可传参。

这是最成熟的方法,也是现在库实现的方法。在Vue源码在extend方法里就用到了这种继承方式。各种继承方式可以手动修改,但修改后就是另一种继承方式了。