对象继承
常见的对象继承方法有:
对象的继承是面向对象编程中的一个核心概念,它指的是一个对象(子类或派生类)可以获取另一个对象(父类或基类)的属性和方法。通过继承,子类可以重用父类的代码,并且可以在此基础上添加或覆盖父类的属性和方法,从而实现代码的复用和扩展。
具体来说,继承有以下几个主要特点:
代码重用:子类可以直接继承父类的属性和方法,从而避免了在子类中重新编写与父类相同的代码。 扩展性:子类可以在继承父类的基础上,添加自己的属性和方法,或者覆盖父类的某些方法,以实现特定的功能。 层次结构:通过继承,可以创建出具有层次结构的类体系,使得类的设计更加清晰和有序。
原型链继承
原型继承的实质就是对原型修改效果的传递,基于原型链的特性:访问属性时,如果子对象没有该属性,则访问其原型的属性类。实际上,所有继承方式的本质都是原型链。
原型链继承就是通过构造函数的方式,将父类的实例赋值给子类的显示原型(prototype)上,以此类推,每一层都可以继承上一层的属性。
function Parent() {}
Parent.prototype.package = ['书']
function Children() {}
// 继承了 Parent
Children.prototype = new Parent()
var instance1 = new Children()
var instance2 = new Children()
instance1.package.push('笔')
console.log(instance1.package) // ['书', '笔']
console.log(instance2.package) // ['书', '笔']
console.log(instance1.package === instance2.package) // true
在上述例子中,Children.prototype 是 Parent 函数的实例,继承Parent.prototype上的属性。当实例instance1向数组中添加值得时候,发生了浅拷贝,所以instance2创建后,继承了数组添加后的结果。
原型链继承的问题:
- 通过原型继承的时候,原型上的所有属性都是共享的,如果这个属性是引用类型的值,那么中途继承的实例一旦将属性修改,修改后的值也会在新继承的实例上获取到。
- 子类型在实例化时,无法在不影响所有对象实例的情况下,给父类型的构造函数传参。
借用构造函数继承
基本思路:在子类构造函数中调用父类构造函数。
使用apply()和call()方法以新创建的对象为上下文执行构造函数。
function Parent (name) {
this.name = name
}
function Children () {
// 继承了 Parent,同时传递了参数
Parent.call(this, 'sunshine')
// this.name = 'sunshine'
// 实例属性
this.age = 25
}
var instance = new Children()
console.log(instance) // { age: 25, name: "sunshine" }
Prent接收参数name然后给它赋值一个属性。在Children构造函数中调用Parent构造函数时传入了这个参数,会在Children的实例上定义name属性。为确保Parent函数不会覆盖Children定义的属性,所以在调用父函数再给子函数添加了age属性。
借用构造函数的问题:
- 必须在构造函数中定义方法,函数不能重用;
- 子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。
对象继承
对象继承也叫原型式继承,不自定义类型也可以通过原型实现对象之间的信息共享。
function object(o) {
// 创建一个临时构造函数
function F () {}
// 将传入的对象赋值给这个构造函数的原型
F.prototype = o
// 返回这个临时类型的一个实例
return new F()
}
var parent = {
name: '父亲',
getName: function() {
console.log(this.name)
}
}
var children = object(parent)
parent.getName() // "父亲"
children.getName() // "儿子"
在ES5中增加Object.create方法将原型式继承的概念规范化了。该方法接受两个参数:第一个参数为新对象的prototype,第二个参数描述了新对象的属性。
var parent = {
name: '父亲',
getName: function() {
console.log(this.name)
}
}
var children = Object.create(parent, {
name: { value: '儿子' }
})
parent.getName() // "父亲"
children.getName() // "儿子"
综上所述,对象继承非常不适合单独创建构造函数,但是仍然需要在对象之间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
寄生式继承
与对象继承最接近的一种继承方式是寄生式继承。寄生式继承的思路:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。实际上,寄生式继承就是将创建对象的过程做了一个封装。
function inherit (obj) {
var cloneObj = Object.create(obj)
cloneObj.getName = function() {
console.log(this.name)
}
return cloneObj
}
var parent = {
name: 'sunshine'
}
var children = inherit(parent)
children.getName() // 'sunshine'
寄生式继承的问题:
通过寄生式继承给对象添加函数会导致函数难以重用,与借用构造函数模式类似。
组合式继承
组合继承的思路:使用原型链实现对原型属性和方法的继承,通过构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function Parent(name) {
this.name = name
this.package = ['书']
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Children (name, age) {
// 继承属性
Parent.call(this, name) // 第2次调用Parent
this.age = age
}
// 继承方法
Children.prototype = new Parent() // 第1次调用Parent
Children.prototype.getAge = function () {
console.log(this.age)
}
// 使用
var instance1 = new Children('sunshine', 20)
var instance2 = new Children('colorful', 30)
instance1.package.push('笔')
console.log(instance1.package) //[ '书', '笔' ]
instance1.getName() //sunshine
instance1.getAge() //20
console.log(instance2.package) //[ '书' ]
instance2.getName() //colorful
instance2.getAge() //30
console.log(instance1 instanceof Parent) // true
console.log(Parent.prototype.isPrototypeOf(instance1)) // true
组合继承弥补了原型链和借用构造函数的不足,也保留了 instanceof 操作符和 isPrototypeOf() 方法的识别能力。
组合继承的问题:
效率问题:父类构造函数始终会被调用2次,一次是在创建子类型时调用,另一次是在子类构造函数中调用。这样就会导致子类和实例上都有相同的属性。
寄生式组合继承
寄生式组合继承通借用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本,即:使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
function inherit(children, parent) {
children.prototype = Object.create(parent.prototype)
children.prototype.constructor = children
children.__proto__ = parent
}
function Parent(name) {
this.name = name
this.package = ['书']
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Children (name, age) {
// 继承属性
Parent.call(this, name)
this.age = age
}
inherit(Children, Parent)
Children.prototype.getAge = function () {
console.log(this.age)
}
// 使用
var instance1 = new Children('sunshine', 20)
instance1.getName() //sunshine
instance1.getAge() //20
这里的 inherit 函数实现了寄生式组合的核心逻辑。这个函数接收两个参数:子构造函数和父构造函数。
- 创建父类原型的一个副本
- 给返回 prototype 对象设置 constructor 属性,解决由于重写原型导致的 constructor 丢失问题
- 将创建的对象赋值给子类型的原型
寄生式组合继承可以算是引用类型继承的最佳模式。但是它还有一些不足之处,就是没有对静态属性的继承。
内置对象继承 我们知道,浏览器的内置对象有很多种,其中包括 Date,Number,String等,对于这种内置的构造器而言,是不能用上面的方式继承的。以Date为例会出现错误的原因是日期对象只能通过 Date 构造函数来实例化生成。
类继承 在ES6时代,我们可以使用 class extends 进行继承。
class DateConstructor extends Date {
constructor () {
super()
this.foo = 'bar'
}
getMyTime () {
return this.getTime()
}
}
var date = new DateConstructor()
console.log(date.getMyTime())