筆記 - JavaScript 高級程序設計【第 04 章:變量、做用域和內存問題】

第04章:變量、做用域和內存問題

JavaScript 的變量只是在特定時間用於保存特定值的一個名字而已,不限制變量保存的數據類型,變量的值及其數據類型能夠在腳本的生命週期內改變;這就是一般所說的有趣、強大但又容易出問題的「靈活性」。前端

4.1. 基本類型和引用類型的值

  ECMAScript 變量可能包含兩種不一樣數據類型的值:基本類型值和引用類型值。基本類型值指的是簡單的數據段;引用類型值指的是那些可能由多個值構成的對象。算法

  變量被賦值時,解析器必須肯定要賦的值是基本類型值仍是引用類型值。擴展:這也是由於須要開闢不一樣的內存空間,由於基本類型值保存在內存的棧區,而引用類型值保存在內存的堆區(堆比棧大,棧比堆快)。瀏覽器

  五種基本數據類型是按值訪問的,由於能夠操做保存在變量中的實際值。閉包

  引用類型值是保存在內存中的對象。與其餘語言不一樣,JavaScript 不容許直接複製保存在內存中的對象,在複製保存着對象的某個變量時,實際上覆制的是對象的引用(內存地址)而非對象自己。函數

  4.1.1. 動態的屬性

    引用類型的值能夠爲其進行屬性和方法的添加、刪除和改變,而基本類型的值是不能添加屬性的,更談不到屬性的刪除和改變了。性能

  4.1.2. 複製變量值

    變量之間值的複製,若是是基本類型的值,會在變量對象上建立一個新值,而後把這個值複製到爲新變量分配的位置上;原值與新值之間是徹底獨立的,新值只是原值的一個副本,兩個變量能夠參與任何操做而不會互相影響。spa

    當變量之間複製的是引用類型的值時,與基本類型值的複製過程是同樣的,而不一樣的是變量中存儲的是指向存儲在堆中的一個對象的指針;完成複製後,兩個變量擁有指向同一對象的指針,因此,改變其中一個變量,就會影響另外一個變量。例如:指針

    var obj1 = new Object();
    var obj2 = obj1;
    obj1.name = 'hawk';
    alert(obj2.name); //兩個變量裏值是指針,它們都指向同一個對象,因此結果必然也是hawk;

  4.1.3. 傳遞參數

    ECMAScript 中全部函數的參數都是按值傳遞的。書中所說的「困惑」,我的理解是:因爲引用類型值保存的是指向對象的「指針」,而在函數內部修改參數引用函數外對象的屬性會反應到這個對象上,會讓人誤覺得參數也會按引用傳遞。code

    參數傳遞與變量複製,二者的機制是相同的;值的副本複製給了參數,而參數能夠看作是函數內部的局部變量。對象

    對象的傳參比較特殊,例如:

    var person = new Object();
    person.name = 'Lucy';

    function setName(obj) {
        obj.name = 'Bob'; //在函數內部將傳入對象的name屬性賦值爲Bob;
    }

    setName(person);
    alert(person.name); //Bob,函數外部的對象name屬性發生了變化;

    function setAge(obj) {
        obj.age = 19; //在函數內部爲傳入對象添加並設置age屬性;
        obj = new Object(); //這時修改了參數的值,也就是修改了參數存儲的指針;
        obj.age = 99; //再次更改age屬性;
    }

    setAge(person);
    alert(person.age); //19,函數內部修改了參數存儲的指針後並未影響外部對象;

    上面的例子中,函數內部重寫了 obj,這個變量的應用已是一個局部對象了,這個局部對象會在函數執行完畢後當即被銷燬。

  4.1.4. 檢測類型

    檢測一個變量是否爲基本類型,可使用 typeof 操做符;可是當要檢測的變量是 null 或者是一個對象時,typeof 沒法作出區分,都會返回 "object"。

    因爲 typeof 操做符沒法準確檢測引用類型的值,因此用處不大,若是要檢測某個值是什麼類型的對象,須要使用 instanceof 操做符。

    若是變量是給定引用類型(根據原型鏈識別)的實例,那麼 instanceof 操做符便返回 turn;若是變量是基本數據類型,會直接返回 false,由於基本類型不是對象。

    全部引用類型的值都是 Object 類型的實例,因此檢測一個引用類型值和 Object 構造函數時,instanceof 操做符會始終返回 turn。

4.2. 執行環境及做用域

  執行環境(execution context)是 JavsScript 中最爲重要的一個概念。我的理解,「執行環境」就是通常所說的「做用域」。執行環境定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。

  每一個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的全部變量和函數都保存在這個對象中;我的理解,函數是一個執行環境,那麼函數內部的變量(包括參數)或函數就至關於它的屬性和方法。

  全局執行環境是最外圍的一個執行環境。根據 ECMAScript 實現所在的宿主環境不一樣,表示執行環境的對象也不同,在 Web 瀏覽器中,全局執行環境是 window 對象,全部的全局變量和函數都是 window 對象的屬性和方法

  某個執行環境中的全部代碼執行完畢後,該環境被銷燬,裏面保存的全部變量和函數也隨之銷燬;全局執行環境直到網頁關閉纔會銷燬。

  當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈(scope chain),它用來保證對執行環境有權訪問的全部變量和函數的有序訪問。說白了就像地圖同樣,按圖索驥。

  做用域鏈的前端,始終都是當前執行代碼所在環境的變量對象;這裏的「前端」,能夠理解爲「起始點」。若是這個環境是函數,則將其活動對象(activation object)做爲變量對象。

  全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。我的理解,例如函數中的變量訪問,先從函數內部找,若是內部沒有,則向上一層的環境中找,一直追溯到全局執行環境,直至找到,若是最終都沒有找到,那麼就會出現編譯錯誤。

  前面所說的變量追溯就是標識符解析的一種,搜索過程始終從做用域鏈的前端開始。

  變量和函數訪問只能經過做用域鏈在執行環境內和向上級(父執行環境)訪問,而不能訪問下一級中的變量和函數,若是要訪問下一級中的變量和函數,可使用閉包。

  4.2.1. 延長做用域鏈

    執行環境的類型有全局和局部(函數)兩種,但有其餘辦法來延長做用域。做用域鏈的延長有兩種狀況,try-catch 語句的 catch 塊,以及 with 語句。

    這兩個語句都會在做用域的前端添加一個變量對象,例如 with 語句,會將括號中制定的對象添加到做用域鏈中;而 catch 語句會建立一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。

  4.2.2. 沒有塊級做用域

    在 JavaScript 中,沒有塊級做用域;就是說,不像其餘類 C 的語言中,使用花括號封閉的代碼塊就有本身的做用域(執行環境),從而根據條件來定義變量。

    沒有塊級做用域,使得在 if、for 等語句中的變量,在語句執行完成後,仍然能夠在語句外部訪問到這些變量。

    1. 聲明變量

      使用 var 聲明的變量會被自動添加到最近的環境中,若是沒有使用 var 聲明,該變量會被自動添加到全局環境中。

      實際開發中,初始化變量以前必定要先聲明;在嚴格模式下,初始化未經聲明的變量會致使錯誤。

    2. 查詢標識符

      標識符的查詢過程從做用域鏈的前端開始,向上逐級查找。找到後,查找結束,變量就緒;沒有找到,便一直向上查找到全局環境,若是到全局環境還沒找到,說明該變量還沒有聲明。

4.3. 垃圾收集

  JavaScript 具備自動垃圾回收機制,執行環境會自動找出再也不繼續使用的變量,而後釋放其佔用的內存;爲此,垃圾收集器會按照固定時間間隔(或代碼預約的間隔)週期性釋放內存。

  變量或函數的生命週期,指的是變量或函數從聲明到其在執行環境中使用完畢後被垃圾回收銷燬的過程。

  4.3.1. 標記清除

    JavaScript 中經常使用的垃圾收集方式是標記清除(mark-and-sweep)。當變量進入環境時,就將該變量標記爲「進入環境」;當變量離開環境時,則將其標記爲「離開環境」。垃圾收集器按照標記進行垃圾收集策略。

  4.3.2. 引用計數

    另外一種不太常見的策略叫引用計數(reference counting)。它的含義是追蹤記錄每一個值被引用的次數。

    當聲明瞭一個變量並將一個引用類型的值賦給該變量時,則該值引用次數爲 1;若是同一值又被賦給另外一變量,則該值引用次數加 1;若是包含對該值引用的變量又取得了另外一個值,則該值引用次數減 1。當次數爲 0 時,則會被回收。

    循環引用致使引用次數的策略出現了問題。循環引用指的是對象 A 中包含一個指向對象 B 的指針,而對象 B 中也包含一個指向對象 A 的指針;也就是二者在相互引用。這致使它們的應用次數永遠不爲 0。

    爲了不循環引用形成的內存泄露,最好實在不使用這些對象的時候手工斷開,好比給不使用的對象會變量賦值 null。

  4.3.3. 性能問題

    垃圾收集器的時間間隔策略的不合理,會致使性能問題,例如 IE6 瀏覽器的垃圾收集器。

    在有的瀏覽器中能夠出發垃圾收集過程,例如 IE 中調用 window.CollectGarbage() 方法,在 Opera7 及更高版本中調用 window.opera.collect() 方法。但在實際開發中不建議手動出發垃圾收集過程。

  4.3.4. 管理內存

    雖然具有垃圾收集機制的 JavaScript 通常沒必要操心內存管理的問題,可是分配給 Web 瀏覽器的內存有限。

    爲了用最少的內存得到更好的性能,在開發中,只爲執行中的代碼保存必要數據,一旦數據不在有用,最好將其設置爲 null 來釋放其引用,這就叫作解除引用(dereferencing)。

    解除引用不會當即釋放值佔用的內存,可是會在垃圾收集器下一輪的運行時將其回收。

4.4. 小結

  ● JavaScript 變量能夠用來保存兩種類型的值:基本類型和引用類型。基本類型的值源自 5 種基本數據類型。基本類型值和引用類型值具備如下特色:

    ◎ 基本類型值在內存中佔據固定大小的空間,所以被保存在棧內存中;

    ◎ 從一個變量向另外一個變量複製基本類型的值,會建立這個值的一個副本;

    ◎ 引用類型的值是對象,保存在堆內存中;

    ◎ 包含引用類型值的變量實際上包含的並非對象自己,而是一個指向該對象的指針;

    ◎ 從一個變量向另外一個變量複製引用類型的值,複製的是指針,所以兩個變量最終指向同一個對象;

    ◎ 肯定一個值是哪一種基本類型可使用 typeof 操做符,而肯定一個值是哪一種引用類型可使用 instanceof 操做符。

  ● 全部變量都存在於一個執行環境中,這個環境決定了變量的生命週期,以及哪部分代碼能夠訪問其中的變量。關於執行環境的總結以下:

    ◎ 執行環境分爲全局執行環境和函數執行環境;

    ◎ 每次進入一個新執行環境,都會建立一個用於搜索變量和函數的做用域鏈;

    ◎ 函數的局部環境不只有權訪問函數做用域中的變量,還有權訪問其包含(父)環境,乃至全局環境;

    ◎ 全局環境只能訪問在全局環境中定義的變量和函數,而不能直接訪問局部環境中的任何數據(除了沒有使用 var 聲明的變量);

    ◎ 變量的執行環境有助於肯定應該什麼時候釋放內存。

  ● JavaScript 是一門具備自動垃圾回收機制的語言,開發中沒必要關心內存的分配和回收問題,關於垃圾回收的總結以下:

    ◎ 離開做用域的值將被自動標記爲能夠回收,所以將在垃圾收集期間被刪除;

    ◎ 「標記清除」是目前主流的垃圾收集算法,這種算法的思想是給當前不使用的值加上標記,而後再回收其內存;

    ◎ 另外一種垃圾手機算法叫「引用計數」,它的思想是跟蹤記錄全部值被引用的次數。目前已不使用,可是 IE 中訪問非原生 JavaScript 對象(如 DOM 元素)時,這種算法仍然會致使問題;

    ◎ 當代碼中存在循環引用時,「引用計數」算法就會致使問題;

    ◎ 解除變量的引用不只有助於消除循環引用,還能有效回收內存,應該及時解除不在使用的全局對象、全局對象屬性以及循環引用變量的引用。

(完)

相關文章
相關標籤/搜索