最近工作一直在用nodejs做開發,有了nodejs,前端、後端、腳本全都可以用javascript搞定,很是友善。但是javascript的很多文法,比如對象,就和我們常用的面向對象的程式設計語言不同;看某個javascript開源項目,也經常會看到使用this關鍵字,而這個this關鍵字在javascript中因上下文不同而意義不同;還有讓人奇怪的原型鍊。這些零零碎碎的東西加起來就很容易讓人不知所措,是以,有必要對javascript這門語言進行一下深入了解。
我這篇文章主要想說說如何在javascript中進行面向對象的程式設計,同時會講一些javascript這門語言在設計之初的理念。下面讓我們開始吧。
其中值得一提的是,為什麼繼承借鑒了self語言的原型機制而不是java的類機制?首先我們要知道:
- self的原型機制是靠運作時的語義
- java的類機制是靠編譯時的類文法
javascript1.0的功能相對簡單,為了在今後不斷豐富javascript本身功能的同時保持舊代碼的相容性,javascript通過改變運作時的支援來增加新功能,而不是通過修改javascript的文法,這就保證了舊代碼的相容性。這也就是javascript選擇基于運作時的原型機制的原因。
在javascript中,除了數字、字元串、布爾值(true/false)、undefined這幾個簡單類型外,其他的都是對象。
數字、字元串、布爾值這些簡單類型都是不可變量,對象是可變的鍵值對的集合(mutable keyed conllections),對象包括數組<code>array</code>、正規表達式<code>regexp</code>、函數<code>function</code>,當然對象<code>object</code>也是對象。
對象在javascript中說白了就是<code>一系列的鍵值對</code>。鍵可以是任何字元串,包括空串;值可以是除了undefined以外的任何值。在javascript中是沒有類的概念(class-free)的,但是它有一個原型鍊(prototype linkage)。javascript對象通過這個鍊來實作繼承關系。
javascript中的每種類型的對象都可以采用<code>字面量(literal)</code>的方式建立。
對于object對象,可以使用<code>對象字面量(object literal)</code>來建立,例如:
當然,也可以用<code>new object()</code>或<code>object.create()</code>的方式來建立對象。
對于<code>function</code>、<code>array</code>對象都有其相應的字面量形式,後面會講到,這裡不再贅述。
javascript中的每個對象都隐式含有一個<code>[[prototype]]</code>屬性,這是ecmascript中的記法,目前各大浏覽器廠商在實作自己的javascript解釋器時,采用的記法是<code>__proto__</code>,也就是說每個對象都隐式包含一個<code>__proto__</code>屬性。舉個例子:
foo這個對象在記憶體中的存儲結構大緻是這樣的:
當有多個對象時,通過<code>__proto__</code>屬性就能夠形成一條原型鍊。看下面的例子:
上面的代碼在聲明對象b、c時,指明了它們的原型為對象a(a的原型預設指向object.prototye,object.prototype這個對象的原型指向null),這幾個對象在記憶體中的結構大緻是這樣的:
除了我們這裡說的<code>__proto__</code>屬性,相信大家平常更常見的是<code>prototype</code>屬性。比如,date對象中沒有加幾天的函數,那麼我們可以這麼做:
那麼以後所有的date對象都擁有<code>adddays</code>方法了(後面講解繼承是會解釋為什麼)。那麼<code>__proto__</code>屬性與<code>prototype</code>屬性有什麼差別呢?
javascript的每個對象都有<code>__proto__</code>屬性,但是隻有<code>函數對象</code>有<code>prototype</code>屬性。
那麼在函數對象中, 這兩個屬性的有什麼差別呢?
1. <code>__proto__</code>表示該函數對象的原型
2. <code>prototype</code>表示使用new來執行該函數時(這種函數一般成為構造函數,後面會講解),新建立的對象的原型。例如:
var d = new date();
d.proto === date.prototype; //這裡為true
看到這裡,希望大家能夠了解這兩個屬性的差別了。
在javascript,原型和函數是最重要的兩個概念,上面說完了原型,下面說說函數對象。
首先,函數在javascript中無非也是個對象,可以作為value指派給某個變量,唯一不同的是函數能夠被執行。
使用對象字面量方式建立的對象的<code>__proto__</code>屬性指向<code>object.prototype</code>(<code>object.prototype</code>的<code>__proto__</code>屬性指向<code>null</code>);使用函數字面量建立的對象的<code>__proto__</code>屬性指向<code>function.prototype</code>(<code>function.prototype</code>對象的<code>__proto__</code>屬性指向<code>object.prototype</code>)。
函數對象除了<code>__proto__</code>這個隐式屬性外,還有兩個隐式的屬性:
1. 函數的上下文(function’s context)
2. 實作函數的代碼(the code that implements the function’s behavior)
和對象字面量一樣,我們可以使用<code>函數字面量(function literal)</code>來建立函數。類似于下面的方式:
一個函數字面量有四個部分:
1. function關鍵字,必選項。
2. 函數名,可選項。上面的示例中就省略了函數名。
3. 由圓括号括起來的一系列參數,必選項。
4. 由花括号括起來的一系列語句,必選項。該函數執行時将會執行這些語句。
一個函數在被調用時,除了聲明的參數外,還會隐式傳遞兩個額外的參數:<code>this</code>與<code>arguments</code>。
this在oop中很重要,this的值随着調用方式的不同而不同。javascript中共有四種調用方式:
1. method invocation pattern。當函數作為某對象一個屬性調用時,this指向這個對象。this指派過程發生在函數調用時(也就是運作時),這叫做late binding
2. function invocation pattern。當函數不作為屬性調用時,this指向全局對象,這是個設計上的錯誤,正确的話,内部函數的this應該指向外部函數。可以通過在函數中定義一個變量來解決這個問題。
construct invocation pattern。javascript是一門原型繼承語言,這也就意味着對象可以直接從其他對象中繼承屬性,沒有類的概念。這和java中的繼承不一樣。但是javascript提供了一種類似與java建立對象的文法。當一個函數用new來調用時,this指向新建立的對象。這時的函數通常稱為構造函數。
需要注意的是,這裡的arguments不是一個數組,它隻是一個有length屬性的類數組對象(array-like),它并不擁有數組的其他方法。
關于對象,最後說一下數組,javascript中的數組和平常程式設計中的數組不大一樣。
數組是一種在記憶體中線性配置設定的資料結構,通過下标計算出元素偏移量,進而取出元素。數組應該是一個快速存取的資料結構,但是在javascript中,數組不具備這種特性。
數組在javascript中一個具有傳統數組特性的對象,這種對象能夠把數組下标轉為字元串,然後把這個字元串作為對象的key,最後對取出對應該key的value(這又一次說明了對象在javascript中就是一系列鍵值對)。
javascript也為數組提供了很友善的<code>字面量(array literal)</code>定義方式:
通過數組字面量建立的數組對象的<code>__proto__</code>指向array.prototype。
在java中,對象是某個類的執行個體,一個類可以從另一個類中繼承。但是在基于原型鍊的javascript中,對象可以直接從另一個對象建立。
在上面講解對象時,我們知道了在建立一個對象時,該對象會自動賦予一個<code>__proto__</code>屬性,使用各種類型的<code>字面量(literal)</code>時,javascript解釋器自動為<code>__proto__</code>進行了指派。當我們在javascript執行使用new操作符建立對象時,javascript解釋器在構造函數時,同時會執行類似于下面的語句
新建立的對象都會有一個<code>__proto__</code>屬性,這個屬性有一個<code>constructor</code>屬性,并且這個屬性指向這個新對象。舉個例子:
如果new不是一個操作符,而是一個函數的話,它的實作類似于下面的代碼:
之前也說了,基于原型的繼承機制是根據運作時的語義決定的,這就給我們提供了很大的便利。比如,我們想為所有的array添加一個map函數,那麼我們可以這麼做:
因為所有的數組對象的<code>__proto__</code>都指向array.prototype對象,是以我們為這個對象增加方法,那麼所有的數組對象就都擁有了這個方法。
javascript解釋器會順着原型鍊檢視某個方法或屬性。如果想檢視某對象的是否有某個屬性,可以使用<code>object.prototype.hasownproperty</code>方法。
<a href="http://dmitrysoshnikov.com/ecmascript/javascript-the-core/">javascript. the core</a>