Javascript中的对象继承和原型链

后端开发做过n年的朋友们,学Javascript时比较头大的地方就是它的面向对象。严格的说,Javascript(在ES6出现之前)本身并非是个面向对象的语言。当然也有不少文章说JS是面向对象的,我也同意,因为它虽然没有class类,但是可以通过其它方法实现对象的重用,封装,继承。多态就更不用讲了,本来就是脚本语言,运行时才解释执行的。

之前在介绍立即执行函数时有提到过对象如何封装私有化的成员,本篇就要介绍怎么实现对象的继承,同时引出JS里面一个重要的概念-原型。

构造函数

Javascript构造函数在声明上同普通函数完全一样。从命名习惯上来说,我们要将构造函数的首字母大写。在构造函数中,一般做的是对成员变量的初始化,这些成员变量无需事先声明。同时函数也不用返回值。

上面的例子中我们有两个构造函数,一个是Ship,里面不做任何事情。另一个是Vehicle,它需要一个参数”wheels”,并在函数里初始化两个成员变量”wheels”和”speed”。我们可以用”new“操作符来构造对象:

现在对象”myCar”和”myBike”都是由Vehicle的构造的,都拥有各自的”wheels”和”speed”变量。注意,这里的变量对外是可见的。

原型

原型是构造函数的一个属性,它有那么点像类,可以描述当前对象是什么类型,同时又有点像Java中的反射,可以动态的改变类中的成员。我们可以通过原型给当前的构造函数添加成员,拿上面Vehicle的例子:

之后,所有Vehicle对象都将拥有”move()”成员函数。但是要注意,在这段代码执行之前调用”move()”方法的话,程序会报错,因为此时”move()”方法还未添加到Vehicle的原型中去。

一般的开发习惯是将成员变量在构造函数里初始化,而成员函数是通过原型注册进去的,这样的代码可读性好。更重要的是,调用构造函数时,里面的内容都要重新初始化,而我们并不希望每次都重新初始化函数(节省内存空间)。当然我也见过不少项目不是这样的,毕竟JS语言太灵活了,你的项目要尽量遵从统一的方式。注意,所有的函数都有原型,但”new”出来的对象是没有原型的。比如”myBike.prototype”就是”undefined”。

我们在控制台将原型打印出来看看”console.log(Vehicle.prototype)”
Print Prototype

再说一个有趣的属性,就是”constructor”,构造函数原型的”constructor”属性就是构造函数自己:

继承

我们已经有了Vehicle构造函数来创建车辆对象,现在我想要有一个汽车的构造函数,它拥有车辆的所有成员,并且也有自己特殊的成员。乍一看,就知道要用继承了,怎么做?让我们先看例子:

我们声明了一个新的构造函数Auto来构造汽车对象,然后将Auto的原型赋上”new Vehicle(4)”。这样Auto就继承了Vehicle了,而且它的”wheels”属性会自动初始化为4。运行下例子试试:

果然,myCar对象拥有了Vehicle所有的成员,同时它拥有自己的成员变量”seats”。我们同样也可以在Auto的原型上注册成员函数,这样函数只属于Auto,而不影响Vehicle。如果将Auto的原型在控制台上打印出来,就是”new Vehicle(4)”的对象。
Prototype Auto

我们再定义两个构造函数:

Bicycle继承了Vehicle,并将其成员”wheels”初始化为2,同时Bicycle注册了自己的成员函数”lock()”,所有Bycicle对象都可以调用(在注册之后),而Vehicle对象无法调用。另外,我们创建了Car继承了Auto,并将其成员”seats”初始化为5。Car也注册了”move()”函数,同Vehicle的成员函数一样的名字。我们来做几个试验:

不出我们所料,Bicycle中注册的”lock()”方法,不影响Vehicle,也不影响从Auto继承过来的Car。而Car中的”move()”方法覆盖了Vehicle中的”move()”方法。虽然有些奇怪,但是有没有觉得同其他语言class继承的效果非常相似啊。

原型链

最后一部分,我们聊下原型链。我们将上面例子中Car的原型在控制台打印出来,并将其”__proto__”属性展开看看
Prototype Car
大家有没有发现规律,似乎这个”__proto__”属性就是指向当前对象所继承的构造函数的原型,当我们有多层继承的时候,”__proto__”属性可以不停地展开,直到遇到Object函数原型。不错,这个”__proto__”所展示的就是上一部分介绍的继承关系,对于多层继承,”__proto__”属性可以不断追溯,就像一个链表,所以这就称为原型链。我们看下例子:

当你调用”A.prototype = new B()”,就是等于将”A.prototype.__proto__”属性赋值为”B.prototype”。而当你想使用”A”构造出来的对象的成员时,比如上面例子中”myCar.move()”,Javascript解释器会先搜索Car的原型上有没有注册这个”move()”方法,有就调用,没有就搜索其”__proto__”属性所指向的原型上有没有注册该方法,要是还没有,那继续搜索”__proto__.__proto__”,一直找到Object函数为止。

关于”prototype”和”__proto__”是有点搞,我一开始也看的一头雾水。网上摘个图,显示其之间的关系:
JS Object Layout
是不是看晕了?还是看我写的文字好。”prototype”就是一个函数的原型,里面记录着这个函数所注册的成员,它是没有继承链的,而且变量是没有”prototype”的。而”__proto__”就是揭示原型的继承关系,也就是原型之间的”is-a”关系。JS也会根据”__proto__”来寻找当前对象所继承的原型链上的成员。

这里给大家出个考题,我们知道上面例子中”Car.prototype.__proto__”是”Auto.prototype”,朋友们肯定不耐烦的说,当然啦,”Car is a Auto”嘛。那”Car.__proto__”是什么呢?

不卖关子了,上面那张复杂的图其实已经揭晓答案了。”Car() is a Function”对吧,所以”Car.__proto__”就是”Function.prototype”。没想通?自己动手写写例子吧!

《Javascript中的对象继承和原型链》有1个想法

  1. 博主我看的书上说new出来的对象没有原型这个是错的,你换成new Array() 你就会发现 他的原型是 Array.prototype

发表评论

电子邮件地址不会被公开。 必填项已用*标注