JavaScript 的奇妙世界!

對於很多新手設計師來說,JavaScript 的繼承機制是比較難以理解的,多是用背誦的方式記憶其特性,但只要理解 JavaScript 的設計思想後就會發現根本沒有想像中這麼難!

1994年

Netscape 公司發布了 Navigator 瀏覽器 0.9 版。這是歷史上第一個比較成熟的網絡瀏覽器,轟動一時。

然而這個版本的瀏覽器只能用來瀏覽,並不具備與使用者互動的能力。比如網頁上有一欄「用戶名」要求填寫,瀏覽器就無法判斷使用者是否真的填寫了,只能讓伺服器端判斷,如果沒有填寫,伺服器端就返回錯誤,要求使用者重新填寫,這太浪費時間和伺服器資源了。

因此 Netscape 公司急需一種網頁腳本語言,使得瀏覽器可以與使用者互動,而工程師 Brendan Eich 負責開發這種新語言。他覺得,沒必要設計得很複雜,這種語言只要能夠完成一些簡單操作就夠了,比如判斷使用者有沒有填寫表單。

當年正是物件導向程式設計(object-oriented programming)最興盛的時期,C++ 是當時最流行的語言,而 Java 的 1.0 版即將於隔年推出,Brendan Eich 無疑受到了影響,JavaScript 裡面所有的數據類型都是物件(object),這一點與Java 非常相似。

但是,他隨即就遇到了一個難題,到底要不要設計「繼承」機制呢?

選擇

如果真的是一種簡易的腳本語言,其實不需要有「繼承」機制。但是 JavaScript 裡面都是物件,必須有一種機制將所有物件聯繫起來,所以 Brendan Eich 最後還是設計了「繼承」。

但是,他不打算引入「類別(class)」的概念,因為一旦有了「類別 」,JavaScript 就是一種完整的物件導向語言了,這好像有點太正式了,而且增加了初學者的入門難度。

他考慮到 C++ 和 Java 都使用 new 命令生成「物件實體(instance) 」,因此他就把 new 命令引入了 JavaScript,用來從原型物件生成一個物件實體。

C++ 的寫法:
ClassName *object = new ClassName(param);
Java 的寫法:
ClassName object = new ClassName();

但是,JavaScript 沒有「類別」,要怎麼表示原型物件呢?

這時他想到 C++ 和 Java 使用 new 命令時,都會調用「類別」的「建構函數/構造函數(constructor)」,因此就做了一個簡化的設計:在 JavaScript 中,new 命令後面跟的不是類別,而是建構函數!

舉例來說,現在有一個叫做 Dog 的建構函數,表示狗物件的原型。

function Dog(name){
this.name = name;
}
(建構函數中的「this」關鍵字,代表了新創建的物件實體。)

對這個建構函數使用 new,就會生成一個狗物件的物件實體。

var doggy = new Dog('小白');
alert(doggy.name); // 小白

new 的缺點

用建構函數來生成物件實體有一個缺點,那就是無法共享屬性和方法。比如在 Dog 物件的建構函數中設置一個物件實體的共有屬性 species。

function Dog(name){
this.name = name;
this.species = '犬科';
}

然後生成兩個物件實體。

var doggyA = new Dog('小白');
var doggyB = new Dog('小黑');

這兩個物件實體的 species 屬性是獨立的,隨意修改其中一個不會影響另外一個。

doggyA.species = '貓科';
alert(doggyB.species); // 犬科

每一個物件實體都有自己的屬性和方法的副本,這不僅無法做到數據共享,也是極大的資源浪費!

prototype

考慮到這一點,Brendan Eich 決定為建構函數設置一個 prototype 屬性。

「這個屬性包含一個物件(以下簡稱「prototype 物件」)」,所有物件實體需要共享的屬性和方法,都放在這個 prototype 物件內;而那些不需要共享的屬性和方法,就放在建構函數里面。

物件實體一旦創建,將自動引用 prototype 物件的屬性和方法。也就是說,該物件實體的屬性和方法分成兩種:一種是本地的(建構函數的),另一種是引用的(prototype 的)。

以 Dog 建構函數為例,現在用 prototype 屬性進行改寫:

function Dog(name){
this.name = name;
}

Dog.prototype = {
species: '犬科'
};

var doggyA = new Dog('小白');
var doggyB = new Dog('小黑');

alert(doggyA.species); // 犬科
alert(doggyB.species); // 犬科

由於現在 species 屬性被放在 prototype 物件裡,是兩個物件實體所共享的,因此只要修改 prototype 物件,就會同時影響到兩個物件實體:

Dog.prototype.species = '貓科';

alert(doggyA.species); // 貓科
alert(doggyB.species); // 貓科

總結

由於所有的物件實體共享同一個 prototype 物件,那麼從外界看起來,prototype 物件就好像是物件實體的原型,而物件實體則好像「繼承」了 prototype 物件一樣。

這就是 JavaScript 繼承機制的設計思想。


最後感謝阮一峰前輩的系列文章對我的啟發,本文也是摘錄整理自阮一峰前輩的其中一篇,有興趣的朋友可以去裡面挖寶,連結在下方:

http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html

發表迴響