JavaScript的變量、作用域和內存問題

基本類型和引用類型的值

  • 引用類型的值是保存在內存中的對象。與其他語言不同,JavaScript不允許直接訪問內存中的位置,也就是說不能直接操作對象的內存空間。在操作對象時,實際上是在操作對象的引用而不是實際的對象。爲此,引用類型的值是按引用訪問的。
  • string類型不是對象

動態的屬性

  • 對於引用類型的值,我們可以爲其添加屬性和方法,也可以改變和刪除其屬性和方法。
  • 不能給基本類型的值添加屬性,儘管這樣做不會導致任何錯誤。

複製變量值

  • 如果從一個變量向另一個變量複製基本類型的值,會在變量對象上創建一個新值,然後把該值複製到爲新變量分配的位置上。
var num1 = 5;
var num2 = num1; // 5

// 修改num1的值,不會影響num2的值
num1 = 6;
alert(num2);     // 5
  • 當從一個變量向另一個變量複製引用類型的值時,同樣也會將存儲在變量對象中的值複製一份放到爲新變量分配的空間中。不同的是,這個值的副本實際上是一個指針,而這個指針指向存儲在堆中的一個對象。複製操作結束後,兩個變量實際上將引用同一個對象。因此,改變其中一個變量,就會影響另一個變量。
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name); // "Nicholas"

傳遞參數

  • ECMAScript中所有函數的參數都是按值傳遞的。也就是說,把函數外部的值複製給函數內部的參數,就和把值從一個變量複製到另一個變量一樣。基本類型值的傳遞如同基本類型變量的複製一樣,而引用類型值的傳遞,則如同引用類型變量的複製一樣。
function add10(num) {
    num += 10;
    return num;
}
var number = 10;
var number2 = add10(number); // 此時number值爲10;number2值爲20.

function setName(obj) {
    obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name) // "Nicholas"
  • 有很多開發人員錯誤地認爲:在局部作用域中修改的對象會在全局作用域中反映出來,就說明參數是按引用傳遞的。爲了證明對象是按值傳遞的,我們再看一看下面這個經過修改的例子:
function setName(obj) {
    obj.name = "nicholas";
    obj = new Object();
    obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); // "Nicholas"

這個例子與前一個例子的唯一區別,就是在setName()函數中添加了兩行代碼:一行代碼爲obj重新定義了一個對象,另一行代碼爲該對象定義了一個帶有不同值的name屬性。在把person傳遞給setName()後,其name屬性被設置爲”Nicholas”。然後,又將一個新對象賦給變量obj,同時將其name屬性設置爲”Greg”。如果person是按引用傳遞的,那麼person就會自動被修改爲指向其name屬性值爲”Greg”的新對象。但是,當接下來再訪問person.name時,顯示的值仍然是”Nicholas”。這說明即使在函數內部修改了參數的值,但原始的引用仍然保持未變。實際上,當在函數內部重寫obj時,這個變量引用的就是一個局部對象了。而這個局部對象會在函數執行完畢後立即被銷燬。

關於什麼是值傳遞,什麼是引用傳遞

值傳遞:
形參是實參的拷貝,改變形參的值並不會影響外部實參的值。從被調用函數的角度來說,值傳遞是單向的(實參->形參),參數的值只能傳入,不能傳出。當函數內部需要修改參數,並且不希望這個改變影響調用者時,採用值傳遞。

引用傳遞:
形參相當於是實參的“別名”,對形參的操作其實就是對實參的操作,在引用傳遞過程中,被調函數的形式參數雖然也作爲局部變量在棧中開闢了內存空間,但是這時存放的是由主調函數放進來的實參變量的地址。被調函數對形參的任何操作都被處理成間接尋址,即通過棧中存放的地址訪問主調函數中的實參變量。正因爲如此,被調函數對形參做的任何操作都影響了主調函數中的實參變量。

檢測類型

  • 檢測一個變量是不是基本數據類型,使用typeof
  • 檢測一個值是什麼類型的對象,使用instanceof

    • 如果變量是給定引用類型的實例,那麼instanceof操作符就會返回true。請看下面的例子:
    alert(person instanceof Object);
    • 根據規定,所有引用類型的值都是Object的實例。因此,在檢測一個引用類型值和Object構造函數時,instanceof操作符始終會返回true。當然,如果使用instanceof操作符檢測基本類型的值,則該操作符始終會返回false,因爲基本類型不是對象。

執行環境及作用域

執行環境(execution context,爲簡單起見,有時也稱爲“環境”)是JavaScript中最爲重要的一個概念。執行環境定義了變量或函數有權訪問的其他數據,決定了它們各自的行爲。每個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在後臺使用它。

全局執行環境是最外圍的一個執行環境。根據ECMAScript實現所在的宿主環境不同,表示執行環境的對象也不一樣。在Web瀏覽器中,全局執行環境被認爲是window對象(第7章將詳細討論),因此所有全局變量和函數都是作爲window對象的屬性和方法創建的。某個執行環境中的所有代碼執行完畢後,該環境被銷燬,保存在其中的所有變量和函數定義也隨之銷燬(全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時纔會被銷燬)。

每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。ECMAScript程序中的執行流正是由這個方便的機制控制着。

當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。如果這個環境是函數,則將其活動對象(activation object)作爲變量對象。活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。作用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是作用域鏈中的最後一個對象。

標識符解析是沿着作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,然後逐級地向後回溯,直至找到標識符爲止(如果找不到標識符,通常會導致錯誤發生)。

延長作用域鏈

雖然執行環境的類型總共只有兩種——全局和局部(函數),但還是有其他辦法來延長作用域鏈。這麼說是因爲有些語句可以在作用域鏈的前端臨時增加一個變量對象,該變量對象會在代碼執行後被移除。
在兩種情況下會發生這種現象。具體來說,就是當執行流進入下列任何一個語句時,作用域鏈就會得到加長:

  1. try-catch語句的catch塊;
  2. with語句。

這兩個語句都會在作用域鏈的前端添加一個變量對象。對with語句來說,會將指定的對象添加到作用域鏈中。對catch語句來說,會創建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。

沒有塊級作用域

  • 對於有塊級作用域的語言來說,for語句初始化變量的表達式所定義的變量,只會存在於循環的環境之中。而對於JavaScript來說,由for語句創建的變量i即使在for循環執行結束後,也依舊會存在於循環外部的執行環境中。

1. 變量聲明

  • 使用var聲明的變量會自動被添加到最接近的環境中。
  • 在函數內部,最接近的環境就是函數的局部環境;在with語句中,最接近的環境是函數環境。
  • 如果初始化變量時沒有使用var聲明,該變量會自動被添加到全局環境。
    在編寫JavaScript代碼的過程中,不聲明而直接初始化變量是一個常見的錯誤做法,因爲這樣可能會導致意外。我們建議在初始化變量之前,一定要先聲明,這樣就可以避免類似問題。在嚴格模式下,初始化未經聲明的變量會導致錯誤。

2. 查詢標識符

當在某個環境中爲了讀取或寫入而引用一個標識符時,必須通過搜索來確定該標識符實際代表什麼。搜索過程從作用域鏈的前端開始,向上逐級查詢與給定名字匹配的標識符。如果在局部環境中找到了該標識符,搜索過程停止,變量就緒。如果在局部環境中沒有找到該變量名,則繼續沿作用域鏈向上搜索。搜索過程將一直追溯到全局環境的變量對象。如果在全局環境中也沒有找到這個標識符,則意味着該變量尚未聲明。

垃圾收集

JavaScript具有自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程中使用的內存。 這種垃圾收集機制的原理其實很簡單:找出那些不再繼續使用的變量,然後釋放其佔用的內存。爲此,垃圾收集器會按照固定的時間間隔(或代碼執行中預定的收集時間),週期性地執行這一操作。 下面我們來分析一下函數中局部變量的正常生命週期。局部變量只在函數執行的過程中存在。而在這個過程中,會爲局部變量在棧(或堆)內存上分配相應的空間,以便存儲它們的值。然後在函數中使用這些變量,直至函數執行結束。此時,局部變量就沒有存在的必要了,因此可以釋放它們的內存以供將來使用。在這種情況下,很容易判斷變量是否還有存在的必要;但並非所有情況下都這麼容易就能得出結論。垃圾收集器必須跟蹤哪個變量有用哪個變量沒用,對於不再有用的變量打上標記,以備將來收回其佔用的內存。用於標識無用變量的策略可能會因實現而異,但具體到瀏覽器中的實現,則通常有兩個策略。

標記清除

JavaScript中最常用的垃圾收集方式是標記清除(mark-and-sweep)。當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲“進入環境”。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,因爲只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記爲“離開環境”。 垃圾收集器在運行的時候會給存儲在內存中的所有變量都加上標記(當然,可以使用任何標記方式)。然後,它會去掉環境中的變量以及被環境中的變量引用的變量的標記。而在此之後再被加上標記的變量將被視爲準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最後,垃圾收集器完成內存清除工作,銷燬那些帶標記的值並回收它們所佔用的內存空間。

到2008年爲止,IE、Firefox、Opera、Chrome和Safari的JavaScript實現使用的都是標記清除式的垃圾收集策略(或類似的策略),只不過垃圾收集的時間間隔互有不同。

引用計數

另一種不太常見的垃圾收集策略叫做引用計數(reference counting)。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1。如果同一個值又被賦給另一個變量,則該值的引用次數加1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲零的值所佔用的內存。

引用計數有一個十分頭痛個問題即循環引用。因此很少被使用。

性能問題

垃圾收集器是週期性運行的,而且如果爲變量分配的內存數量很可觀,那麼回收工作量也是相當大的。在這種情況下,確定垃圾收集的時間間隔是一個非常重要的問題。說到垃圾收集器多長時間運行一次,不禁讓人聯想到IE因此而聲名狼藉的性能問題。IE的垃圾收集器是根據內存分配量運行的,具體一點說就是256個變量、4096個對象(或數組)字面量和數組元素(slot)或者64KB的字符串。達到上述任何一個臨界值,垃圾收集器就會運行。這種實現方式的問題在於,如果一個腳本中包含那麼多變量,那麼該腳本很可能會在其生命週期中一直保有那麼多的變量。而這樣一來,垃圾收集器就不得不頻繁地運行。結果,由此引發的嚴重性能問題促使IE7重寫了其垃圾收集例程。 隨着IE7的發佈,其JavaScript引擎的垃圾收集例程改變了工作方式:觸發垃圾收集的變量分配、字面量和(或)數組元素的臨界值被調整爲動態修正。IE7中的各項臨界值在初始時與IE6相等。如果垃圾收集例程回收的內存分配量低於15%,則變量、字面量和(或)數組元素的臨界值就會加倍。如果例程回收了85%的內存分配量,則將各種臨界值重置回默認值。這一看似簡單的調整,極大地提升了IE在運行包含大量JavaScript的頁面時的性能。

管理內存

使用具備垃圾收集機制的語言編寫程序,開發人員一般不必操心內存管理的問題。但是,JavaScript在進行內存管理及垃圾收集時面臨的問題還是有點與衆不同。其中最主要的一個問題,就是分配給Web瀏覽器的可用內存數量通常要比分配給桌面應用程序的少。這樣做的目的主要是出於安全方面的考慮,目的是防止運行JavaScript的網頁耗盡全部系統內存而導致系統崩潰。內存限制問題不僅會影響給變量分配內存,同時還會影響調用棧以及在一個線程中能夠同時執行的語句數量。 因此,確保佔用最少的內存可以讓頁面獲得更好的性能。而優化內存佔用的最佳方式,就是爲執行中的代碼只保存必要的數據。一旦數據不再有用,最好通過將其值設置爲null來釋放其引用——這個做法叫做解除引用(dereferencing)。這一做法適用於大多數全局變量和全局對象的屬性。局部變量會在它們離開執行環境時自動被解除引用。

小結

JavaScript變量可以用來保存兩種類型的值:基本類型值和引用類型值。基本類型的值源自以下5種基本數據類型:Undefined、Null、Boolean、Number和String。基本類型值和引用類型值具有以下特點:
  • 基本類型值在內存中佔據固定大小的空間,因此被保存在棧內存中;
  • 從一個變量向另一個變量複製基本類型的值,會創建這個值的一個副本;
  • 引用類型的值是對象,保存在堆內存中;
  • 包含引用類型值的變量實際上包含的並不是對象本身,而是一個指向該對象的指針;
  • 從一個變量向另一個變量複製引用類型的值,複製的其實是指針,因此兩個變量最終都指向同一個對象;
  • 確定一個值是哪種基本類型可以使用typeof操作符,而確定一個值是哪種引用類型可以使用instanceof操作符。

所有變量(包括基本類型和引用類型)都存在於一個執行環境(也稱爲作用域)當中,這個執行環境決定了變量的生命週期,以及哪一部分代碼可以訪問其中的變量。以下是關於執行環境的幾點總結:
+ 執行環境有全局執行環境(也稱爲全局環境)和函數執行環境之分;
+ 每次進入一個新執行環境,都會創建一個用於搜索變量和函數的作用域鏈;
+ 函數的局部環境不僅有權訪問函數作用域中的變量,而且有權訪問其包含(父)環境,乃至全局環境;
+ 全局環境只能訪問在全局環境中定義的變量和函數,而不能直接訪問局部環境中的任何數據;
+ 變量的執行環境有助於確定應該何時釋放內存。

JavaScript是一門具有自動垃圾收集機制的編程語言,開發人員不必關心內存分配和回收問題。

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