論JavaScript和C/C++語言的相通之處

本文假設讀者熟悉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__ 屬性訪問的原因)



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章