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

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

构造函数

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

function Ship() {
}

function Vehicle(wheels) {
    this.wheels = wheels;
    this.speed = 0;
}

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

var myShip = new Ship();
var myCar = new Vechile(4);
var myBike = new Vechile(2);
console.log(myBike.speed);   // print 0

现在对象myCarmyBike都是由Vehicle的构造的,都拥有各自的wheelsspeed变量。注意,这里的变量对外是可见的。

原型

原型是构造函数的一个属性,它有那么点像类,可以描述当前对象是什么类型,同时又有点像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)

Print 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)的对象。

Prototype Auto

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

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初始化为5Car也注册了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__属性展开看看

Prototype Car

大家有没有发现规律,似乎这个__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__是有点搞,我一开始也看的一头雾水。网上摘个图,显示其之间的关系:

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。没想通?自己动手写写例子吧!