Javascript中的对象继承和原型链
后端开发做过n年的朋友们,学Javascript时比较头大的地方就是它的面向对象。严格的说,Javascript(在ES6出现之前)本身并非是个面向对象的语言。当然也有不少文章说JS是面向对象的,我也同意,因为它虽然没有class类,但是可以通过其它方法实现对象的重用,封装,继承。多态就更不用讲了,本来就是脚本语言,运行时才解释执行的。
之前在介绍立即执行函数时有提到过对象如何封装私有化的成员,本篇就要介绍怎么实现对象的继承,同时引出JS里面一个重要的概念-原型。
构造函数
Javascript构造函数在声明上同普通函数完全一样。从命名习惯上来说,我们要将构造函数的首字母大写。在构造函数中,一般做的是对成员变量的初始化,这些成员变量无需事先声明。同时函数也不用返回值。
function Ship() {
}
function Vehicle(wheels) {
this.wheels = wheels;
this.speed = 0;
}
上面的例子中我们有两个构造函数,一个是Ship
,里面不做任何事情。另一个是Vehicle
,它需要一个参数wheels
,并在函数里初始化两个成员变量wheels
和speed
。我们可以用new
操作符来构造对象:
var myShip = new Ship();
var myCar = new Vechile(4);
var myBike = new Vechile(2);
console.log(myBike.speed); // print 0
现在对象myCar
和myBike
都是由Vehicle
的构造的,都拥有各自的wheels
和speed
变量。注意,这里的变量对外是可见的。
原型
原型是构造函数的一个属性,它有那么点像类,可以描述当前对象是什么类型,同时又有点像Java中的反射,可以动态的改变类中的成员。我们可以通过原型给当前的构造函数添加成员,拿上面Vehicle
的例子:
Vehicle.prototype.move = function() {
this.speed = 20;
}
之后,所有Vehicle
对象都将拥有move()
成员函数。但是要注意,在这段代码执行之前调用move()
方法的话,程序会报错,因为此时move()
方法还未添加到Vehicle的原型中去。
myBike.move(); // TypeError
Vehicle.prototype.move = function() {
this.speed = 20;
}
myBike.move();
console.log(myBike.speed); // print 20
一般的开发习惯是将成员变量在构造函数里初始化,而成员函数是通过原型注册进去的,这样的代码可读性好。更重要的是,调用构造函数时,里面的内容都要重新初始化,而我们并不希望每次都重新初始化函数(节省内存空间)。当然我也见过不少项目不是这样的,毕竟JS语言太灵活了,你的项目要尽量遵从统一的方式。注意,所有的函数都有原型,但new
出来的对象是没有原型的。比如myBike.prototype
就是undefined
。
我们在控制台将原型打印出来看看console.log(Vehicle.prototype)
再说一个有趣的属性,就是constructor
,构造函数原型的constructor
属性就是构造函数自己:
console.log(Vehicle.prototype.constructor == Vehicle); // true
console.log(myBike.constructor == Vehicle.prototype.constructor); // true
继承
我们已经有了Vehicle
构造函数来创建车辆对象,现在我想要有一个汽车的构造函数,它拥有车辆的所有成员,并且也有自己特殊的成员。乍一看,就知道要用继承了,怎么做?让我们先看例子:
function Auto(seats) {
this.seats = seats;
}
Auto.prototype = new Vehicle(4);
我们声明了一个新的构造函数Auto
来构造汽车对象,然后将Auto
的原型赋上new Vehicle(4)
。这样Auto
就继承了Vehicle
了,而且它的wheels
属性会自动初始化为4
。运行下例子试试:
var myCar = new Auto(5);
myCar.move(); // Call Vehicle.prototype.move
console.log(myCar.speed);
果然,myCar
对象拥有了Vehicle
所有的成员,同时它拥有自己的成员变量seats
。我们同样也可以在Auto
的原型上注册成员函数,这样函数只属于Auto
,而不影响Vehicle
。如果将Auto
的原型在控制台上打印出来,就是new Vehicle(4)
的对象。
我们再定义两个构造函数:
function Bicycle() {
}
Bicycle.prototype = new Vehicle(2);
Bicycle.prototype.lock = function() {
this.speed = 0;
}
function Car() {
}
Car.prototype = new Auto(5);
Car.prototype.move = function() {
this.speed = 40;
}
Bicycle
继承了Vehicle
,并将其成员wheels
初始化为2
,同时Bicycle
注册了自己的成员函数lock()
,所有Bycicle
对象都可以调用(在注册之后),而Vehicle
对象无法调用。另外,我们创建了Car
继承了Auto
,并将其成员seats
初始化为5
。Car
也注册了move()
函数,同Vehicle
的成员函数一样的名字。我们来做几个试验:
myBike = new Bicycle();
myBike.lock(); // Set speed to 0
console.log(myBike.speed); // print 0
myVehicle = new Vehicle();
myVehicle.lock(); // TypeError
myVehicle.move(); // Set speed to 20
console.log(myVehicle.speed); // print 20
myCar = new Car();
myCar.lock(); // TypeError
myCar.move(); // Set speed to 40
console.log(myCar.speed); // print 40
不出我们所料,Bicycle
中注册的lock()
方法,不影响Vehicle
,也不影响从Auto
继承过来的Car
。而Car
中的move()
方法覆盖了Vehicle
中的move()
方法。虽然有些奇怪,但是有没有觉得同其他语言class继承的效果非常相似啊。
原型链
最后一部分,我们聊下原型链。我们将上面例子中Car
的原型在控制台打印出来,并将其__proto__
属性展开看看
大家有没有发现规律,似乎这个__proto__
属性就是指向当前对象所继承的构造函数的原型,当我们有多层继承的时候,__proto__
属性可以不停地展开,直到遇到Object
函数原型。不错,这个__proto__
所展示的就是上一部分介绍的继承关系,对于多层继承,__proto__
属性可以不断追溯,就像一个链表,所以这就称为”原型链“。我们看下例子:
myCar = new Car();
console.log(myCar.__proto__ == Car.prototype); // true
console.log(Car.prototype.__proto__ == Auto.prototype); // true
console.log(myCar.__proto__.__proto__ == Auto.prototype); // true
console.log(myCar.__proto__.__proto__.__proto__ == Vehicle.prototype); // true
当你调用A.prototype = new B()
,就是等于将A.prototype.__proto__
属性赋值为B.prototype
。而当你想使用A
构造出来的对象的成员时,比如上面例子中myCar.move()
,Javascript解释器会先搜索Car
的原型上有没有注册这个move()
方法,有就调用,没有就搜索其__proto__
属性所指向的原型上有没有注册该方法,要是还没有,那继续搜索__proto__.__proto__
,一直找到Object
函数为止。
关于prototype
和__proto__
是有点搞,我一开始也看的一头雾水。网上摘个图,显示其之间的关系:
是不是看晕了?还是看我写的文字好。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
。没想通?自己动手写写例子吧!