总所周知,继承是所有OO语言中都拥有的一个共性。在JavaScript中,它的继承机制与其他OO语言有着很大的不同,尽管ES6为我们提供了像面向对象继承一样的语法糖,但是其底层依然是构造函数,所以理解继承的底层原理非常重要,所以今天让我们来探讨一下JavaScript中的继承机制。
原型与原型链要理解继承,必须理解JavaScript中的原型与原型链,我在之前的上一篇文章对原型进行了深入的探讨,有兴趣的小伙伴可以看看~
《理解原型与原型链》
继承在JavaScript中,有六种主要常见的继承方式,下面我会对每一种继承方式进行分析并总结它们的优缺点。
1.原型链继承 原型链继承的概念在JavaScript中,实现继承主要是依靠原型链来实现的。其基本思想是是利用原型让一个引用类型继承另一个引用类型的属性和方法。
让我们简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象prototype,原型对象都包含一个指向构造函数的指针constructor,而实例都包含一个指向原型对象的内部指针__proto__
假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?让我们来看下面这段代码。
function Father() { this.name = 'zhang'; } Father.prototype.sayName = function() { console.log(this.name); } function Son() { this.age = 18; } // 继承了Father Son.prototype = new Father(); Son.prototype.sayAge = function() { console.log(this.age); } const xiaoming = new Son(); console.log(xiaoming.sayName()) // 'zhang'以上代码,Son继承了Father,而继承是通过创建Father的实例,并将Son.prototype指向new出来的Father实例。实现的本质是重写了原型对象,待之是一个新类型的实例,也就是说,原来存在于Father构造函数中的所有属性和方法,现在也存在于Son.prototype中。
通过上图可知,我们没有使用Son默认提供的原型,而是给它换了一个新原型,这个原型就是Father的实例,其内部还有一个指针,指向Father的原型。由于Son的原型被重写了,所以xiaoming这个实例的constructor属性现在指向的是Father。一句话总结就是Son继承了Father,而Father继承Object,当调用xiaoming.toString()方法时,实际上是调用Object.prototype中的toString方法。
注意:给子类原型添加方法的代码一定要放到替换原型的语句之后
还有一点需要提醒各位小伙伴们,在使用原型链继承时,千万不能使用对象字面量创建原型方法,因为这样做会重写原型链,来看下面这段代码。
function Father() { this.name = 'zhang'; } Father.prototype.sayName = function() { console.log(this.name); } function Son() { this.age = 18; } // 继承了Father Son.prototype = new Father(); Son.prototype = { sayAge: function() { console.log(this.age) } } const xiaoming = new Son(); console.log(xiaoming.sayName()) // '报错'使用对象字面量创建原型方法,会切断Father与Son之间的继承关系哦~
原型链继承的优点子类型的实例对象拥有超类型的全部属性和方法。
原型链继承的缺点我在上面的那篇文章提到过,包含引用类型值的原型属性会被所有实例共享。在通过原型实现继承时,原型实际上会变成另一个类型的实例,原先的实例属性也就顺理成章地变成了现在的原型属性了。
function Father() { this.cars = ['奔驰', '宝马', '兰博基尼']; } Father.prototype.sayName = function() { console.log(this.name); } function Son() { this.age = 18; } // 继承了Father Son.prototype = new Father(); const xiaoming = new Son(); xiaoming.cars.push('五菱宏光'); console.log(xiaoming.cars); //'奔驰, 宝马, 兰博基尼, 五菱宏光' const xiaohong = new Son(); console.log(xiaohong.cars); //'奔驰, 宝马, 兰博基尼, 五菱宏光'可以从上述代码中发现,当Father中的属性是引用类型的时候,当然Father的每个实例都会有各自的数组cars属性。当Son继承Father之后,Son.prototype就变成了Father的一个实例,结果就是xiaoming和xiaohong两个实例对象共享一个cars属性,这是在继承中我们不希望出现的。