Javascript是一种基于原型的对象语言,而不是我们比较熟悉的,像C#语言基于类的面向对象的语言。在前一篇文章中,我们已经介绍了Javascript中对象定义的创建。接下来我们来介绍一下Javascript对象的继承。我们通过一个例子来介绍对象继承实现的几种方法。
比如,现在有一个“水果”的构造函数:
function Fruit(color) { this.color = color; this.whatColor = function () { console.log("This fruit color is " + this.color + "."); } this.contain = function () { console.log("Which vitamins it contains.") } }
另外”苹果“的构造函数:
function Apple(variety, color) { this.variety = variety; this.color = color; this.whatVariety = function () { console.log("It is " + this.variety + "."); }; }
那么在Javascript中如何让”苹果“继承”水果“呢?
- 构造函数的继承
在前面的文章中,我们介绍过函数对象的方法call和apply,它们都是用来调用某个函数,并用方法中指定的对象来替换所调用方法中this。正是基于这一点,我们可以考虑在Apple的构造函数中调用函数Fruit,让Apple对象来替换Fruit中的this,这样就让Apple对象有了Fruit的contain方法,Apple本身的属性color也可以不用在本身的构造函数中定义了。
function Apple(variety, color) { Fruit.call(this, color); this.variety = variety; this.whatVariety = function () { console.log("It is " + this.variety + "."); } } var myApple = new Apple("红富士", "red"); myApple.whatColor(); //This fruit color is red. myApple.contain(); //Which vitamins it contains. myApple.whatVariety(); //It is 红富士.
可以看出Apply对象除了拥有自己本省的属性外,还拥有Fruit对象的属性和方法。但是如果Fruit对象原型有一个属性type定义如下
Fruit.prototype.type = "fruit";
这时候如果还利用上面的方法继承,我们就会发现在myApple里面没有定义属性type
alert(typeof(myApple.type)); // undefined
- 原型(prototype)模式继承
我们知道每个对象都有一个prototype,这也是Javascript的特点。
如果让Apple的原型指向Fruilt对象的一个实例,那么Apple对象就能有Fruit的所有属性和方法了
function Apple(variety, color) { Fruit.call(this, color); this.variety = variety; this.whatVariety = function () { console.log("It is " + this.variety + "."); } } Apple.prototype = new Fruit(); Apple.prototype.constructor = Apple; var myApple = new Apple("红富士", "red"); myApple.whatColor(); // This fruit color is red. myApple.contain(); //Which vitamins it contains. alert(myApple.type); // fruit
每个对象原型都有一个constructor属性指向对象的构造函数,每一个对象实例也有一个constructor属性,默认调用原型的constructor。如果没有 Apple.prototype.constructor = Apple; ,那么会因为 Apple.prototype = new Fruit(); 让Apple.prototype.constructor指向了Fruit,这显然会导致继承链的紊乱,因此我们必须手动纠正,将Apple.prototype对象的constructor值改为Apple。这是很重要的一点,编程时务必要遵守。我们在Javascript中应该遵循这一点,即如果替换了prototype对象,必须要将属性constructor指回原来的构造函数(对象原本的构造函数)。
上面这种方法看上去没有什么问题,但是有一个缺点:定义新对象时需要创建一个被继承对象的实例,有时候在一做法需要消耗一定内存,效率不高。如果直接把Fruit.prototype赋给Apple.prototype的话,那就不需要创建对象实例了,
function Fruit(color) { this.color = color; this.whatColor = function () { console.log("This fruit color is " + this.color + "."); } this.contain = function () { console.log("Which vitamins it contains.") } } Fruit.prototype.type = "fruit"; function Apple(variety, color) { Fruit.call(this, color); this.variety = variety; this.whatVariety = function () { console.log("It is " + this.variety + "."); } } Apple.prototype = Fruit.prototype; // it may let the constructor of Apple be Fruit. Apple.prototype.constructor = Apple; // assign the constructor back to itself var myApple = new Apple("红富士", "red"); alert(typeof (myApple.type)); // fruit
这样Apple对象继承了父对象Fruit的所有属性和方法, 而且效率高,比较省内存。但是缺点是Apple对象的原型和Fruilt对象的原型是同一个对象,如果我们修改其中的一个,势必就会影响到另外一个。
所以代码 Apple.prototype.constructor = Apple; ,虽然让修正了Apple构造函数的问题,但是Fruit对象的构造函数又有问题。
alert(Fruit.prototype.constructor === Apple); // true
结合原型模式继承的这两种方式的优缺点和问题,我们有了另外一种继承方法。
- 利用空对象作为中介来继承
function Fruit(color) { this.color = color; this.whatColor = function () { console.log("This fruit color is " + this.color + "."); } this.contain = function () { console.log("Which vitamins it contains.") } } Fruit.prototype.type = "fruit"; function Apple(variety, color) { Fruit.call(this, color); this.variety = variety; this.whatVariety = function () { console.log("It is " + this.variety + "."); } } function Empty() { } Empty.prototype = Fruit.prototype; Apple.prototype = new Empty(); Apple.prototype.constructor = Apple;
Empty对象是空对象,几乎不占内存,而且如果修改Apple.prototype不会影响到Fruit的prototye对象。在有的javascript库通常将上面的这种方法封装成一个函数:
function extend(child, parent) { var e = function () { }; e.prototype = parent.prototype; child.prototype = new e(); child.prototype = child; }
大家在使用这种方式的时候千万不要忘记在子对象的构造函数中调用父对象的构造函数来初始化父对象中属性和方法,不至于每个新创建的子对象实例中父对象属性都是默认值。如果上面的例子中移除
Fruit.call(this, color);
之后,
var myApple = new Apple("HFS", "red"); myApple.whatColor(); // This fruit color is undefined.
虽然我们在创建Apple实例的时候传了“red”值进去,但是取出来的却是undefined,为什么呢?问题就出现在 Apple.prototype = new Empty(); 上,这时候Apple里面没有定义color属性,所以即使有值传进构造函数中,获取到color属性还是未定义的。加上父对象构造函数调用的代码后,就让子对象拥有了父对象的方法和属性,并利用传入的值对它们进行初始化。
- 拷贝继承
接下来我们介绍另外一种方式,纯粹采用"拷贝"方法实现继承。简单说,通过把父对象实例的所有属性和方法,拷贝进子对象来实现继承。
function Fruit(color) { this.color = color; this.whatColor = function () { console.log("This fruit color is " + this.color + "."); } this.contain = function () { console.log("Which vitamins it contains.") } } Fruit.prototype.type = "fruit"; Function.prototype.extendEx = function (parent) { for (var p in parent) { this.prototype[p] = parent[p]; } } function Apple(variety, color) { Fruit.call(this, color); this.variety = variety; this.whatVariety = function () { console.log("It is " + this.variety + "."); } } Apple.extendEx(new Fruit()); var myApple = new Apple("HFS", "red"); myApple.whatColor(); // This fruit color is red. myApple.contain(); //Which vitamins it contains. alert(myApple.type); // fruit