2013年4月13日 星期六

[JS] javascript OO設計 - 繼承

接觸javascript有一定的人應該都知道在js中,你所看到的幾乎都是物件。
函數是物件的第一型,Function和Object都是物件,也是函數的實例,而所有的function都是Function的instance。

關於原形(prototype)

prototype是函數物件特有的屬性,使用函數建構出來的物件會藉由原型鏈(prototype chain)而存取得到建構函數的prototype的屬性,用文字說清楚很難,不如我們直接看範例:
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.nation = "Taiwan";
var kevin = new Person("Kevin", "18");
var laura = new Person("Laura", "17");
alert(kevin.nation); // "Taiwan"
alert(laura.nation); // "Taiwan"
雖然kevin和laura本身並沒有被定義"nation"這個屬性,但是當使用這些不存在的屬性時,會找他們的建構函數中的prototype,再沒有的話還會看這個prototype物件的建構函數中有沒有這個屬性,直到最高層,也就是Object建構函數的prototype,這就是所謂的prototype chain。為了證明這點,我們再次修改一下上面這個例子:
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.nation = "Taiwan";
Function.prototype.whatTypeAmI = "Funciton";
Object.prototype.whatTypeAmI = "Object";
var kevin = new Person("Kevin", "18");
alert(kevin.nation); // "Taiwan"
alert(kevin.whatTypeAmI); // "Object"
alert(Person.whatTypeAmI); // "Function"

從上面可以看出雖然kevin這個物件沒有whatTypeAmI這個屬性,但是他會往上找Person.prototype物件有沒有這個屬性,再找不到就會找Person.prototype的建構函式,也就是預設的Object函式,這時我們會在他的prototype中找到whatTypeAmI這個屬性是"Object"。
而我們在呼叫Person的whatTypeAmI時也會發生找不到的情形,這時就會往上找到他的建構函數也就是Funciton的prototype,如範例所示,這時回傳的是"Function"。


利用prototype來實踐javascript繼承模式

我們可以利用上面所提到的prototype chain來實踐多層祖孫繼承的模式,我們用範例來解釋一下這點:
// 哺乳綱
function Mammals() {
    this.blood = "warm";
}

// 靈長目
function Primate() {
    this.tail = true;
    this.skin = "hairy";
}
Primate.prototype = new Mammals();

// 人科
function Homo() {
    this.skin = "smooth";
}
Homo.prototype = new Primate();

var human = new Homo();
human.name = "Kevin";

alert(human.name); // "Kevin", from self
alert(human.skin); // "smooth", from Homo
alert(human.tail); // "true", from Primate
alert(human.blood); // "warm", from Mammals
上例應該很清楚的顯示出以原型實踐繼承模式的原理了。


原型設計模式的漏洞

雖然原型設計模式能夠很方便的實踐物件間的共用屬性及繼承模式,但是在操作上要對prototype chain有一定的了解,再加上細心的邏輯驗證,才不會出現如下的錯誤:
function Human() {}
Human.prototype.blood = "red";
Human.prototype.body = ["foot","hand"];

var john = new Human();
var kevin = new Human();

john.blood = "purple";
john.body.push("wing");

alert(kevin.blood); // "red"
alert(john.blood); // "purple"

alert(kevin.body.toString()); // "foot, hand, wing"
alert(kevin.body.toString()); // "foot, hand, wing"
從上面的例子可以看到,john因為不明原因而突變以後,不只血變成紫色的,也長出翅膀來了!但是在john突變之後,kevin的血雖然沒有變色,但是卻莫名其妙長出了翅膀。很明顯的,我們不小心改動到了Human的prototype。
原來在我們為john的blood指定顏色時,javascript會為john這個物件增加一個屬於自己的"blood"屬性,這種情況就跟為物件增加屬性的方式一樣。於是在後來的呼叫時,會先找到john自己的blood屬性。但要john的body屬性執行push函式時,會發生在john中找不到body的狀況,於是就往上找到了Human.prototype的body屬性,並由他來執行push函式,此時改動到的便是Human.prototype.body了,也就連帶的影響到了無辜的kevin。

其他繼承模式設計

javascript是個很活的語言,除了prototype的實踐方式以外,我們也可以使用別的方式來實現繼承:
// 哺乳綱
function Mammals() {
    this.blood = "warm";
}

// 靈長目
function Primate() {
    Mammals.call(this); // 記得放前面,不然會蓋掉重複的屬性
    this.tail = true;
    this.skin = "hairy";
}
Primate.prototype = new Mammals();

// 人科
function Homo() {
    Primate.call(this); // 記得放前面,不然會蓋掉重複的屬性
    this.skin = "smooth";
}

var human = new Homo();
human.name = "Kevin";

alert(human.name); // "Kevin", from self
alert(human.skin); // "smooth", from Homo
alert(human.tail); // "true", from Primate
alert(human.blood); // "warm", from Mammals
這種方式是將父類別的建構函式放在子類別的建構函式中以「this」的身分來執行,為自己建置父類別的屬性。這樣的作法有個好處,就是不會因為不當的操作,改動到別的物件的屬性,但是相對的也失去了共用屬性的便利性。

這種方式也能讓我們很方便的實作多重繼承,只要在子類別的建構函數中呼叫多個父類別的建構函式即可。