原型链与继承
一、基础概念
显式原型
每个函数都有一个属性pototype
,被称为显式原型。
构造函数的显式原型默认有一个 constructor
属性,指向构造函数本身,该属性可以被实例继承,从而查询到构造函数
function Person() {}
let p1 = new Person();
console.log(Person.prototype.constructor === Person); // true
console.log(p1.constructor === Person); // true
隐式原型
每个对象都有一个私有属性 __proto__
,被称为隐式原型。
普通情况下实例对象的隐式原型指向构造函数的显式原型,如下所示
function Person() {}
let p1 = new Person();
console.log(p1.__proto__ === Person.prototype); // true
通过 Object.create
创建的对象是例外,该方法相当于手动修改原型指向
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
这个属性
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
(推荐):实例与原型的关系,判断后者是否在前者的原型链中jsfunction Person() {} let tom = new Person(); console.log(tom instanceof Person); // true console.log(tom instanceof Object); // true
isPrototypeOf
:原型与实例的关系,判断前者是否出现在后者的原型链中jsconsole.log(Person.prototype.isPrototypeOf(tom)); // true console.log(Object.prototype.isPrototypeOf(tom)); // true
安全建议:不应这样直接调用原型中的方法,应当只使用实例方法
二、继承
原型链继承
prototype
和 __proto__
都是可以修改的,通过修改 prototype
可以形成继承关系
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
特点:
- 完整继承了父类整条原型链中的属性
缺点:
创建实例时无法向父类构造函数传参
当原型链中包含引用类型值的原型时,会被所有实例共享,做修改会影响到所有实例,如下所示
jslet jak = new Son('jak'); tom.like.push('Peach'); console.log(jak.like); // [ 'Apple', 'Pear', 'Peach' ]
借用构造函数继承
使用 call
或者 apply
这类可以更改 this
指向的方法来继承属性
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
中最常用的继承模式
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
,只是由于访问时默认是私有属性,所以这个缺陷没有显现
为了解决组合继承的缺陷,出现了寄生组合式继承,为了便于理解,先将其中部分逻辑拆解为原型式继承与寄生式继承
原型式继承
首先定义一个函数,传入原型对象,返回一个空的实例对象
function createInstance(obj) {
function TempConstructor() {} // 定义临时的构造函数
TempConstructor.prototype = obj; // 将传入的对象座位构造函数的原型
return new TempConstructor(); // 返回实例
}
该方法内的逻辑等价于
return Object.create(obj)
该继承模式与原型链继承的效果类似,如下所示
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' ]
特点与缺点:
- 与原型链继承的效果类似
寄生式继承
原型式继承创建的实例本身没有属性,每个实例都需要重复地手动添加,因此可以再包裹一层函数,在创建实例后自动添加属性
function createInstance(obj) {
return Object.create(obj);
}
function createSon(obj) {
let clone = createInstance(obj);
clone.sex = 'man';
return clone;
}
继承效果与优缺点同上
寄生组合式继承
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
方法里就用到了这种继承方式。各种继承方式可以手动修改,但修改后就是另一种继承方式了。