要理解原型原型链,继承的问题,首先要知道js里的几个基本的游戏规则。
概念
- 引用类型,都具有对象特性,即可自由扩展属性。
- 引用类型,都有一个隐式原型(
__proto__)属性,属性值是一个普通对象。 - 引用类型,隐式原型 (
__proto__)的属性值指向它的构造函数的显式原型(prototype)的属性值。 - 当你试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型(
__proto__)也就是它的构造函数的显式原型(prototype)中寻找。
这几个规则放到最上面,待会会用到。
原型链继承
原型链继承的重点是让子构造函数的原型等于父构造函数的实例。
function Father(){
this.name = 'Father'
this.height = 175
this.sons = ['J','K','L']
this.sayName = function(){
console.log(this.name)
}
}
Father.prototype.age = 22
function Son(){
this.sex = 'man'
this.name = 'Son'
}
// 重点:让子构造函数的原型等于父构造函数的实例。
let f1 = new Father() // 创建父构造函数的实例
Son.prototype = f1 // 让子构造函数的原型等于父构造函数的实例
let s1 = new Son()
console.log(s1.name) // Son
console.log(s1.age) // 22
s1.sayName() // Son
// 搜索轨迹
// s1 => f1 => Father.prototype…=> Object.prototype
// 缺点 :引用类型值会被所有实例共享
// 如果再创建一个实例,并且修改这个实例继承的引用类型属性
let s2 = new Son()
s2.sons.push('Q')
console.log(s1.sons) // ['J', 'K', 'L', 'Q']
console.log(s2.sons) // ['J', 'K', 'L', 'Q']
特点:
- 可以继承父类构造函数属性,父类原型的属性。 缺点:
- 新实例无法向父类构造函数传参。
- 继承单一。
- 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享。
于是,我们针对原型链继承的缺点进行修改,就有了借用构造函数继承:
借用构造函数继承
function Father(sons){
this.name = 'Father'
this.height = 175
this.sons = ['J','K','L',...sons]
this.sayName = function(){
console.log(this.name)
}
}
Father.prototype.age = 22
function Son(){
Father.call(this,['Q','W']) // 这里借用构造函数 可以借用多个
this.sex = 'man'
this.name = 'Son'
}
let s1 = new Son()
s1.sons.push('E')
console.log('s1',s1) // sons: (6) ['J', 'K', 'L', 'Q', 'W', 'E']
let s2 = new Son()
console.log('s2',s2) // sons: (5) ['J', 'K', 'L', 'Q', 'W']
// 缺点:继承不了父构造函数原型上的属性
console.log(s2.age) // undefined
特点:
- 只继承了父类构造函数的属性,没有继承父类原型的属性。
- 保证了原型链中引用类型值的独立,不再被所有实例共享。
- 可以继承多个构造函数属性(call多个)。
- 在子实例中可向父实例传参。 缺点:
- 只能继承父类构造函数的属性。
- 无法实现构造函数的复用。(每次用每次都要重新调用)。
- 每个新实例都有父类构造函数的副本,臃肿。
借用构造函数继承一举解决了原型链继承的三个问题,但是,又出现了新问题,就是子类继承不到父类原型上的属性,从最后一行s2.age输出为undefined可以看出。
于是,我们针对借用构造函数的缺点1进行优化,就有了组合继承:
组合继承
组合继承是组合了前两个继承为一体,也就是组合了原型链继承和借用构造函数继承。
通过原型链对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
function Father(sons=[]){
this.name = 'Father'
this.height = 175
this.sons = ['J','K','L',...sons]
this.sayName = function(){
console.log(this.name)
}
}
Father.prototype.age = 22
function Son(){
Father.call(this,['Q','W']) // 这里借用构造函数 可以借用多个
this.sex = 'man'
this.name = 'Son'
}
Son.prototype = new Father(); //继承父类方法,第二次调用Father()
console.log(Son.prototype.constructor) // 子类的构造函数会被原型上的那个父类构造函数代替 Father(){...} 这里后面会讲
let s1 = new Son();
s1.sons.push("E");
console.log('s1',s1.sons); // (6) ['J', 'K', 'L', 'Q', 'W', 'E']
let s2 = new Son();
console.log('s2',s2.sons); // (5) ['J', 'K', 'L', 'Q', 'W']
console.log(s2.age) // 22
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式, 而且, instanceof 和 isPrototypeOf()也能用于识别基于组合继承创建的对象。
特点:
- 可以继承父类原型上的属性,可以传参,可复用。
- 每个新实例引入的构造函数属性是私有的。 缺点:
- 调用了两次父类构造函数(耗内存),子类的构造函数会被原型上的那个父类构造函数代替。
原型式继承
该方法最初由道格拉斯·克罗克福德于2006年在一篇题为 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式继承) 的文章中提出. 他的想法是借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型. 大意如下:
在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function Father(sons=[]){
this.name = 'Father'
this.height = 175
this.sons = ['J','K','L',...sons]
this.sayName = function(){
console.log(this.name)
}
}
Father.prototype.age = 22
let f1 = new Father()
let s1 = object(f1)
s1.sons.push('E')
console.log(s1.sons) // (4) ['J', 'K', 'L', 'E']
let s2 = object(f1)
console.log(s2.sons) // (4) ['J', 'K', 'L', 'E']
console.log(s2.age) // 22
用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。Object.create()就是这个原理。
特点:
- 类似于复制一个对象,用函数来包装。 缺点:
- 所有实例都会继承原型上的属性。
- 无法实现复用。(新实例属性都是后面添加的)。
在 ECMAScript5 中,通过新增Object.create()方法规范化了上面的原型式继承。
寄生式继承
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function Father(sons=[]){
this.name = 'Father'
this.height = 175
this.sons = ['J','K','L',...sons]
this.sayName = function(){
console.log(this.name)
}
}
Father.prototype.age = 22
function createSon(original){
let clone = object(original) // 通过调用object函数创建一个新对象
clone.sayHi = function(){ // 以某种方式来增强这个对象
console.log("hi");
};
return clone;//返回这个对象
}
let f1 = new Father()
let s1 = createSon(f1)
s1.sons.push('E')
console.log('s1',s1) // sons: (4) ['J', 'K', 'L', 'E']
let s2 = createSon(f1)
console.log('s2',s2) // sons: (4) ['J', 'K', 'L', 'E']
console.log(s2.age) // 22
仔细看,这不就是给原型式继承外面套了个壳子嘛
优点:
- 没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。 缺点:
- 没用到原型,无法复用。
寄生组合式继承
// 寄生
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function Father(sons=[]){
this.name = 'Father'
this.height = 175
this.sons = ['J','K','L',...sons]
this.sayName = function(){
console.log(this.name)
}
}
Father.prototype.age = 22
let con = object(Father.prototype) // 用原型式继承父类原型。 继承了 Father 的原型上的属性
// 组合
function Son(){
Father.call(this) // 用借用构造函数继承父类构造函数里的属性。 继承了父类构造函数里创建的属性
this.sayHi = function(){ // 以某种方式来增强这个对象
console.log("hi");
};
}
Son.prototype = con // 因为con 继承了 Father 原型上的属性,这样相当于 Son 也继承了原型上的属性
Son.prototype.constructor = Son // 修复 Son 的构造函数指向
console.log(con.constructor) // Son
console.log(Son.prototype.constructor) // Son
let s1 = new Son()
console.log('s1',s1)
s1.sons.push('E') // sons: (4) ['J', 'K', 'L', 'E']
console.log('s1.__proto__',s1.__proto__) // Father
let s2 = new Son()
console.log('s2',s2) // sons: (3) ['J', 'K', 'L']
寄生:
- 在函数内返回对象然后调用。
组合:
- 函数的原型等于另一个实例。
- 在函数中用apply或者call引入另一个构造函数,可传参。
这是最成熟的方法,也是现在库实现的方法。在Vue源码在extend方法里就用到了这种继承方式。
继承这些知识点与其说是对象的继承,更像是函数的功能用法,如何用函数做到复用,组合,这些和使用继承的思考是一样的。上述几个继承的方法都可以手动修复他们的缺点,但就是多了这个手动修复就变成了另一种继承模式。