本文介紹的是,對象之間的"繼承"的五種方法。
比如,現在有一個"動物"對象的構造函數:
function Animal() {
this.species = "動物";
}
還有一個"貓"對象的構造函數:
function Cat(name, color) {
this.name = name;
this.color = color;
}
怎樣才能使"貓"繼承"動物"呢?
一、 構造函數綁定
第一種方法也是最簡單的方法,使用 call
或 apply
方法,將父對象的構造函數綁定在子對象上,即在子對象構造函數中加一行:
function Cat(name, color) {
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛", "黃色");
cat1.species; // 動物
二、 prototype 模式
第二種方法更常見,使用 prototype
屬性。
如果"貓"的 prototype
對象,指向一個 Animal
的實例,那么所有"貓"的實例,就能繼承 Animal
了:
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛", "黃色");
cat1.species; // 動物
代碼的第一行,我們將 Cat
的 prototype
對象指向一個 Animal
的實例,相當于完全刪除了 prototype
對象原先的值,然后賦予一個新值。
Cat.prototype = new Animal();
第二行又是什么意思呢?
任何一個 prototype
對象都有一個 constructor
屬性,指向它的構造函數。一個普通函數的構造函數就是其自身,因此 prototype.constructor
的值就是該函數本身;而一個用關鍵字 new
創建的新對象的 constructor
屬性會指向創建它的函數。
假設我們只使用第一行代碼:
Cat.prototype = new Animal();
這時候 Cat
的構造函數變成了 Animal
Cat.prototype.constructor == Animal; // true
更重要的是,每一個實例也有一個 constructor
屬性,默認調用 prototype
對象的 constructor
屬性:
cat1.constructor == Cat.prototype.constructor; // true
可以看到 cat1 的構造函數也變成了 Animal
,這顯然會導致繼承鏈的紊亂(cat1 明明是用構造函數 Cat
生成的),因此我們必須手動糾正,將 Cat.prototype
對象的 constructor
值改為 Cat
。這就是第二行的意思。
這是很重要的一點,編寫 javascript 時切記遵守。即如果替換了 prototype
對象,那么,下一步必然是為新的 prototype
對象加上 constructor
屬性,并將這個屬性指回原來的構造函數:
foo.prototype = {};
foo.prototype.constructor = foo;
三、 直接繼承 prototype
第三種方法是對第二種方法的改進。由于 Animal 對象中,不變的屬性都可以直接寫入 Animal.prototype
。所以,我們也可以讓 Cat()
跳過 Animal()
,直接繼承 Animal.prototype
。
現在,我們先將 Animal
對象改寫:
function Animal(){}
Animal.prototype.species = "動物";
然后,將 Cat
的 prototype
對象指向 Animal
的 prototype
對象,這樣就完成了繼承:
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛", "黃色");
cat1.species; // 動物
與前一種方法相比,這樣做的優點是效率比較高(不用執行和建立 Animal
的實例了),比較省內存。
缺點是 Cat.prototype
和 Animal.prototype
現在指向了同一個對象,那么任何對 Cat.prototype
的修改,都會反映到 Animal.prototype
。所以,上面這一段代碼其實是 有問題的。
因為當執行了:
Cat.prototype = Animal.prototype;
之后,Animal
和 Cat
共享了同一個 prototype
對象,對 Cat.prototype.constructor
的改動也會改變 Animal.prototype.constructor
。
所以在執行:
Cat.prototype.constructor = Cat;
之后,Animal.prototype
對象的 constructor
屬性也就變成了 Cat
。
Animal.prototype.constructor == Cat; // true
四、 利用空對象作為中介
由于直接繼承 prototype
存在上述的缺點,所以就有第四種方法,利用一個空對象作為中介:
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
F
是空對象,所以幾乎不占內存。這時,修改 Cat
的 prototype
對象,就不會影響到 Animal
的 prototype
對象:
Animal.prototype.constructor == Animal; // true
我們將上面的方法,封裝成一個函數,便于使用:
function Animal(){ }
Animal.prototype.species = "動物";
function Cat(name,color) {
this.name = name;
this.color = color;
}
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
使用的時候,方法如下:
extend(Cat,Animal);
var cat1 = new Cat("大毛", "黃色");
cat1.species; // 動物
另外,說明一點,函數體最后一行,意思是為子對象設一個 uber
屬性,這個屬性直接指向父對象的 prototype
屬性。(uber 是一個德語詞,意思是"向上"、"上一層"。)這等于在子對象上打開一條通道,可以直接調用父對象的方法。這一行放在這里,只是為了實現繼承的完備性,純屬備用性質。
五、 拷貝繼承
上面是采用 prototype
對象,實現繼承。我們也可以換一種思路,純粹采用"拷貝"方法實現繼承。簡單說,如果把父對象的所有屬性和方法,拷貝進子對象,不也能夠實現繼承嗎?
首先,還是把 Animal
的所有不變屬性,都放到它的 prototype
對象上:
function Animal(){}
Animal.prototype.species = "動物";
然后,再寫一個函數,實現屬性拷貝的目的:
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
這個函數的作用,就是將 Parent
對象的 prototype
對象中的屬性,一一拷貝給 Child
對象的 prototype
對象。
使用的時候,這樣寫:
extend2(Cat, Animal);
var cat1 = new Cat("大毛", "黃色");
cat1.species; // 動物