为了账号安全,请及时绑定邮箱和手机立即绑定

绝对干货!彻底理解Js面向对象程序设计(四)

标签:
JavaScript

原型链

下面的逻辑第一次读会有点绕,多读几遍:
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原
型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每
个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型
对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的
原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数
的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实
例与原型的链条。这就是所谓原型链的基本概念。
实现原型链有一种基本模式,其代码大致如下:

// 仔细阅读下面代码(重点):function SuperType(){ this.property = true;
} 
SuperType.prototype.getSuperValue = function(){ return this.property;
};function SubType(){ this.subproperty = false;
}//继承了 SuperTypeSubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){ return this.subproperty;
};var instance = new SubType();console.log(instance.getSuperValue()); //true

如果下面的看不懂,请重回这个系列前几章重新看一遍
以上代码定义了两个类型:SuperType 和 SubType。每个类型分别有一个属性和一个方法。它们
的主要区别是 SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋给
SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原
来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。在确立了
继承关系之后,我们给 SubType.prototype 添加了一个方法,这样就在继承了 SuperType 的属性和方
法的基础上又添加了一个新方法。这个例子中的实例以及构造函数和原型之间的关系如图 所示:

webp


在上面的代码中,我们没有使用 SubType 默认提供的原型,而是给它换了一个新原型;这个新原型
就是 SuperType 的实例。于是,新原型不仅具有作为一个 SuperType 的实例所拥有的全部属性和方法,
而且其内部还有一个指针,指向了 SuperType 的原型。最终结果就是这样的:instance 指向 SubType 的原型, SubType 的原型又指向 SuperType 的原型。 getSuperValue() 方法仍然还在SuperType.prototype 中,但 property 则位于 SubType.prototype 中。这是因为 property 是一个实例属性,而 getSuperValue()则是一个原型方法。既然 SubType.prototype 现在是 SuperType的实例,那么 property 当然就位于该实例中了。此外,要注意 instance.constructor 现在指向的是 SuperType,这是因为原来 SubType 的原型指向了另一个对象——SuperType 的原型,而这个原型对象的 constructor 属性指向的是 SuperType。通过实现原型链,本质上扩展了本章前面介绍的原型搜索机制。读者大概还记得,当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用instance.getSuperValue()会经历三个搜索步骤:
重点又来了:
1)搜索实例;
2)搜索 SubType.prototype;
3)搜索 SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。


1.别忘记默认原型:

事实上,前面例子中展示的原型链还少一环。我们知道,所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString()、valueOf()等默认方法的根本原因。所以,我们说上面例子展示的原型链中还应该包括另外一个继承层
次。下图为我们展示了该例子中完整的原型链。


webp

如果不知道什么是toString方法,请回本系列前几章复习
一句话,SubType 继承了 SuperType,而 SuperType 继承了 Object。当调用 instance.toString()
时,实际上调用的是保存在 Object.prototype 中的那个方法。

2. 确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用 instanceof 操作符,只要用
这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。以下几行代码就说明了这
一点。

alert(instance instanceof Object); //truealert(instance instanceof SuperType); //truealert(instance instanceof SubType); //true

由于原型链的关系,我们可以说 instance 是 Object、SuperType 或 SubType 中任何一个类型
的实例。因此,测试这三个构造函数的结果都返回了 true。
第二种方式是使用 isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该
原型链所派生的实例的原型,因此 isPrototypeOf()方法也会返回 true,如下所示:

alert(Object.prototype.isPrototypeOf(instance)); //truealert(SuperType.prototype.isPrototypeOf(instance)); //truealert(SubType.prototype.isPrototypeOf(instance)); //true

3. 谨慎地定义方法

子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎
样,给原型添加方法的代码一定要放在替换原型的语句之后。来看下面的例子。

function SuperType(){ this.property = true;
}
SuperType.prototype.getSuperValue = function(){ return this.property;
};function SubType(){ this.subproperty = false;
}//继承了 SuperTypeSubType.prototype = new SuperType();//添加新方法SubType.prototype.getSubValue = function (){ return this.subproperty;
};//重写超类型中的方法SubType.prototype.getSuperValue = function (){ return false;
};const one = new SubType();const two = new SuperType();console.log(one.getSuperValue()); // falseconsole.log(two.getSuperValue()); // true

在以上代码中,第一个方法 getSubValue()被添加到了 SubType中。第二个方法 getSuperValue()是原型链中已经存在的一个方法,但重写这个方法将会屏蔽原来的那个方法。换句话说,当通过 SubType 的实例调用 getSuperValue()时,调用的就是这个重新定义的方法;但通过 SuperType 的实例调用 getSuperValue()时,还会继续调用原来的那个方法。这里要格外注意的是,必须在用 SuperType 的实例替换原型之后,再定义这两个方法。还有一点需要提醒读者,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链,如下面的例子所示:

function SuperType(){ this.property = true;
}
SuperType.prototype.getSuperValue = function(){ return this.property;
};function SubType(){ this.subproperty = false;
}//继承了 SuperTypeSubType.prototype = new SuperType();//使用字面量添加新方法,会导致上一行代码无效SubType.prototype = { getSubValue : function (){ return this.subproperty;
 }, someOtherMethod : function (){ return false;
 }
};var instance = new SubType();
alert(instance.getSuperValue()); //error!

以上代码展示了刚刚把 SuperType 的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个 Object 的实例,而非 SuperType 的实例,因此我们设想中的原型链已经被切断——SubType 和 SuperType 之间已经没有关系了。

4.原型链的问题

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引
用类型值的原型。想必大家还记得,我们前面介绍过包含引用类型值的原型属性会被所有实例共享;而
这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原
型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。

function SuperType(){ this.colors = ["red", "blue", "green"]; 
}function SubType(){
}//继承了 SuperTypeSubType.prototype = new SuperType();var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

这个例子中的 SuperType 构造函数定义了一个 colors 属性,该属性包含一个数组(引用类型值)。
SuperType 的每个实例都会有各自包含自己数组的 colors 属性。当 SubType 通过原型链继承了
SuperType 之后,SubType.prototype 就变成了 SuperType 的一个实例,因此它也拥有了一个它自
己的 colors 属性——就跟专门创建了一个 SubType.prototype.colors 属性一样。但结果是什么
呢?结果是 SubType 的所有实例都会共享这一个 colors 属性。而我们对 instance1.colors 的修改
能够通过 instance2.colors 反映出来,就已经充分证实了这一点。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,
应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上
前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。
下一章我们将介绍以上问题的解决办法。



作者:alex夏夜
链接:https://www.jianshu.com/p/2ca1272d5657


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消