原型模式
原型对象
在我们创建的一个函数时,都有一个 prototype(原型)属性,这个属性是一个指针,指向原型对象,并且所有的原型对象都会自动获得一个 constructor ,下面我们先定义一个函数,并把所有的属性和方法都挂载在函数的 prototype 属性下,并新建两个对象实例:
function Person() {}
Person.prototype.name = 'jerome'
Person.prototype.age = 18
Person.prototype.job = 'Front End Engineer'
Person.prototype.sayName = function() {
alter(this.name)
}
const person1 = new Person()
const person2 = new Person()
那么,代码中实际上 Person 构造函数、Person 的原型属性以及 Person 现有的两个实例间的关系是怎么样的呢,让我们看下图:
从图中我们可以看到,Person 的 prototype 属性指向了 Person 的原型对象,而原型对象中的 constructor 又指回了 Peron。并且在 Person 的原型下,还有我们定义的属性和方法。再看创建的两个实例,每个实例都有一个内部属性 [[prototype]](proto) 指向了原型对象。
原型对象的问题
由于原型模式省略了为构造函数初始化参数这一环节,默认情况下所有的实例都会取得相同的属性值,在一定程度上造成了不便。但是,还有一个更大的问题,是由其共享的本性所导致的,在属性有引用类型的时候,这个问题就比较突出了,让我们看下下面的代码:
function Person() {}
Person.prototype = {
constrcutor: Person,
name: 'jerome',
age: 18,
job: 'Front End Engineer',
friends: ['shy', 'faker'],
sayName: function() {
alter(this.name)
}
}
const person1 = new Person()
const person2 = new Person()
person1.friends.push('rookie')
console.log(person1.friends) // "shy,faker,rookie"
console.log(person2.friends) // "shy,faker,rookie"
console.log(person1.friends === person2.friends) // true
当原型对象中有引用类型的值时,修改引用类型的值,由于他是一个引用(指针),会修改所有实例的这个属性的值,就如上代码,在 person1.friends 增加一个值之后,person1 和 person2 的 friends 都被修改了。
组合使用构造函数模式和原型模式
由于在使用原型模式时有引用类型会造成的问题,那么可以结合构造函数和原型模式来使用。这个方法我总结了一句话:属性使用构造函数模式(不共享),方法使用原型模式(共享)。让我们来看下下面的例子:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['shy', 'faker']
}
Person.prototype = {
constrcutor: Person,
sayName: function() {
alter(this.name)
}
}
const person1 = new Person('jerome', 18, 'Front End Engineer')
const person2 = new Person('the shy', 18, 'gamer')
person1.friends.push('rookie')
console.log(person1.friends) // "shy,faker,rookie"
console.log(person2.friends) // "shy,faker"
console.log(person1.friends === person2.friends) // false
可以看到,两个实例的 friends 已经不共享了。
原型链
原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.property.getSubValue = function() {
return this.subproperty
}
const instance = new SubType()
console.log(instance.getSuperValue()) // true
我们可以看到,instance 是 SubType 对象,却可以使用 SuperType 的方法,这就是继承了 SuperType ,而继承的实现就是通过创建父函数的实例,并将该实例赋值给子函数的 prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。让我们来看下他们之间的关系:
在上面代码中,我们没有使用 SubType 默认的原型对象,而是使用了 SuperType 的实例当作他的原型。这个原型含有 SuperType 所有的属性和方法。从之前原型对象的知识点可知,实例的内部还有一个指针([[prototype]]),指向原型,这里相当于是 SubType 的原型对象指向了 SuperType 的原型,这样就实现了继承。
原型链的问题
同样的,原型链也含有引用类型的问题,那么,接下来我将介绍一下如何解决这个问题。
借用构造函数
这个方法的基本思想就是在子类型构造函数的内部通过 call 或者 apply 调用超类型构造函数。
function SuperType(name) {
this.name = name
this.friends = ['the shy', 'faker']
}
function SubType() {
SuperType.call(this, 'jerome')
this.age = 18
}
const instance1 = new SubType()
instance1.friends.push('rookie')
console.log(instance1.friends) // [ 'the shy', 'faker', 'rookie' ]
console.log(instance1.name) // jerome
const instance2 = new SubType()
console.log(instance2.friends) // [ 'the shy', 'faker' ]
但这个方法存在一个问题,方法都在构造函数中定义,函数复用也无从说起,这个方法很少单独使用。
组合继承
组合继承,就是将原型链和借用构造函数的技术组合到一起。
function SuperType(name) {
this.name = name
this.friends = ['the shy', 'faker']
}
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.constructor = SubType
SubType.prototype.sayAge = function() {
console.log(this.age)
}
const instance1 = new SubType('jerome', 18)
instance1.friends.push('rookie')
console.log(instance1.friends) // [ 'the shy', 'faker', 'rookie' ]
instance1.sayName() // jerome
instance1.sayAge() // 18
const instance2 = new SubType('the shy', 19)
console.log(instance2.friends) // [ 'the shy', 'faker' ]
instance2.sayName() // the shy
instance2.sayAge() // 19
在这个例子中,可以看到 SubType 继承了 SuperType 的属性和方法,又有自己的属性和方法,并且 friends 属性是相互独立的,避免了原型链(引用类型问题)和借用构造函数(方法无法复用)的缺陷,但组合继承也有缺点,我们后续会说到。
原型式继承
原型式继承,就是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。可以利用 Object.create() 来实现,这个方法接收两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象(可选)
const person = {
name: 'jerome',
friends: ['the shy', 'faker']
}
const anotherPerson = Object.create(person, {
name: {
value: 'rookie'
}
})
anotherPerson.friends.push('zoom')
console.log(anotherPerson.name) // rookie
console.log(person.friends) // [ 'the shy', 'faker', 'zoom' ]
console.log(anotherPerson.friends) // [ 'the shy', 'faker', 'zoom' ]
可以看到,原型式继承还有引用类型的问题
寄生式继承
寄生式继承,就是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
function createAnother(original) {
const clone = Object.create(original)
clone.sayHi = function() {
console.log('hi')
}
return clone
}
在例子中,该函数为源对象增加了一个 sayHi 的方法,和原型式继承类似。
寄生组合式继承
从前面看,组合式继承似乎就是最好最常用的继承方式,不过,它也有自己的不足,组合式继承无论在什么情况下,都会调用两次超类型的构造函数,第一次是在 new SuperType() 的时候,第二次是在调用 SuperType.call 的时候。
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
// 相当于 subType 的原型对象不直接 new 出来,而是从 Object 继承下来
function inheritPrototype(subType, superType) {
const prototype = Object.create(superType.prototype)
prototype.constructor = subType // constructor 指回构造函数
subType.prototype = prototype // prototype 指向原型对象
}
function SuperType(name) {
this.name = name
this.friends = ['the shy', 'faker']
}
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)
}
const instance1 = new SubType('jerome', 18)
instance1.friends.push('zoom')
console.log(instance1.friends) // [ 'the shy', 'faker', 'chen' ]
instance1.sayName() // jerome
instance1.sayAge() // 18
const instance2 = new SubType('rookie', 19)
console.log(instance2.friends) // [ 'the shy', 'faker' ]
instance2.sayName() // rookie
instance2.sayAge() //19
这个例子的高效率提现在它只调用了一次超类型的构造函数,避免在 SubType.prototype 上创建不必要的、多余的属性。与此同时,原型链还能保持不变,寄生组合继承是引用类型最理想的继承范式。