1. 程式人生 > >《JavaScript 高級程序設計》第四章:變量、作用域和內存問題

《JavaScript 高級程序設計》第四章:變量、作用域和內存問題

綁定 pro 擔心 block lock 效率 TE 二次 problem

目錄

  • 變量的引用
  • 執行環境及作用域
  • 作用域鏈延長
  • 塊級作用域
  • 垃圾回收機制

變量的引用

當一個變量保存了基本數據類型時,此時對於變量的操作(賦值,運算)就是操作這個基本數據的本身,就算是賦值操作,賦值時拷貝後的值與之前的值也是相互獨立互不影響的。

var a = 1;
var b = a
b++;
console.log(a); //1
console.log(b); //2

這非常好理解,但是如果一個變量保存的是一個引用類型的數據,例如對象,那麽情況將會不同,這是因為變量保存的並不是對象本身,而是其在內存中的地址(指針),所以當對引用類型進行賦值時,雖然也會進行拷貝,但是這個拷貝後的值只是一個指針,它們兩個最終指向的都是對同一個對象的引用,因此便會存在相互影響的問題。

var obj = new Object();
var copyObj = obj;
obj.name = 'test';
console.log(obj2.name); //test

用圖表示就是如下關系:
技術分享圖片

通過具體的事例再來感受這兩者的不同:

function PersonnelInfo(age, info) {
    age = 10;
    info.name = "cheng";
}
var info = {};
var age = 0;
PersonnelInfo(age, info);
console.log(age);
console.log(info);

由此我們可以得出變量的訪問有兩種,一種是“按值訪問”,另一種則是“按引用訪問”,按值訪問操作變量操作的就是值的本身,在進行賦值的時候也是相互獨立互不影響的,而按引用訪問,再進行賦值的時候,實際上多個變量訪問的對象依然是同一個。

一般來說我們會將固定大小的值(例如基本數據類型)保存到棧內存中,將不能固定大小的值(例如對象,數組等)保存到堆內存中,這樣的區分也是更好的利用內存空間,提高執行效率。

執行環境及作用域

變量的訪問有兩種,“按值訪問”以及“按引用訪問”,而執行環境(Execution Context) 則形成了作用域,作用域又確定了變量或函數是否有權訪問其它環境中的數據。也就是說執行環境確定了某些標識符是否能被訪問。

每個執行環境都會有個與之對應的變量對象(Variable Object),變量對象用於管理和保存當前環境中的變量以及函數(標識符)。當從一個執行環境進入到另一個執行環境時,首先便會創建變量對象,然後該執行環境會被加入到當前的執行棧中進行執行,如果執行完畢則會從執行棧中彈出,並且該環境中的代碼、函數以及變量也都會被銷毀。

在JavaScript中執行環境主要的有兩種:全局執行環境,函數執行環境。全局執行環境綁定在 WEB瀏覽器頂層宿主對象 window上,也就是說我們在全局執行環境中聲明的變量或者函數都會將作為 window對象的屬性或者是方法,也因此只有在退出瀏覽器或關閉WEB頁面才會銷毀全局執行環境。
函數的執行環境也是一種局部執行環境,變量對象會比較特殊,我們更多的將其稱之為“活動對象(Active Object)”,它默認保存的一個標識符就是 arguments,而 arguments 中保存的便是該函數的參數。

當執行環境被加入到執行棧中進行執行的時候,JS引擎會根據變量對象來解析標識符,首先它會查找當前執行環境中的標識符是否在變量對象中有定義,如果有則取得標識符的值進行下一步操作,如果沒有,則向上進入到上一級執行環境,訪問其變量對象,依次類推,像這樣對不同的環境不同的變量對象進行訪問的路徑,我們可以稱之為作用域鏈(scope chain)。

技術分享圖片

因此,所謂的標識符解析實際上就是沿著作用域鏈進行標識符的查找。
作用域鏈的訪問只能由前向後,由下向上,而不能反方向訪問,具體可見事例代碼:

var x = 1;
function method(){
    var y = 2;
    console.log(x);
}
method();
console.log(y); //Uncaught ReferenceError: y is not defined

也可見下圖:

技術分享圖片

作用域鏈延長

在 JavaScript中有的語句可以在當前的作用域鏈前端增加一個變量對象,從而延長作用域鏈。
對於 with 語句它會將指定的對象作為變量對象添加至作用域鏈的前端。

function buildUrl(){

    var search = '?debug=true';
    with(location){
        var url = href + search; #註意with並沒有作用域。
    }
    return url;
}

而對於 catch 語句而言則會創建一個新的對象,然後添加至當前作用域鏈的前端,在這個變量對象中保存的主要是錯誤對象 Error 的相關信息,例如 name,message等。

塊級作用域

JavaScript (ES5) 中並不存在塊級作用域,ES5支持的作用域跟執行環境相同,主要有函數(局部)作用域、全局作用域。
聲明變量時,如果使用 var 關鍵字,則所聲明的變量添加至當前執行環境中的變量對象上,如果沒有使用 var 關鍵字,則默認添加至全局執行環境的的變量對象上。

不論是全局作用域還是局部作用域,聲明變量時正確的操作都是使用 var 關鍵字去聲明。

垃圾回收機制

概述

JavaScript支持自動的垃圾回收機制,而不像C,C++那樣需要手動的跟蹤內存的使用情況,而所謂的自動垃圾回收機制,其本質原理非常簡單,那就是每隔一段時間,周期性的檢查程序的執行情況,將不在使用的標識符其所占據的內存釋放,或者自動分配程序執行期間所需要的內存空間,這樣開發人員只需關註業務功能代碼,無需過多關心內存的使用情況。

當我們明白垃圾回收機制的大致原理時,那麽如何確定一個標識符,其生命周期是否已經結束,就時垃圾自動回收的功能核心。

標記清除

主流瀏覽器廠商,基本都時采用 “標記清除(mark and sweep)”的方式來標識那些變量可以被回收,那些變量還具有引用關系不能被回收。
其大致思路時當一個變量進入執行環境時,會為它添加一個標誌位,用於說明該變量進入了該環境,原則上永遠不能釋放進入環境中的變量,因為執行流程進入到相應的環境就有可能會用得到。而當變量離開環境時,則再將其標誌置為離開狀態。
這種標誌位的記錄方式有很多種,你可以通過翻轉某一個位來記錄一個變量何時進入環境,何時離開環境,也可以使用 map表的方式來分別記錄進入與離開時的狀態信息。
當JS的垃圾回收機制運行的時候,它會給存儲在內存中的所有變量都加上標記(可以使用任何方式)然後它會去掉全局環境中的以及執行環境中具有引用關系的變量標記,而在此之後在被添加標記(可以認為是離開標記)都被視為準備刪除的變量,原因是這些變量已經不需要再被訪問了。
簡單的來概括,那就時當變量進入環境時,其標記為1,離開時置為0,然後JS的垃圾回收機制每隔一段時間來掃描,將標記為0的變量進行釋放。
用代碼來表示如下:

var status = 1 //進入環境

status = null //離開環境,或者為Null的時候,馬上被垃圾回收機制回收。

引用計數

“引用計數 (reference counting)”。引用計數實際上就是對值的一種計數標記,當我們定義一個變量並為它賦值一個引用類型時,這個引用類型的值其引用次數就默認為1,當這個值還被其它的變量所引用,則引用次數加1。相反當引用了這個值的變量引用了別的值,則其引用次數減1。當這個值的引用次數為0時,便說明這個值已經以及沒有被其它變量引用了,此時便可以將其所占據的內存釋放出來。

引用計數的方式有一個非常嚴重的問題,那就是“循環引用”,當A的值有對B值的引用,而B值中也有對A值的引用時就會發生循環引用。

function problem(){
    var ObjectA = new Object();
    var ObjectB = new Object();
    ObjectA.A = ObjectB;
    ObjectB.B = ObjectA;
}

在這個事例中變量 ObjectA 與 ObjectB的值分別被引用了兩次,第一次是聲明變量並賦值的時候,第二次則是它們各自的屬性進行了交叉引用。所以此時這兩個對象的值引用次數就時 2,如果在標記清除的策略中並沒用什麽問題,但是在引用計數的方式下,ObjectA與ObjectB將在函數執行完畢後還會存在,因為它們的引用次數永遠不會為0。假如這個函數被重復執行多次,那麽就會導致更多的內存空間得不到回收。

采用“引用計數”方式的瀏覽器都非常古老了,主要是 Netspace Navigator 3.0 以及之前,所以不需要太擔心,但是了解下還是非常有必要的,這是因為在IE9之前,對於JavaScript中原生對象采用的是標記清除方式,但是對於非原生對象,例如BOM,DOM等垃圾回收機制依然還是引用計數策略,所以如果需要兼容IE9以下版本的瀏覽器,在操作非原生對象時,就有可能會出現循環引用的問題。

事例代碼:

var element = document.getElementById('element');
var myObject = new Object();

myObject.element = element;
element.hostObject = myObject;

對於這種情況,我們最好在書寫代碼的時候就要盡量避免,如果難以避免,則在程序執行完成以後也要記得手動釋放。

myObject.element = null;
element.hostObject = null;

性能問題

我們知道垃圾回收機制是周期性的進行運行的,因此確定垃圾收集的時間間隔是一個非常重要的問題。
在IE7之前,IE的回收機制都是根據內存的分配量進行的,具體一點就是256個變量,4096個對象(或數組)字面量和數組元素(slot)或者64KB的字符串,達到上述的任何一個臨界值,垃圾收集器就會運行。這種實現方式的問題在於如果一個腳本具有非常多的變量,那麽腳本很可能會在其生命周期中一直保有那麽多的變量。這樣一來,垃圾收集器就不得不頻繁的執行,從而影響了正常的程序執行。
到了IE7後,微軟重寫了IE瀏覽器的垃圾回收機制,觸發垃圾收集的變量分配、字面量和(或)數組元素的臨界值被調整為動態修正,其各項臨界值在初始時與IE6及之前的版本相同,如果垃圾收集例程回收的內存分配量低於15%,則變量、字面量和(或)數組元素的臨界值就會加倍,如果例程回收了85%的內存分配量,則再將各臨界值重置為默認值。

在日常編碼時還有另一種方式可以很好的提升執行的性能,那就是為不再使用到的變量賦值 null 來進行手動的釋放。一般來說這種方式常用於全局變量,因為局部變量會由垃圾回收機制自行清理。

function doSomething() {
    var obj = new Object();
    return obj.name = 'csutom';

}

var global_var = doSomething();

//...

global_var = null; //當不用的時候最好記得手動釋放

《JavaScript 高級程序設計》第四章:變量、作用域和內存問題