本文假設讀者熟悉C/C++語言, 如果你不熟悉, 那麼你可以忽略C/C++部分的論述, 只看JavaScript的部分就可以了, 這篇文章是筆者學習JavaScript語言時候的一些知識點.
JavaScript給筆者的印象一直是物件導向, 一切皆是對象, 包括函數. 我們可以給方便的給對象賦一個函數值, 於是它就成為了一個函數, 可以被呼叫執行. 但是, 事實上, 函數不過是一個指標, JavaScript對象只不過能夠接受一個函數指標, 這是C/C++語言也具有的特性. 一般來說, 函數在記憶體裡面只有一個拷貝, 即使對JavaScript來說也是如此.
JavaScript一切都是對象, 事實上, 指令碼語言的變數無類型是一種假象, C/C++也有variant類型, 可以接受各種類型的賦值, JavaScript只不過語言預置了這種類型, 這個類型可以儲存各種不同類型的資料. 事實表明, 即使放棄C++自由的自訂類型方式, 我們仍然能夠做同樣的事情, JavaScript就是如此. 使用一個或幾個預製的類型, 我們完全可以構造各種資料結構,
因為即使是C++的類定義, 最終的類型還是那幾種預定義類型, 複雜類型是簡單類型的組合而已. JavaScript把預製類型減少到很少, 字串,數字,函數,對象, 而且你一般沒有必要就不用管它是哪種類型, 最重要的是, 你不用像C/C++那樣因為不同類型的變數不能互相賦值而造成不方便, JavaScript能完成大多數類型的自動轉換.
JavaScript的變數無需聲明就可以使用, 但是其實這是個誤解, 任何指令碼語言的變數都是需要聲明的, 與其說不用聲明, 準確的說是利用指派陳述式來聲明一個變數. 而且很多時候, 我們確實需要聲明一個變數, 但是留待後面再賦值, 所以沒有聲明關鍵字的指令碼語言, 使用起來多少有些不便, 你可能不得不給一個變數賦一個不必要的值, 來聲明它. 而既沒聲明, 也沒賦值的變數, 如果直接使用, 都會提示未定義的語法錯誤.
重要提示: JavaScript使用var關鍵字聲明一個變數, 局部如果使用了一個沒有用var聲明的變數, 它必須不能和全域變數同名, 否則它是那個全域變數而不是一個新的局部變數. 這很正常, C/C++也是這樣, 但是要命的是JavaScript不需要變數聲明, 程式員可能就是想要一個局部變數, 但是恰好和全域變數同名了. C/C++這種錯誤是不會出現的,
因為你使用一個局部變數必須先聲明它. 當然了, 另一個規則, 導致程式員幾乎必須給函數體內的變數加上var聲明, 因為一個沒有用var聲明的變數, 即使它只在一個函數體內部出現過, 實際上它是一個全域變數, 在任何地方都可以訪問. 如果你不希望函數內的變數被別處使用, 還是加上var關鍵字, 否則很多函數你不知道哪個變數可能名稱相同, 而被認為是同一個變數, 在函數執行過程中, 產生各種奇怪的問題(調了個函數, 我這的某個變數怎麼就變了? 答案是被調的函數內部可能有一個同名變數, 也沒有用var聲明).
JavaScript是動態指令碼語言, 這句話真正的意思是: JavaScript的變數只有執行到那裡才會被建立, 這和C/C++的聲明是有區別的, 除了new的對象之外, C/C++的聲明過的變數對象都是開始就在記憶體中存在的, 包括局部變數, 當然它們可能沒有被初始化, 只不過預留了記憶體空間, 局部變數甚至會在堆棧中共用記憶體空間. 也有例外, JavaScript的函數如果是用function
funcname(){}這樣的方式聲明的, 那麼在執行任何語句前, 這些函數對象已經被建立. 但是僅限這種方式, 如果用 funcname = function(){}這種方式定義一個函數, 則必須執行到這一句, 函數才會被建立.
JavaScript有一個重要的基本類型Object, 它是所有物件類型的基礎類型, 但一個實值型別的變數不是一個Object . 給一個變數賦值可以使用如下的方式:
//整數var i = 0;//字串var str = "abc";//數組var a = ["a","b","c"];//對象var obj = {};//給對象賦初始值var obj = {name:"myObject";func:function(){alert("func call");}};
到現在, 你只能使用指派陳述式, 即"="號初始化一個變數. 這沒有什麼問題, 但是某些時候有些不方便, C/C++的new關鍵字以及類的成員函數調用, 都提供了方便, 很多時候C可以類比C++的類成員函數調用, 你只要在全域儲存一個對象指標, 函數調用的時候, 給這個指標賦一個特定的值, 那麼所有函數的調用也就是針對這對象的, 實際上C++也就是這麼實現的, 只不過它把這些東西自動化了, C++使用ecx儲存this指標,
相當於每個成員函數多了一個隱形的參數, 你只要寫對象的成員函數調用, 對象指標就會自動傳給那個函數.
JavaScript也提供了這種便利, JavaScript的核心就是函數調用, 一切都是函數, 我們無需什麼類. JavaScript也有this機制, 這個this是誰呢? 誰呼叫這個函數就是誰, 比如: obj.func(); func函數內部的this就是這個obj. 如果不是這麼呼叫呢? 直接定義一個函數func然後呼叫它, 那麼此時, func內部的this就是一個預設值, 在網頁上它是全域的window對象;
在其它平台, 也會對應某個對象, 或者null. 也就是說, 一個函數如果不是寫給某個對象的成員, 而是直接調用的, 就不要濫用this, 因為那個this是全域預設的一個對象, 如果你真要訪問它也用不著使用this關鍵字. 總之函數內部的this是依賴誰調用的.
JavaScript的new JavaScript一般使用這樣的語句建立一個對象: var obj = new MyObject(); 這裡MyObject是一個函數. 這個語句做兩件事, 建立一個Object對象, 然後用這個對象調用MyObject函數, 也就是MyObject被呼叫的時候, 內部this訪問到的是這個建立的對象,
並且函數執行完後, 返回這個建立的對象. 我們會發現, 即使不用new關鍵字, 如果我們定義MyObject函數在內部建立一個Object對象, 並且對它進行某些操作, 然後返回這個值, 這樣:var obj = MyObject(); 效果是相同的. 系統提供的函數比如Array,Object都可以用new或者不用new來調用, 作用是相同的, 但是這僅限於系統函數, 我們自己定義的函數, 這兩種方式的調用, 效果是不同的. 注意, 如果不打算用new來建立對象, 函數就應該自己建立它, 而且不用使用關鍵字this.
這兩者函數的寫法是不同的.
因為我們可以任意的給 Object 對象添加屬性, 實際上一個對象可以看成一個類. 只不過這個類的定義是動態, 而不是像C++那樣必須預定義再執行個體化. 其實C++也可以寫一個類, 然後提供給這個類動態添加資料和函數(指標)的方法, 只不過訪問方式不能用"."或"->", 而是某個函數. 還有一點, 我們希望建立的這個對象, 一次就初始化好, 而不是先建立Object, 再調用一個函數初始化它.
new 關鍵字就能達到這個效果. 先定義一個函數, 把對象需要初始化的代碼寫在這個函數裡(用this訪問將來要建立的對象), 然後只要這樣寫: var obj = new MyObject(param...); 就完成了obj的建立和初始化, 已經非常"像"C++的類了.
實值型別和物件類型的區別 不僅僅是規定誰是實值型別, 誰是物件類型, 實值型別和物件類型有分類的標準. 我們可以認為 JavaScript 的每一個變數都是指標, 指向一個對象或者不指向任何對象(此時它的值為null), 事實是不是這樣, 並不是非常的重要, 我們不去管底層如何?, 我們只需要知道這種邏輯, 事實上, 你也可以把null變數視為指向一個null對象.
那麼, 實值型別的特點在於, 它指向的是一個常數對象. 什麼是常數對象呢? 通俗的說, 常數對象是一種沒有屬性的對象. JavaScript 這門語言只提供改變一個對象的屬性的操作, 如果一個對象沒有屬性, 我們就無法改變它的值. 注意, 這裡說的是對象而不是變數, 變數僅僅是一個指標, 給它賦值, 就是讓它指向一個新的對象. 而 Object 就是有屬性的對象.
實際上, 我們有必要區分變數和變數指向的對象這種兩種概念, 因為你不能給實值型別的對象賦予或改變屬性, 所以你唯一能做的就是引用這對象或者把變數重新指向一個對象, 這會造成實值型別看起來就像自身擁有那個值一樣. 字串是實值型別的對象, 所以你永遠無法更改一個字串, 你能做的就是建立新字串. 還有一點, 一個物件類型就意味著它沒有值, 它的全部內容都在屬性裡. 但是 JavaScript
的內建對象還有一些額外的東西, 比如函數能夠執行, 它必然有 Object 之外的內部結構, 但是這些額外的內容, 我們用屬性是訪問不到的. 除此之外, 記住, 變數都是相同的, 所謂的實值型別變數和物件類型變數, 是指它們指向的對象是何種類型. 所以 JavaScript 從來也沒有複製拷貝的概念, 除非顯示的調用一個函數, 任何給變數賦值的操作都是簡單的把變數指向一個新的對象(這裡把置 null 值理解成指向 null 對象), 而沒有其它更複雜的操作.
記住: JavaScript 變數從來都不擁有任何值, 所有 JavaScript 變數都是一個簡單的指標, 當我們使用 "=" 給一個變數賦值的時候, 是讓這個變數指標指向一個新的對象, 而不是改變它原來指向對象的內容.
還有繼承 JavaScript是如何完成繼承功能的呢? 所謂繼承, 是一個寫好的類(對JavaScript是一個函數), 我們希望再定義一個類, 這個類具有我們想繼承的類的函數和資料成員. JavaScript為了滿足這個需求, 引進了prototype屬性,這個屬性是函數對象特有的,
Object對象沒有, 即使添加了也沒有意義. 雖然函數也是對象(Object), 但是函數確實有兩個方面和Object不同, 一個是可以調用, 再一個是 new 操作的時候 函數的prototype 屬性成為繼承的參考屬性. 每個函數都有預設的prototype屬性, 但是初始值就是一個空的Object, prototype屬性就是一個普通的對象, 凡是對象都可以設定為一個函數的prototype, 但是數實值型別比如數字和字串不行, 因為數實值型別只有值, 沒有屬性, 而 prototype
只關心它的屬性, 不關心它的值, 值對於prototype沒有任何意義. JavaScript對象的屬性其實就是C++對象的成員, 使用函數和 new 建立一個JavaScript對象的時候, 這個函數的prototype的屬性(成員), 自動成為新建立對象的屬性. 比如下面的代碼:
function A(){this.Name = "A";this.Call = function(){alert("A Call");}}function B(){this.Value = 0;this.Func = function(){alert("B Func");}}B.prototype = new A();var b = new B();for(var i in b){alert(">"+i+":"+b[i]);}
運行代碼, 我們可以看到b有4個屬性,Value,Func,Name,Call. 繼承的本質是 b 自動添加 B.prototype 具有的屬性, 這裡添加屬性僅僅是添加了變數, 也就是b內部有4個變數, 但是這些變數指向的對象不是新的, 要麼是B函數添加給它的, 要麼是 B.prototype 相同名稱的屬性指向的那個對象. 如果B函數建立的屬性恰好和
B.prototype 的某個屬性同名, 那麼你通過 (b.屬性名稱) 訪問到的是B添加的屬性. 當然, 這僅僅是看起來是這樣, 事實上, 真正的操作, 是 b 對象僅僅儲存了一個 B.prototype 的指標, 並沒有建立那些屬性, 也就是說, b "記得" B.prototype , 當你訪問一個屬性時, 如果b自身沒有, 解譯器就到它儲存的這個指標指向的對象裡去找, 找到後就認為 b "有"這個屬性. 如果我們給 b 的這個屬性賦值, 那麼, 這個時候才會真的建立這個屬性, 當然, 這種賦值僅僅是改變了
b 自身這個屬性的指向, 而 B.prototype 裡的屬性指向和指向的對象都沒有變化. 之所以說, b "記得" B.prototype 是因為, 即使 b 已經建立完成, 我們改變了 B.prototype ,仍然會"感知", 也就是 b 的那些繼承屬性, 如果沒被重寫的話, 也會跟著變化, 當然會這樣.
原型鏈 實際上, 每個 Object 都儲存著一個 prototype 指標, 但是這個指標我們訪問不到, 是一個隱形屬性. 既然這個指標指向一個 Object 對象, 那麼那個 Object對象, 也會有一個自己的 prototype 指標, 直到最終, 這個指標為空白. 事實上, 最原始的 new Object() 對象的這個
prototype 指標就是 null. 處於這個鏈上的任何一個prototype 指標具有的屬性, 都可以作為 它上級對象的屬性被讀取, 但是如果寫入的話, 會建立一個屬性, 而不會改變原型上的屬性. ( 在某些瀏覽器裡, 這個隱藏的 prototype 屬性是可以通過 __proto__ 屬性名稱來訪問)
(上面的提過 new Object() 的原型是 null , 這其實是我自己的想當然, 因為原型鏈必須終止於 null . 但是事實上, 使用 new 建立的任何對象的原型 __proto__ 並不是 null (還好這個變數能訪問), 而是一個內部 Object , 之所以稱為內部 Object, 是因為它是一個系統內建的 Object
對象, 全域唯一. 它唯一不同於使用者自建立 Object 的地方就在於它的 __proto__ 是 null. 我們藉助 __proto__ 可以訪問到這個變數, 但是我們不能刪除它, JavaScript 沒有強制移除對象的功能, 但是我們能改變它, 比如給它添加屬性, 結果就是使所 JavaScript 有對象在原型鏈的作用下, 都具備了這個屬性, 這也是為什麼某些瀏覽器不提供 __proto__ 屬性訪問的原因)