本文是前端学习笔记的第六篇,对应web前端开发JavaScript精英课js的第21课时,本篇主要写关于JS中的四种继承方式,这四种也可以说是整个JS继承的发展史了
目录
JavaScript中的继承发展史
JS继承一共可分为四种,根据不断的发展进化由第一种进化到如今普遍使用的第四种
- 原型链
- 借用构造函数(通过call/apply)
- 共享原型
- 圣杯模式
1. 原型链
通过原型链的方式实现继承,是最初级版的继承,这种继承方式存在一定的弊端,譬如我只想继承某个构造函数的原型对象的一些属性,但却不得不一并继承了属于构造函数自己的一些属性
<script>
Father.prototype.Firstname = "周";
function Father() {
this.englishName = "I am Father";
}
var father = new Father();
Son.prototype = father;
function Son() {
}
var son = new Son();
</script>
son此时只想继承Father的FirstName,但却因为原型链的缘故,不得不继承了原本不需要的属性EnglishName,这显然不是我们所期望的,因此便有了后来的一系列继承方式
2. 借用构造函数(通过call/apply)
通过call/apply函数,我们可以借用别的构造函数来为对象增添属性。先复习一下call和apply的用法,二者都是用于重定义this的指向,区别是前者是通过 函数.call(对象引用,args0,args1...)的方式可以把对象引用取代前面函数中的this的位置进行操作,后者是通过函数.apply(对象引用,args[n])的方式改变函数中的this引用,也即是参数用数组来表示
接下来看下面一个例子
<script>
function Car(weight,height,speed) {
CarFactory.call(this,weight,height,speed);
}
function CarFactory(weight,height,speed) {
this.weight = weight;
this.height = height;
this.speed = speed;
}
var car = new Car('4900','160','1km/s');
</script>
此时在创建对象car时通过借用构造函数CarFactory,为自己添加了属性weight,height,speed,当然准确来说这不能算继承,只是借用别的构造函数为自己增加属性,且这种方式也有缺点,若过多使用会造成编码效率降低,代码冗余
3. 共享原型
在第一第二种继承方式都不理想的情况下,又发展出了第三种继承模式:共享原型
看下面一个例子
<script>
Father.prototype.firstName = "周";
function Father() {
this.englishName = "I am Father";
}
Son.prototype = Father.prototype;
function Son() {
}
var son = new Son();
</script>
共享了Father.prototype与Son.prototype后,此时对象son相比第一种原型链的情况有了很大的改善,不再会继承无关的属性englishName,因为这个属性是属于用构造函数Father创建的对象,而此时并没有用构造函数Father创建对象,只是共享了彼此的原型,也就是只是获得了属性firstName
但是这种做法仍有缺陷,再看下面一个例子
<script>
Father.prototype.firstName = "周";
function Father() {
this.englishName = "I am Father";
}
Son.prototype = Father.prototype;
function Son() {
}
var son = new Son();
Son.prototype.firstName = '陈';
console.log(Father.prototype.firstName); // 陈
</script>
此时试图修改Son的原型对象上的firstName属性,但是因为共享原型的缘故,Father.prototype和Son.prototype的引用是相同的,那么修改其中一个的属性就会修改另外一个对象的属性,这显然不是我们希望看到的,因此,便提出了新的解决方案
4. 圣杯模式
既然修改原型对象会导致共享的原型对象的属性改变,那么是否可以在二者之间建一个中间层隔开一下呢,基于这样的考虑,圣杯模式便诞生了,看下面一个例子
<script>
function inherit(Orign,Target) {
function F () {}
F.prototype = Orign.prototype;
Target.prototype = new F();
Target.prototype.constructor = Target;
Target.prototype.uber = Orign.prototype;
}
function Son() {
}
Father.prototype.firstName = '周';
function Father() {
}
inherit(Father,Son);
var son = new Son();
</script>
原本的共享原型是直接Target.prototype = Orign.prototype,而这里通过一个构造函数F,作为一个中间层,使得Target.prototype修改的用构造函数 f 造出来的对象的属性,而不会干预到Father.prototype的属性值,这里再提提另外两行特别的代码
- Target.prototype.constructor = Target
- Target.prototype.uber = Orign
第一行是修改原型对象的构造器,我们知道,对象本身是没有constructor属性的,这个属性在对象的原型对象身上的,而这里son的原型对象是用构造函数F造出来的对象,本身系统默认给的prototype,其身上同样没有constructor,继续在其原型对象身上找,也就是Father.prototype身上找,终于找到了constructor属性,而constructor属性表示的是对象的构造函数,因此如果没有这句,那么son的constructor便是Father.prototype身上的constructor,修改后,便变为了自己,更符合实际情况
第二行是为Son的原型对象新增一个uber属性,值为Orign.prototype,因为此时已改变了构造器,此时不再知道其共享的到底是哪个原型对象,因此便新增一个属性用于找回这个原型对象
当然,还可以更进一步,把圣杯模式写的更加简洁、结构清晰
<script>
var inherit = (function () {
var f = function () {}
return function (Orign,Target) {
f.prototype = Orign.prototype;
Target.prototype = new f();
Target.prototype.constructor = Target;
Target.prototype.uber = Orign.prototype;
}
}());
function Son() {
}
Father.prototype.firstName = '周';
function Father() {
}
inherit(Father,Son);
var son = new Son();
</script>
把整个共享原型的过程以立即执行函数的方式表现,代码可读型大大提高,代码复用率也高,最重要的是,避免了对全局变量的污染,推荐平时如果需要用继承就用这种做法