Javascript 的继承的实现方法有很多种,之前虽然学习过,但是没有综合整理过,这一次就来整理整理 Javascript 语言的继承方面的知识。关于详细的Javascript 的继承方面的知识,推荐大家去看那本红宝书 ————《JavaScript高级程序设计》。
虽然 ES6 推出了 class 这个概念,方便了我们开发人员的学习和理解,但是,class 只是一个语法糖,实际上底层的实现还是原来的那一套,利用原型链和构造函数来实现继承。因此要想 Javascript 的基本功牢实一点,还是需要去学习这些知识的。
在 Javascript 的继承实现里,目前有原型链继承法,构造函数继承法,组合继承法等等方法,下面我就一一对这些方法来进行说明。
1. 原型链继承
原型链继承法是运用 Javascript 的原型来实现,在 Javascript 中任意函数都拥有 prototype
和 __proto__
这两个属性,而每个对象都拥有一个 __proto__
属性,对象里 __proto__
属性的值是来自于构造这个对象的函数的 prototype
属性,通过 prototype
和 __proto__
,我们构造出原型链,然后利用原型链来实现继承。
具体的代码例子如下
function Animal() {
this.type = 'Cat'
this.name = 'Nini'
this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat() {
this.age = '1'
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
let smallCat = new Cat()
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini
let bigCat = new Cat()
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball', 'sleep' ]
复制代码
从上面的例子我们可以看到,原型链继承的优点:
- 多个实例共同引用可复用的属性和方法,不是创建每一个实例的时候再创建一遍这些数据
缺点:
- 所有的属性都被实例所共享,这意味着如果属性是基本数据类型的话,实例是无法修改这个属性的值,因为实例会新增一个同名的属性,我们只能对新增的属性进行操作,以刚刚的代码为例
smallCat.name = 'Kiki' // 此时 smallCat 对象上新增了 name 属性,如果访问这个属性的话,我们得到是这个新增的属性而不是在原型上的 name 属性
console.log(smallCat.name) // 'Kiki'
console.log(bigCat.name) // 'Nini'
复制代码
如果属性是引用属性的话,修改这个属性所指向的数据里的内容将会影响所有的实例(注意不是对属性直接赋值,如果直接赋值了就像基本数据类型一样,在实例本身上新建一个同名属性),如之前的代码实例
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball', 'sleep' ]
复制代码
2. 构造函数继承
构造函数继承的基本原理就是利用 call, apply 这样的可以指定函数 this 值的方法,来实现子类对父类属性的继承,例子如下
function Animal(type, name) {
this.type = type
this.name = name
this.hobbies = ['eat fish', 'play ball']
}
function Cat(type, name) {
Animal.call(this, type, name)
this.age = '1'
this.say = () => {
console.log('type is ' + this.type + ' name is ' + this.name);
}
}
let smallCat = new Cat('Cat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini
let bigCat = new Cat('Cat', 'Nicole')
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball' ]
bigCat.say() // type is Cat name is Nicole
复制代码
从上面的例子可以看到,构造函数继承的优点是
-
所有的实例没有共享引用属性,也就是说每个实例都独立拥有一份从父类那里继承来的属性,任一个实例修改了引用属性里的数据内容,并不会影响到其他的实例
-
可向父函数传参
缺点:
- 由于所有的属性和方法都不再被所有的实例共享,因此那些公有的属性和方法就会被重复的创建,造成了内存的额外开销
3. 组合继承 (原型链继承和构造函数继承的合体)
其实通过之前的分析,可以知道,无论是原型链继承还是构造函数继承,都存在自己的优缺点,对于我们的开发实现而言,都是不完美的。原型链继承把所有的属性和方法都共享给了所有的实例,也就是说,我们想要个性化的针对某一实例上所继承的引用属性的数据内容进行修改的话,这一操作将同时影响别的实例,这可能会给我们的开发带来一定的问题。构造函数继承把所有的属性和方法都为每个实例单独拷贝了一份,虽然实现了实例之间的数据隔离,但是对于那些本来就应该是公共的属性和方法来说,重复而无意义的复制也无疑是增加了额外的内存开销。
因此,组合继承方法吸收了这两个方法的优点,同时避免了各自的缺点,是一种可行的实现继承的方法,实现的代码如下
function Animal(type, name) {
this.type = type
this.name = name
this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat(type, name) {
Animal.call(this, type, name) // 构造函数继承
this.age = '1'
}
Cat.prototype = new Animal() // 原型链继承
Cat.prototype.constructor = Cat
let smallCat = new Cat('smallCat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is smallCat name is Nini
let bigCat = new Cat('bigCat', 'Nicole')
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
bigCat.say() // type is bigCat name is Nicole
复制代码
组合继承方法的思路是将公共的属性和方法放在父类的 prototype
上,然后利用原型链继承来实现公共的属性和方法的继承,而对于那种每个实例都可自定义修改的属性采取构造函数继承的方法来实现每个实例都独有一份这样的属性。
4. 原型式继承
原型式继承的实现原理就是将一个对象作为创建对象的原型传入到一个构建新对象的函数中,比如
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
复制代码
其实原型式继承的思路也就是 Object.create()
方法的实现思路,来看看一个完整的原型式继承的实现,代码如下
let Animal = {
type: 'Cat',
name: 'Nini',
hobbies: ['eat fish', 'play ball']
}
function createCat(o) {
function F() {}
F.prototype = o
return new F()
}
let smallCat = createCat(Animal)
let bigCat = createCat(Animal)
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
bigCat.name = 'Nicole' // 直接在 bigCat 这个对象上新增一个 name 属性,并非去修改原型上的 name 属性
console.log(smallCat.name); // 'Nini'
console.log(bigCat.name); // 'Nicole'
console.log(bigCat.__proto__.name); // 'Nini' 原型上的 name 属性依旧保持
复制代码
原型式继承法其实和原型链继承有点相似,都是所有的属性和方法放在了原型上,如果创建所有的实例时都用的是同一个对象作为原型的话,那么原型链继承遇到的问题,这个方法同样也有。
关于原型式继承的更多思考
在学习原型式继承的时候,我想到了如果创建每个实例的时候,传入的父类对象都是不同的对象,但是都是同属于一个父类的对象,那么如果我们将公共的属性和方法放在父类的原型上,把可自定义的属性放在父类的构造函数上,那也可以实现比较合理的继承,具体代码如下
function Animal(type, name) {
this.type = type
this.name = name
this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
console.log('type is ' + this.type + ' name is ' + this.name);
}
function createCat(o) {
function F() {}
F.prototype = o
return new F()
}
let smallCat = createCat(new Animal('smallCat', 'Nini'))
let bigCat = createCat(new Animal('bigCat', 'Nicole'))
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
复制代码
这个思路看起来不错,但是仔细想想还是有一定的问题的,相比于之前提到的组合式继承来说,这个方法每次在创建实例的时候,我们都会 new 一个新的父类实例,这其实造成了内存的浪费,而组合继承则保证了父类的实例只会被 new 一次,而那些可以自定义的属性都被存在每个子类的实例中,保证了数据的互不影响,我们可以通过下面的图片来看看具体的差异
5.寄生式继承
寄生式继承其实和原型式继承的实现有些相似,不过寄生式继承在原型式继承的基础上添加了在创建实例的函数中以某种形式来增强对象,最后返回对象。其实意思就是,在创建子实例的函数中,先通过原型式继承的方法创建一个实例,然后为这个实例添加属性和方法,最后返回这个实例,代码实例如下
function createCat(o) {
let cloneObj = Object.create(o)
cloneObj.say = function (){ // 为实例添加一个 say 方法
console.log('type is ' + this.type + ' name is ' + this.name);
}
return cloneObj
}
let Animal = {
type: 'Cat',
name: 'Nini',
hobbies: ['eat fish', 'play ball']
}
let smallCat = createCat(Animal)
let bigCat = createCat(Animal)
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini
bigCat.say() // type is Cat name is Nini
复制代码
通过上面代码我们可以很清楚的看到,寄生式继承有原型链继承的缺点和构造函数继承的缺点,也就是说通过寄生式继承创造出来的实例,如果修改了它原型上的引用属性里的内容,其他的实例也会受影响,而且每次创建实例的时候,那些公共的属性和方法都会被创建一次。
6. 寄生组合式继承
上面我们提到了组合式继承是一种还不错的继承实现方式,既能让每个实例拥有继承来的可自定义的属性和方法,也能共享公共的方法和属性。但是这种方法还有能够优化的地方,这个需要优化的点在于,组合式继承时,父类的构造函数会被调用两次,结合代码看一下
function Cat(type, name) {
Animal.call(this, type, name) // 这里调用了一次父类的构造函数
this.age = '1'
}
Cat.prototype = new Animal() // 这里也调用了一次父类的构造函数
Cat.prototype.constructor = Cat
复制代码
实际上,子函数的 prototype
只需要指向那些公共的属性和方法就可以了,不需要指向整个父函数的实例,由于我们把需要继承的公共的属性和方法放在了父函数prototype
上,所以我们可以考虑让子函数的 prototype
间接访问父函数的 prototype
。实现的代码例子如下
// 利用寄生式继承来让子函数的 prototype 能访问到父函数的原型
function createObj(child, parent) {
let prototype = Object.create(parent.prototype)
// 这个对象相比于父实例少了那些子函数已通过parent.call 继承到的属性和方法,仅仅含有一个指向父函数原型的属性
prototype.constructor = child
child.prototype = prototype
}
createObj(Cat, Animal)
复制代码
最后,完整的寄生组合式继承的实现代码如下
function Animal(type, name) {
this.type = type
this.name = name
this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat(type, name) {
Animal.call(this, type, name)
this.age = '1'
}
function createObj(child, parent) {
let prototype = Object.create(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
createObj(Cat, Animal)
let smallCat = new Cat('smallCat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is smallCat name is Nini
let bigCat = new Cat('bigCat', 'Nicole')
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
bigCat.say() // type is bigCat name is Nicole
复制代码
因此寄生组合式继承在吸取了组合式继承的优点上,避免了在子函数的原型上面创建不必要的、多余的属性,而寄生组合式继承也是目前的一种理想的比较好的继承方法的实现。
总结
其实 Javascript 继承的关键点是一定要将私有的属性和方法,公有的属性和方法分别处理,私有的属性和方法需要让每个实例都独有一份,保证数据的更改互不影响,公有的属性和方法需要放在父类的原型上,确保不重复创建。