大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现,本章介绍 JavaScript 的原型链继承。
JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。
下面,先看怎么为对象指定原型。JavaScript 规定,每个函数都有一个prototype属性,指向一个对象。
function f() {}
typeof f.prototype // "object"
上面代码中,函数f默认具有prototype属性,指向一个对象。
对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。注意 如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
原型链
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。
如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOf和toString方法的原因,因为这是从Object.prototype继承的。
那么,Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null。
constructor 属性
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
function P() {}
P.prototype.constructor === P // true
constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。语法:object instanceof constructor (表示对象是否为某个构造函数的实例。)
另一方面,有了constructor属性,就可以从一个实例对象新建另一个实例。
function Constr() {}
var x = new Constr();
var y = new x.constructor();
y instanceof Constr // true
这使得在实例方法中,调用自身的构造函数成为可能。
Constr.prototype.createCopy = function () {
return new this.constructor(); //this===Constr.prototype
};
如果不能确定constructor属性是什么函数,还有一个办法:通过name属性,从实例得到构造函数的名称。
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"
注意,instanceof运算符只能用于对象,不适用原始类型的值。
instanceof
instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。
var s = 'hello';
s instanceof String // false
上面代码中,字符串不是String对象的实例(因为字符串不是对象),所以返回false。
构造函数的继承
让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现。第一步是在子类的构造函数中,调用父类的构造函数。
function Sub(value) {
Super.call(this);
this.prop = value;
}
上面代码中,Sub是子类的构造函数,this是子类的实例。在实例上调用父类的构造函数Super,就会让子类实例具有父类实例的属性。
第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
上面代码中,Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。
另外一种写法是Sub.prototype等于一个父类实例。
Sub.prototype = new Super();
上面这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法。
举例来说,下面是一个Shape构造函数。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
我们需要让Rectangle构造函数继承Shape。
// 第一步,子类继承父类的实例
function Rectangle() {
Shape.call(this); // 调用父类构造函数
}
// 另一种写法
function Rectangle() {
this.base = Shape;
this.base();
}
// 第二步,子类继承父类的原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
采用这样的写法以后,instanceof运算符会对子类和父类的构造函数,都返回true。
var rect = new Rectangle();
rect instanceof Rectangle // true
rect instanceof Shape // true
上面代码中,子类是整体继承父类。有时只需要单个方法的继承,这时可以采用下面的写法。
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
}
上面代码中,子类B的print方法先调用父类A的print方法,再部署自己的代码。这就等于继承了父类A的print方法。