在学习继承时要对js的原型对象/原型链/构造函数/this的指向有一定的理解
使用以下例子来辅助学习,首先定义两个构造函数,希望Son能继承Father
function Father() {
this.name = 'father'
this.color = ['red', 'blue']
this.age = 40
Father.prototype.sayHello = function () {
console.log('hello')
}
}
function Son() {
this.name = 'Son'
}
原型链继承
在es6之前声明的构造函数主要有以下特点:函数名大写(es6类名也要大写),实例的私有变量使用this定义,一般的函数方法是实例之间公用的,所以会定义在函数的原型对象上,这样每个实例对象都能共用相同的方法。
直接使用例子中的代码
function Father() {
this.name = 'father'
this.color = ['red', 'blue']
this.age = 40
Father.prototype.sayHello = function () {
console.log('hello')
}
}
function Son() {
this.name = 'Son'
}
通过原型链继承的关键代码就是:
Son.prototype = new Father()
/* 同时因为Son.prototype被重新定义了,
他的constructor指向了通过Father原型对象指向Father了如需利用constructor,
我们可以手动修复这个问题
*/
Son.prototype.constructor = Son
修改Son的原型对象,使Son的原型对象指向Father的实例。这样Son的实例在调用属性或者方法时,自己没有的属性或方法会去Son构造函数的原型对象上去找,这时候就找到了Father的实例上,而Father的实例又会指向Father的构造函数的原型对象上,这样我们就通过原型链实现了继承。
接下来测试一下
const p1 = new Son()
console.log(p1.name, p1.age, p1.color) // Son 10 ['red', 'blue']
p1.sayHello() // 'hello'
乍一看没什么问题了,但是还是存在许多问题(听君一席话,如听一席话)
首先是Father实例中如果有引用属性,在原型链继承方式下,Son的实例都会共用该引用属性。(不明白的可以去了解一下堆/栈/传值/传址这些知识)
const p2 = new Son()
p2.color.push('black')
console.log('p2.color') // ['red', 'blue','black']
console.log('p1.color') // ['red', 'blue','black']
/* 如果使用p2.color = ['red', 'blue','black']这种形式来修改,
其实是给p2实例上添加了一个color属性,会遮蔽原型上的color,
并不会修改原型上的color属性,此时p1的color仍是 ['red', 'blue']
*/
原型链继承第二个问题,无法给父类型的构造函数传参,因为传参发生在new的时候,在修改Son的原型对象时Father已经new了。这两个问题导致原型链继承不会单独使用。
盗用构造函数继承
使用这种方式继承需要对call/apply/bind这种强绑定修改this指向的方法有一定了解。
直接使用例子中的Father代码
function Father() {
this.name = 'father'
this.color = ['red', 'blue']
this.age = 40
Father.prototype.sayHello = function () {
console.log('hello')
}
}
在创建Son构造函数时,我们要先利用call/apply执行一下Father的构造函数,这样在创建Son实例时,就会为实例添加父类所有的属性。
function Son() {
Father.call(this)
this.name = 'son'
}
测试:
const p1 = new Son()
const p2 = new Son()
p1.color.push('black')
console.log(p1.color) // ['red', 'blue','black']
console.log(p2.color) // ['red', 'blue']
p1.sayHello() // Uncaught TypeError: p1.sayHello is not a function
通过这种方式的继承的构造函数其实等用于这样:
function Son() {
// Father.call(this)
this.name = 'father'
this.color = ['red', 'blue']
this.age = 40
Father.prototype.sayHello = function () {
console.log('hello')
}
this.name = 'son' // 注意私有变量要写在下方,不然会被父类构造方法覆盖
}
原封不动的将Father构造函数放在了Son构造函数中执行,会为每一个Son的实例添加一个color属性,所以彼此直接并无关联。这种新式也能解决原型链继承无法给父类传参的问题。如下我们简单改造一下函数即可证明这一点
function Father(fatherName) {
this.name = fatherName
}
function Son(name) {
Father.call(this, name)
}
const p1 = new Son('father')
console.log(p1.name) // 'father'
但是这种形式也有一些问题,主要缺点就是父类的构造函数中的方法没法复用,我们知道想要方法复用需要定义在构造函数的原型对象上,通过调用父类的构造函数,父类的方法只有在构造函数中定义,子类才能获取到,最终方法相当于定义在了子类的构造函数中。父类定义在原型对象上的方法,当子类执行时相当于给父类原型对象添加一个方法,而子类并不能获取到。简单来说就是,函数不能重用,子类不能访问父类的原型所以这种继承方式我们也不会单独使用。
组合继承
组合继承综合了原型链继承和盗用构造函数继承,将两者方式结合起来,利用原型链来继承父类原型上的属性和方法,利用盗用构造函数来继承实例属性,既能复用方法,也能让每个实例有自己的属性。
function Father() {
this.name = 'father'
this.color = ['red', 'blue']
this.age = 40
Father.prototype.sayHello = function () {
console.log('hello')
}
}
function Son() {
Father.call(this)
this.name = 'Son'
}
Son.prototype = new Father()
Son.prototype.constructor = Son
测试
const p1 = new Son()
const p2 = new Son()
p1.color.push('black')
console.log(p1.color) // ['red', 'blue','black']
console.log(p2.color) // ['red', 'blue']
p1.sayHello() // 'hello'
这种继承弥补了原型链和盗用构造函数的不足,但是也出一些冗余属性。
其他一些继承类型
原型式继承,寄生式继承,寄生式组合继承 感兴趣的可以深入研究一下
ES6类
ES6的类的写法类似java的类的定义,但是它其实是ES5语法糖的形式,背后仍然使用的原型和构造函数的概念
使用类来重写上面的例子 私有变量写在constructor中,通用方法直接写在constructor同级,此外还可以加入一些静态方法,使用static修饰。继承使用extends关键字,写法上只允许继承一个类或者一个构造函数,constructor中super()(执行父类的构造函数)一般放在第一行,不允许在super之前出现this。
class Father {
constructor() {
this.name = 'father'
this.color = ['red', 'blue']
this.age = 40
}
sayHello() {
console.log('hello')
}
}
class Son extends Father {
constructor() {
super() // 不要在调用super()之前引用this,否则会抛出ReferenceError
this.name = 'Son'
}
}
测试
const p1 = new Son()
const p2 = new Son()
p1.color.push('black')
console.log(p1.color) // ['red', 'blue','black']
console.log(p2.color) // ['red', 'blue']
p1.sayHello() // 'hello'
类还是语法糖的形式,我们还是要理解他们背后的原理。