这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天
前言
在es5时期JavaScript关于类没有官方的定义,类的定义和继承在技术人之间有多种实现方案,在es5中实现类的继承是一件非常复杂的事。本次笔记会对es5的JavaScript实现类的继承做出一个简单的说明,并再次体验es6版本的JavaScript的强大功能。同时,也是对JavaScript原型链再次进行一个相对完善的理解。
JS的7种继承方式总览
原型链继承
通过JavaScript的原型链机制,我们明白了任何对象都存在原型对象。所以,我们可以利用对象构造函数的prototype属性实现继承,将子类构造函数的原型对象变更为父类实例对象。
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child'
}
Child.prototype = new Parent()
//Child的原型应当能够得到其构造函数
Child.prototype.constructor = Child
console.log(new Child())
虽然子对象能够继承父对象的属性,但所有子实例共享一个父实例存在问题
child1.play.push(4)
console.log(child1.play)
console.log(child2.play)
输出结果
[1, 2, 3, 4]
[1, 2, 3, 4]
原型链继承中child1和child2的play方法都指向其原型对象,但有时我们也需要将父类的属性挂载在子实例上而非原型对象上,即我们想要子类的独有属性,这时需要我们需要借助构造函数来解决此问题。
构造函数继承
js定义的函数中内置call方法,能够调用该函数并将函数内部的this指向修改为指向传入的第一个参数,通过在子构造函数中调用父构造函数中的call方法,向子实例中添加父类的属性
function Parent() {
this.name = 'parent'
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this)//函数内置的call方法,第一个参数能修改函数内this指向
this.type = 'child'
}
const child = new Child()
console.log(child)
console.log(child.getName)//不能获得getname方法
输出结果
{name: 'parent', type: 'child'}
undefined
构造函数继承的子实例相当于把父类的属性复制了一遍,因此子实例不会再共用属性,但也能发现,仅使用构造函数继承只能获得构造函数中的属性,并没有继承父类原型上的方法,即没有继承父类的原型链。
组合继承
原型链继承不能独享属性,构造函数不能继承原型链,所以两个一起上就能各取所需
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
//第二次调用Parent构造函数
Parent.call(this)//函数自带的call方法,第一个参数能修改函数内this指向
this.type = 'child'
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
//第一次调用Parent构造函数
const child1 = new Child()
const child2 = new Child()
child1.play.push(4)
console.log(child1.play)
console.log(child2.play)
console.log(child1)
console.log(child1.getName())
输出结果
[1, 2, 3, 4]
[1, 2, 3]
{name: 'parent', play: [1, 2, 3, 4], type: 'child'}
parent
子实例的play通过父类构造函数得到,互不干扰,同时也继承了原型链,但在继承中调用了两次Parent构造函数。这时我们可以思考,能否对代码进行优化,让Parent构造函数只调用一次就实现同样功能呢?
Object.create方法
在优化前,我们先介绍Object.create方法,该方法可传入一个对象参数,并返回一个以该参数为原型对象的空对象
const parent = {
type: 'parent',
hello() {
console.log('hello world')
}
}
const child = Object.create(parent)
console.log(child)
console.log(child.__proto__ == parent)
输出结果
{}
true
原型式继承
借助Object.create方法,我们就可以省去修改prototype过程
const parent = {
name: 'parent',
play: [1, 2, 3]
}
const child1 = Object.create(parent)
const child2 = Object.create(parent)
child1.play.push(4)
console.log(child1.play)
console.log(child2.play)
输出结果
[1, 2, 3, 4]
[1, 2, 3, 4]
思路与原型链继承相同,问题也和原型链继承相同
寄生式继承
原型式继承的简单加强,获得的子对象不是空对象了
const parent = {
name: 'parent',
play: [1, 2, 3]
}
//这次没用构造函数
function clone(origin) {
let child = Object.create(origin)
child.type = 'child'
child.hello = function() {
console.log('hello world!')
}
return child
}
const child = clone(parent)
console.log(child)
console.log(child.play)
输出结果
{type: 'child', hello: ƒ}
[1, 2, 3]
寄生组合式继承
不难发现,如果使用Object.create能够减少一次Parent实例对象的构造
function Parent() {
this.name = 'parent'
this.play = [1, 2 , 3]
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this)
this.type = 'child'
this.hello = function() {
console.log('hello world!')
}
}
function clone(Parent, Child) {
//这里少创建了一个Parent实例对象
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
}
clone(Parent, Child)
const child = new Child()
console.log(child)
console.log(child.getName())
输出结果
{name: 'parent', play: [1, 2, 3], type: 'child', hello: ƒ}
parent
寄生组合式继承实现了所有功能并且效率有所提升
ES6 class extends
本质是es5寄生组合式继承,但es6不仅统一了继承标准,更是将为我们省去了十分繁琐的步骤
class Parent {
name
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Child extends Parent {
type
constructor(name, type) {
super(name)
this.type = type
}
}
const child = new Child('parent', 'child')
console.log(child)
console.log(child.getName())
输出结果
{name: 'parent', type: 'child'}
parent
总结
通过本次的学习,我们了解了es6中class关键字的实现原理,循序渐进的理解了JavaScript在实现类的继承上所进行的各式各样的优化方案,进一步强调了在实现功能的基础上尽可能的减少代码量增强可读性的核心概念。