前言
通过这篇文章我们了解一下JS的原型和实现继承的几种方式,以及他们各自的优缺点。
原型链
基本思想就是通过原型继承多个引用类型的属性和方法,我们来重温一个构造函数、原型和实例的关系。 每个构造函数都有原型对象(prototype),原型有一个属性(constructor)指回构造函数,而实例有一个内部指针(proto)指向原型,如果这个原型是另一个类型的实例呢?那么就意味着这个原型内部也有一个指针指向另一个原型,另一个原型也有一个属性指向另一个构造函数,这样就形成了一个原型链。
例如:c1.prototype = instance2 如果试图引用c1构造的实例instance1中的某个属性p1
- 首先会在instance1 内部找一遍
- 然后会在instance1.proto(c1.prorotype)中找,而c1.prototype 实际上是instance2,也就是在instance2中找
- 如果instance2中还没找到,会继续往上找instance2.proto,直到Object的原型对象。
instance1->instance2->instance2._proto->....->Object.prototype
原型链的问题
- 当原型链中包含引用值类型值是,该引用类型值会被所有实例共享。
- 子类型在实例化时不能给父类型的构造函数传参。
盗用构造函数(经典继承)
为了解决原型中包含引用值导致的继承问题 基本思路:在子类构造函数中调用父类构造函数。
typescript
function father(){
this.arr = [1,2,3]
}
function son(){
father.call(this) // 继承father 可以传递参数 用 apply 也可以
}
const instance1 = new son()
instance1.arr.push(4)
console.log(instance1.arr) // 1,2,3,4
const instance2 = new son()
console.log(instance2.arr) // 1,2,3
当然这种继承方法也有问题,方法都在构造函数中定义,因此son函数也不能复用了,而且father 原型中定义的方法也不能访问。
组合继承(伪经典继承)
基本思路:使用原型链实现对原型属性和方法的继承,同时通过借用构造函数来实现对实例属性的继承。
typescript
function father(name){
this.name = name
this.arr = [1,2,3]
}
father.prototype.sayName = function(){
console.log(this.name)
}
function son(name,age){
father.call(this,name) // 继承实例属性 第一次调用father
this.age = age
}
son.prototype = new father() // 继承父类方法 第二次调用father
son.prototype.sayAge = function(){
console.log(this.age)
}
let instance1 = new son('tao',5)
instance1.arr.push(4)
console.log(instance1.arr) // 1,2,3,4
instance1.sayName() // tao
instance1.sayAge() // 5
let instance2 = new son('lin',10)
console.log(instance2.arr) // 1,2,3
instance2.sayName() // lin
instance2.sayAge() // 10
这样就解决了原型链和盗用构造函数的缺陷,但是父类构造函数会调用两次。
原型式继承
基本思路:在object() 函数内部,创建一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时构造函数的实例。
typescript
function object(o){
function F(){}
F.prototype = o
return new F()
}
本质上讲,object() 函数是对传入的对象进行了一次浅复制
typescript
let p = {
name:['tao','lin']
}
let a1 = object(p)
a1.name.push('zhange')
let a2 = object(p)
a2.name.push('xu')
console.log(p.name)
把p 传入object() 函数中,然后返回一个新对象,这个新对象的原型指向p,因此新对象的原型中还包含引用类型的属性。 其实现在Object.create()
方法已经规范了上面的原型式继承。
寄生式继承
基本思路:创建一个用于封装继承过程的函数,在函数内部以某种方式来增强对象,然后再返回这个对象
typescript
function create(o){
let clone = object(o) // 创建一个新对象,其他方法也可以
clone.sayName = function(){ // 以某种方式增强这个对象
console.log('li')
}
return clone
}
这个方法中基于 o 返回了一个新对象,新对象不仅具有 o 的属性和方法,还被增强了。 使用寄生式继承来为对象添加函数,也会导致函数难以复用,这一点与盗用构造函数相似
寄生组合式继承
基本思路:不通过调用父类构造函数给子类原型赋值,而是获取父类原型的一个副本。
typescript
function extend(son,father){
let prototype = Object.create(father.prototype) // 创建父类原型的副本
prototype.constructor = son // 设置新对象的constructor,解决重写原型导致constructor丢失问题
son.prototype = prototype // 将新创建的对象赋值给子类原型
}
function father(name){
this.name = name
this.arr = [1,2,3]
}
father.prototype.sayName = function(){
console.log(this.name)
}
function son(name,age){
father.call(this,name) // 一次调用
this.age = age
}
extend(son,father) //
son.prototype.sayAge = function(){
console.log(this.age)
}
let instance1 = new son('tao',5)
instance1.arr.push(4)
console.log(instance1.arr) // 1,2,3,4
instance1.sayName() // tao
instance1.sayAge() // 5
let instance2 = new son('lin',10)
console.log(instance2.arr) // 1,2,3
instance2.sayName() // lin
instance2.sayAge() // 10
这种继承模式可以是引用类型继承的最佳方式。
class继承
class继承本质上还是基于原型机制的语法糖,具体请看阮一峰老师的es6 class 这一章节。
总结
ES5 有5种实现继承的方式:
- 原型继承
- 盗用构造函数继承
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
ES6 则可以通过class关键字来实现类继承。
如果本文对你有一点帮助,请点一个大大的赞,你的支持就是我创作的动力。