1. 构造函数
在聊JavaScript的继承方式之前,我们还需要做一点准备工作,首先来聊一聊构造函数。
在js中,构造函数其实和函数时一样的,写法都是函数的定义方式
function A(){
// 定义其他操作
}
上面这个函数A
我们既可以看成是普通函数,也可以看成是一个对象的构造函数。普通函数就直接用A()
进行调用,而作为构造函数创建对象的时候,我们则是用new
关键字
var obj = new A()
现在我们知道了,一个函数可以作为创建对象实例的构造函数,也可以当做普通函数使用,区别就是在于是否使用了new
关键字。
构造函数在创建对象的时候,会执行如下的操作
在内存中创建一个对象
-
在新对象内部的
[[Prototype]]
属性指向构造函数的prototype
属性function A(){} let instance = new A() instance.__proto__ === A.prototype
构造函数内部的
this
指向这个创建的新对象执行构造函数内部的代码,比如添加一些属性、方法等
如果构造函数有返回值,该返回值为非空对象,则返回该对象,否则返回创建的新对象。
2. 原型链继承
我们知道在调用构造函数的时候,创建了一个新对象,这个对象有一个[[Prototype]]
属性是指向构造函数原型对象prototype
的,根据原型链查找方式,如果寻找一个实例对象的属性和方法,在实例对象上没找到,则会沿着[[Prototype]]
属性在原型对象中查找,如果还未找到则继续往上,直到到达Object.prototype
为止,因为该原型对象的原型为null
Object.prototype.__proto__ == null // true
借助这个原型链,我们就有了原型链的继承方式。原型链继承就是创建一个子类,指定它的原型对象为父类的原型对象,这样父类中添加的属性和方法就可以由子类实例共享。
/**
* 原型链继承可以继承原型对象的属性、方法,这些都是共享的
* 如果一个实例修改了原型对象的属性方法,另一个实例也会跟着修改
*/
function SuperType() {
this.colors = ['red', 'green', 'orange']
this.say = function () {
console.log('SuperType saying')
}
}
function SubType() {}
// 原型链方式继承,共享原型对象上的所有属性和方法
SubType.prototype = new SuperType()
let instance = new SubType()
console.log(instance.colors) // [ 'red', 'green', 'orange' ]
instance.colors.push('black')
instance.say() // SuperType saying
let anotherInstance = new SubType()
console.log(anotherInstance.colors) // [ 'red', 'green', 'orange', 'black' ]
console.log(instance instanceof SuperType) // true
console.log(instance instanceof SubType) // true
原型链继承的缺点也很明显,所有子类实例都是共享原型对象的属性方法,如果其中一个对原型属性进行修改,也会反映到其他实例上。为了弥补这个缺点,便引出了盗用构造函数的方式。
3. 盗用构造函数
盗用构造函数的出现是为了使得子类实例对象有着自己独立的属性和方法,我们可以认为这些子类实例都是相互独立的,不共享属性方法的,另一方面盗用构造函数还可以实现子类向父类构造函数传参,具体代码参考如下:
/**
* 盗用继承是利用父类的构造函数在子类中进行调用,创建专属于子类实例的属性
* 这样在修改实例属性的时候,实例之间相互独立,还可以实现向父类传参
* 缺点是它断开了与原型的关系,不能继承原型的方法,函数都要在子类中重写
*/
function SuperType(name) {
this.name = name
this.colors = ['red', 'green', 'orange']
}
function SubType(name, age) {
// 调用父类构造函数,传入当前的this对象和参数
SuperType.call(this, name)
// 实例自己的属性
this.age = age
}
let obj1 = new SubType('tom', 20)
obj1.colors.push('black')
console.log(obj1.colors) // [ 'red', 'green', 'orange', 'black' ]
console.log(obj1.name, obj1.age) // tom 20
let obj2 = new SubType('lucy', 19)
console.log(obj2.colors) // [ 'red', 'green', 'orange' ]
console.log(obj2.name, obj2.age) // lucy 19
// 盗用继承得到的实例,原型对象已经丢失
console.log(obj1.prototype) // undefined
我们可以看到关键的一步代码就是
SuperType.call(this)
这里是用父类构造函数作为普通函数,调用call()
方法,改变this
指向为当前创建的实例对象,从而使得当前创建的实例对象有了父类的属性和方法,因为在调用父类函数的过程中,属性和方法都保存到了this
里,并且也可以实现传参,利用call()
方法的第二到第n个参数,传递了到个参数给父类。
另一方面,由于调用call()
方法,子类实例对象的原型并没有指向父类的原型,因此断开了与父类原型的联系,这也是盗用构造函数的一个缺点,多个实例之间有了自己的属性方法空间,却不能有共享的原型对象的属性和方法,因此一些属性和方法在原型链中能够很好继承的模式,在这里就不适用了,只能重复造轮子。
由于这个缺点,便又引出了组合继承的方式。
4. 组合继承
组合继承方式结合了原型链的属性共享的优点以及盗用构造函数子类实例拥有独立属性空间的优点,因此也非常值得学习和借鉴。
/**
* 组合继承结合了原型链继承和盗用继承的优点,
* 既可以实现实例之间属性方法相互独立,又可以继承原型对象的属性方法
*/
function SuperType(name) {
this.name = name
}
// 父类原型对象上的方法
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.sayAge = function () {
console.log(this.age)
}
let obj1 = new SubType('tom', 20)
let obj2 = new SubType('lucy', 19)
obj1.sayName() // tom
obj2.sayName() // lucy
obj1.sayAge() // 20
obj2.sayAge() // 19
从代码我们可以看到,组合继承中在子类构造函数里调用了父类构造函数,创建了属于子类实例的独立属性空间,由于盗用构造函数的实例断开了与父类原型的联系,因此通过手动方式指定原型对象,从而连接了子类实例和父类原型对象,从而实现父类原型对象的属性方法被子类实例共享的效果。
// 原型链继承
SubType.prototype = new SuperType()
组合继承方式几乎完美的实现了js的继承,但是也还是差了一点,比如调用了两次父类构造函数,真正完美解决的继承方案,即寄生式组合继承,我们将在下一期进行分析。