了解物件導向程式設計的基本理論、其與 JavaScript (幾乎所有東西都是物件) 之間的關係、應如何寫出建構子與物件實體。
一、從古代說起
在 1994 年有家公司叫 Netscape (網景) 發佈了 Navigator 瀏覽器,但是,這個版本的瀏覽器只能用來瀏覽,不具備跟使用者互動的能力。因此,Netscape 很快了解到 Web 需要變得更動態才行 (具備與使用者互動的能力),急需一種指令搞語言,接著在 1995 年 Netscape 聘用了 Brendan Eich,就在 5 月花了 10 天開發出了這個原型。在開發當時稱為 LiveScript,之後為了搭上 Java 語言的名氣,而改為 JavaScript。
1994 年正是物件導向程式最興盛的時期,C++ 是當時最流行的語言,而 Java 語言的 1.0 版本也即將於第二年推出。 Brendan Eich 很顯然地受到了影響,在 JavaScript 裡面所有的的資料類型都是物件,這一點與 Java 非常接近。接著,很快地他就遇到了一個難題,到底要不要設計繼承機制呢 !?
二、Brendan Eich 的選擇:
如果真的是一種很簡易的指令搞語言,其實不需要有繼承機制。但是,在 JavaScript 裡面都是物件,就必須需要一種機制,將所有物件聯繫起來。最終,Brendan Eich 還是設計了繼承。
但是,這時侯他不打算引入 class (類別) 的概念,因為一但有了class (類別),JavaScript 就是一種完整的物件導向程式語言了,這又有點太過於正式,而且也會增加了初學者的入門難度。
他考慮到,C++ 和 Java 語言都是使用 new 命令,來生成 instance (實體 / 實例)。
C++ 的寫法是:
ClassName *object = new ClassName(param);
Java 的寫法是:
Foo foo = new Foo();
最後,他就把 new 命令引入到 JavaScript,用來從原型物件生成一個實體物件。但是,JavaScript 沒有 class (類別),哪要怎樣表示原型對象呢 !? 這時,他想到 C++ 和 Java 使用 new 命令時,都會使用 class (類別) 的 constructor function (構造函式)。他就做了一個簡化的設計,在 JavaScript 語言中,new 命令後面跟的不是 class (類別),而是 constructor function (構造函式)。
從 ES6 起最大的改變就是物件導向語法。在 ES6 中導入的 class 語法,使得整個程式外觀改變。為了配合這本書當時的時空背景,這次會以 ES6 之前的語法來做說明與介紹,後半段再來介紹 ES6 中導入的 class 語法。
JavaScript 中使用一種名為 constructor function 的函式來定義物件和它們的特色,透過 constructor function 可以根據你的需要用更有效率的方式來產生許多物件,而這些物件都已經包含你所定義好的屬性和方法。
// 未來有空再改寫
If you don’t understand prototypes, you don’t understand JavaScript.
如果你沒搞懂原型,你不算真的懂 JavaScript。
JavaScript 是以原型為基礎的物件導向設計,一直到 ES6 標準制定後仍沒變動過。在 ES6 新增的 class 定義方式,也只是原型物件導向語法的語法糖,骨子裡還是原型,並不是真正的以 class 為基礎的物件導向設計。
原型:以某物件為基礎的物件,JavaScript 中使用原型 (取代類別) 建立新的物件,此種性質的物件導向就稱為以原型為基礎 (Prototype-based) 的物件導向。
正式開始說明如何使用 JavaScript constructor function 之前,先來看看,如果要設計衣服是如何進行的呢 !?首先,會有一個衣服的設計圖,在這裡定義了顏色及尺寸,接著就會根據設計圖製作出實際的衣服,每件衣服都是同樣的款式,但會擁有自己的顏色及尺寸,每件衣服都會別上一個名牌。
根據上圖的範例,如果要使用 JavaScript 告訴電腦衣服的設計圖呢 !?
function Clothes(color, size, country) {
this.color = color;
this.size = size;
this.country = country;
this.whichCountry = function () {
console.log('Made in ' + this.country);
}
}
這裡有幾點妳我必須知道:
const c1 = new Clothes('red', 'XL', 'Taiwan');
const c2 = new Clothes('blue', 'L', 'Japan');
const c3 = new Clothes('black', 'M', 'Korea');
console.log(c1);
console.log(c2);
console.log(c3);
語法:new
運算子
var 變數名稱 = new 物件名稱([參數,...])
new :創造一個空的物件。/ 告訴記憶體,你要開一個空間給我,我要創造一個新的物件。
整個上述的程式整理如下:
function Clothes(color, size, country) {
this.color = color;
this.size = size;
this.country = country;
this.whichCountry = function () {
console.log('Made in ' + this.country);
}
}
const c1 = new Clothes('red', 'XL', 'Taiwan');
const c2 = new Clothes('blue', 'L', 'Japan');
const c3 = new Clothes('black', 'M', 'Korea');
console.log(c1);
console.log(c2);
console.log(c3);
問題:
this
指的是什麼 !?new
運算子,會發生什麼事 !?根據上述的程式碼,對照下圖妳我可以了解原型與物件的區別,以及new
、=
等語法的使用:
在上面的內容提到,若要定義 instance 共用方法,哪就需要在 constructor function 中定義方法,但是,在 constructor function 新增方法會隨著方法數量增多而浪費記憶體的問題。
什麼意思呢 !? 來看範例。
function Clothes(color, size, country) {
this.color = color;
this.size = size;
this.country = country;
this.whichCountry = function () {
console.log('Made in ' + this.country);
}
}
const c1 = new Clothes('red', 'XL', 'Taiwan');
const c2 = new Clothes('blue', 'L', 'Japan');
console.log(c1.whichCountry === c2.whichCountry)
根據上述的程式碼,對照下圖妳我可以了解 Constructor function 問題的所在:
繼續上述未講完的故事:
三、Constructor function 的問題
由於上述的問題產生,透過 constructor function 生成的 instance,有一個缺點,就是無法共享方法。
四、prototype 屬性的引入
考慮到了這一點,Brendan Eich 決定為 constructor function 設置一個 prototype 屬性。這個 prototype 是一個物件,所有 instance 需要共享的方法,就放入這個物件裡面 ; 哪些不需要共享的屬性,就放入 constructor function 裡。
因此,一但 instance 被創建之後,將會自動引用 prototype 物件的方法。也就是說,instance 物件的屬性和方法分成兩種,一種是本身自己的,另一種就是引用的。
接著,上述的問題,現在使用 prototype 屬性進行改寫,請看以下程式碼。
function Clothes(color, size, country) {
this.color = color
this.size = size
this.country = country
}
Clothes.prototype.whichCountry = function () {
console.log('Made in ' + this.country)
}
const c1 = new Clothes('red', 'XL', 'Taiwan')
const c2 = new Clothes('blue', 'L', 'Japan')
console.log(c1.whichCountry === c2.whichCountry)
現在,whichCountry
這個方法是放在 prototype 物件裡面,是這兩個 c1 及 c2 instance 共享的。只要修改了 prototype 物件的whichCountry
方法,就會同時影響到所產生的這兩個 instance。
五、總結
由於所有的 instance 物件共享者同一個 prototype 物件,哪也就是說,prototype 物件就好像是 instance 物件的原型,而 instance 物件就好像繼承了 prototype 物件一樣。
物件導向程式語言很重要的一個概念是就是繼承,所謂繼承就是保有原本物件的功能並用以定義新類別的功能。
直接來看範例程式:
function Clothes(color, size, country) {
this.color = color
this.size = size
this.country = country
}
Clothes.prototype.whichCountry = function () {
console.log('Made in ' + this.country)
}
const c1 = new Clothes('red', 'XL', 'Taiwan')
c1.whichCountry()
跟據上述的程式碼,要思考的方向有幾點。
Clothes.prototype
增加了whichCountry
這個 function。new
運算子新增了一個 instance,並且將值賦予給 c1 這個變數。c1.whichCountry()
呼叫到whichCountry
這個 function。c1.whichCountry()
可以正常執行 !? 是不是就代表c1
與Clothes.prototype
透過某種方式,將這兩者連接起來。不然的話,JavaScript 的引擎怎麼會知道whichCountry()
這個方法要來Clothes.prototype
這裡找。__proto__
。**這個屬性就是暗示說如果在 c1 身上找不到 whichCountry 的話,哪應該就要去哪裡找。這個概念其實會跟上去我講到的scope chain
有點像。console.log(c1.__proto__)
。其實就是一個物件,內容長這樣。console.log(c1.__proto__ === Clothes.prototype)
,會得到 true。c1.whichCountry()
的時候,運作流程:
c1.__proto__
身上有沒有 whichCountry ?c1.__proto__ = Clothes.prototype
,這是為什麼呢 !? 這個就是new
幫我們做的事情,後面有時間的話,再來講到。Clothes.prototype
有沒有這個 whichCountry,哪結果是有,哪就是說他找到了。這裡的this
指的就是這個 c1。哪問題來了,如果還是找不到呢 !?
c1.__proto__.__proto__
身上有沒有 whichCountry ?console.log(c1.__proto__.__proto__ === Object.prototype)
,會得到 true。哪問題又來了,根據上面的程序,如果還是找不到呢 !?
c1.__proto__.__proto__.__proto__
身上有沒有 whichCountry ?console.log(c1.__proto__.__proto__.__proto__)
會得到 null。在 ES6 新增了 class 指令,對定義 JavaScript 的類別也變得更簡單及直覺,請看以下範例:
class Clothes {
constructor(color, size, country) {
this.color = color
this.size = size
this.country = country
}
whichCountry() {
console.log('Made in ' + this.country)
}
}
const c1 = new Clothes('red', 'XL', 'Taiwan')
c1.whichCountry()
class 指令的語法如下。
class 類別名稱{
...建構子定義...
...屬性定義...
...方法定義...
}
class Clothes {
constructor(color, size, country) {
this.color = color
this.size = size
this.country = country
}
whichCountry() {
console.log('Made in ' + this.country)
}
}