變量、做用域和內存問題

一、基本類型和引用類型的值

  • 概念
    • 變量可能包含兩種不一樣數據類型的值:基本類型值和引用類型值。基本類型值指的是簡單的數據段,而引用類型值指那些可能由多個值構成的對象;
    • 基本數據類型:Undefined、Null、Boolean、Number 和 String。這 5 種基本數據類型是按值訪問的,由於能夠操做保存在變量中的實際的值;
    • 引用類型的值是保存在內存中的對象,當複製保存着對象的某個變量時,操做的是對象的引用,但在爲對象添加屬性時,操做的是實際的對象。
  • 動態的屬性前端

    • 定義基本類型值和引用類型值的方式是相似的:建立一個變量併爲該變量賦值算法

    • 對於引用類型的值,能夠爲其添加屬性和方法,也能夠改變和刪除其屬性和方法編程

      var person = new Object();
      person.name = "Nicholas";
      alert(person.name); //"Nicholas"
    • 不能給基本類型的值添加屬性,儘管這樣作不會致使任何錯誤數組

      var name = "Nicholas";
      name.age = 27;
      alert(name.age); //undefined
  • 複製變量值瀏覽器

    • 若是從一個變量向另外一個變量複製基本類型的值,會在變量對象上建立一個新值,而後把該值複製
      到爲新變量分配的位置上,這兩個變量能夠參與任何操做而不會相互影響安全

      var num1 = 5;
      var num2 = num1;
    • 當從一個變量向另外一個變量複製引用類型的值時,一樣也會將存儲在變量對象中的值複製一份放到爲新變量分配的空間中。不一樣的是,這個值的副本其實是一個指針,而這個指針指向存儲在堆中的一個對象。複製操做結束後,兩個變量實際上將引用同一個對象。所以,改變其中一個變量,就會影響另外一個變量編程語言

      var obj1 = new Object();
      var obj2 = obj1;
      obj1.name = "Nicholas";
      alert(obj2.name); //"Nicholas"
  • 傳遞參數函數

    • 訪問變量有按值和按引用兩種方式,而參數只能按值傳遞(全部函數的參數都是按值傳遞的),把函數外部的值複製給函數內部的參數,就和把值從一個變量複製到另外一個變量同樣工具

    • 在向參數傳遞基本類型的值時,被傳遞的值會被複制給一個局部變量(即命名參數, 也是arguments 對象中的一個元素)性能

      function addTen(num) {
          num += 10;
          return num;
      }
      var count = 20;
      var result = addTen(count);
      alert(count); //20,沒有變化
      alert(result); //30
    • 在向參數傳遞引用類型的值時,會把這個值在內存中的地址複製給一個局部變量,所以這個局部變量的變化會反映在函數的外部

      //例1:
      function setName(obj) {
          obj.name = "Nicholas";
      }
      var person = new Object();
      setName(person);
      alert(person.name); //"Nicholas"
      //例2:
      function setName(obj) {
          obj.name = "Nicholas";
          obj = new Object();
          obj.name = "Greg";
      }
      var person = new Object();
      setName(person);
      alert(person.name); //"Nicholas"
  • 檢測類型

    • typeof 操做符

      //typeof 操做符是肯定一個變量是字符串、數值、布爾值,仍是 undefined 的最佳工具
      //若是變量的值是一個對象或 null,則 typeof 操做符會返回"object"
      //使用 typeof 操做符檢測函數時,該操做符會返回"function"
      var s = "Nicholas";
      var b = true;
      var i = 22;
      var u;
      var n = null;
      var o = new Object();
      alert(typeof s); //string
      alert(typeof i); //number
      alert(typeof b); //boolean
      alert(typeof u); //undefined
      alert(typeof n); //object
      alert(typeof o); //object
    • instanceof操做符

      //在檢測一個引用類型值和 Object 構造函數時,instanceof 操做符始終會返回 true
      //若是使用 instanceof 操做符檢測基本類型的值,則該操做符始終會返回 false
      alert(person instanceof Object); // 變量 person 是 Object 嗎?
      alert(colors instanceof Array); // 變量 colors 是 Array 嗎?
      alert(pattern instanceof RegExp); // 變量 pattern 是 RegExp 嗎?

二、執行環境及做用域

  • 概念

    • 執行環境(execution context,也稱爲「環境」)定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的全部變量和函數都保存在這個對象中。某個執行環境中的全部代碼執行完畢後,該環境被銷燬,保存在其中的全部變量和函數定義也隨之銷燬;

    • 全局執行環境是最外圍的一個執行環境,宿主環境不一樣,表示執行環境的對象也不同,在 Web 瀏覽器中,全局執行環境被認爲是 window 對象,全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時纔會被銷燬;

    • 每一個函數都有本身的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境;

    • 當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈(scope chain)。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。

    • 做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是環境是函數,則將其活動對象(activation object)做爲變量對象。活動對象在最開始時只包含一個變量,即 arguments 對象(這個對象在全局環境中是不存在的)。做用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。

    • 標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直至找到標識符爲止(若是找不到標識符,一般會致使錯誤發生)

      var color = "blue";
      function changeColor(){
          if (color === "blue"){
              color = "red";
          } else {
              color = "blue";
          }
      }
      changeColor();
      alert("Color is now " + color);
    • 在局部做用域中定義的變量能夠在局部環境中與全局變量互換使用,每一個環境均可以向上搜索做用域鏈,以查詢變量和函數名;但任何環境都不能經過向下搜索做用域鏈而進入另外一個執行環境。

      var color = "blue";
      function changeColor(){
          var anotherColor = "red";
          function swapColors(){
              var tempColor = anotherColor;
              anotherColor = color;
              color = tempColor;
              // 這裏能夠訪問 color、anotherColor 和 tempColor
          }
          // 這裏能夠訪問 color 和 anotherColor,但不能訪問 tempColor
          swapColors();
      }
      // 這裏只能訪問 color
      changeColor();
  • 延長做用域鏈

    • try-catch 語句的 catch 塊:會建立一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明

    • with 語句:會將指定的對象添加到做用域鏈中

      function buildUrl() {
          var qs = "?debug=true";
          with(location){
              var url = href + qs;
          }
          return url;
      }
  • 沒有塊級做用域

    • if 語句中的變量聲明會將變量添加到當前的執行環境(在這裏是全局環境)中

      if (true) {
          var color = "blue";
      }
      alert(color); //"blue"
    • for 語句建立的變量 i 即便在 for 循環執行結束後,會存在於循環外部的執行環境中

      for (var i=0; i < 10; i++){
          doSomething(i);
      }
      alert(i); //10
    • 使用 var 聲明的變量會自動被添加到最接近的環境中,若是初始化變量時沒有使用 var 聲明,該變量會自
      動被添加到全局環境

      function add(num1, num2) {
          sum = num1 + num2;
       return sum;
      }
      var result = add(10, 20); //30
      alert(sum); //30
    • 查詢標識符的過程:

      • 從做用域鏈的前端開始,向上逐級查詢與給定名字匹配的標識符

      • 若是在局部環境中找到了該標識符,搜索過程中止,變量就緒

        var color = "blue";
        function getColor(){
          var color = "red";
          return color;
        }
        alert(getColor()); //"red"
      • 若是在局部環境中沒有找到該變量名,則繼續沿做用域鏈向上搜索。搜索過程將一直追溯到全局環境的變量對象

        var color = "blue";
        function getColor(){
          return color;
        }
        alert(getColor()); //"blue"
      • 若是在全局環境中也沒有找到這個標識符,則意味着該變量還沒有聲明

三、垃圾收集

  • 概念

    • JavaScript 具備自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程當中使用的內存,開發人員不用再關心內存使用問題,所需內存的分配以及無用內存的回收徹底實現了自動管理
    • 原理:找出那些再也不繼續使用的變量,而後釋放其佔用的內存,垃圾收集器會按照固定的時間間隔(或代碼執行中預約的收集時間),週期性地執行這一操做
    • 垃圾收集器必須跟蹤哪一個變量有用哪一個變量沒用,對於再也不有用的變量打上標記,以備未來收回其佔用的內存。用於標識無用變量的策略可能會因實現而異,但具體到瀏覽器中的實現,則一般有兩個策略。
  • 標記清除

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

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

    • 產生的問題:循環引用,指的是對象 A 中包含一個指向對象 B 的指針,而對象 B 中也包含一個指向對象 A 的引用,會致使內存得不到回收

      function problem(){
          var objectA = new Object();
          var objectB = new Object();
          objectA.someOtherObject = objectB;
          objectB.anotherObject = objectA;
      }
    • IE 的 JavaScript 引擎是使用標記清除策略來實現的,但JavaScript 訪問的 COM(組件對象模型) 對象依然是基於引用計數策略的,涉及 COM 對象,就會存在循環引用的問題,爲了不相似這樣的循環引用問題,最好是在不使用它們的時候手工斷開鏈接

      //因爲存在這個循環引用,即便將例子中的 DOM 從頁面中移除,它也永遠不會被回收
      var element = document.getElementById("some_element");
      var myObject = new Object();
      myObject.element = element;
      element.someObject = myObject;
      //將變量設置爲 null 意味着切斷變量與它此前引用的值之間的鏈接
      myObject.element = null;
      element.someObject = null;
  • 性能問題

    • 垃圾收集器是週期性運行的,並且若是爲變量分配的內存數量很可觀,那麼回收工做量也是至關大的,在這種狀況下,肯定垃圾收集的時間間隔是一個很是重要的問題;
    • IE7以前的垃圾收集器是根據內存分配量運行的,具體一點說就是 256 個變量、4096 個對象(或數組)字面量和數組元素(slot)或者 64KB 的字符串,達到上述任何一個臨界值,垃圾收集器就會運行,這種實現方式的問題在於,若是一個腳本中包含那麼多變量,那麼該腳本極可能會在其生命週期中一直保有那麼多的變量,而這樣一來,垃圾收集器就不得不頻繁地運行;
    • IE7以後的垃圾收集器觸發垃圾收集的變量分配、字面量和(或)數組元素的臨界值被調整爲動態修正,各項臨界值在初始時相等,若是垃圾收集例程回收的內存分配量低於 15%,則變量、字面量和(或)數組元素的臨界值就會加倍,若是例程回收了 85%的內存分配量,則將各類臨界值重置回默認值;
    • 在有的瀏覽器中能夠觸發垃圾收集過程,但不建議這樣作,在IE 中,調用 window.CollectGarbage()方法會當即執行垃圾收集。在 Opera 7 及更高版本中,調用 window.opera.collect()也會啓動垃圾收集例程。
  • 管理內存

    • JavaScript在進行內存管理及垃圾收集時面臨的最主要的一個問題,就是分配給 Web瀏覽器的可用內存數量一般要比分配給桌面應用程序的少。這樣作的目的主要是出於安全方面的考慮,目的是防止運行 JavaScript 的網頁耗盡所有系統內存而致使系統崩潰,內存限制問題不只會影響給變量分配內存,同時還會影響調用棧以及在一個線程中可以同時執行的語句數量,

    • 所以,確保佔用最少的內存可讓頁面得到更好的性能。而優化內存佔用的最佳方式,就是爲執行中的代碼只保存必要的數據,一旦數據再也不有用,最好經過將其值設置爲 null 來釋放其引用——這個作法叫作解除引用(dereferencing)。這一作法適用於大多數全局變量和全局對象的屬性。局部變量會在它們離開執行環境時自動被解除引用

      function createPerson(name){
          var localPerson = new Object();
          localPerson.name = name;
          return localPerson;
      }
      var globalPerson = createPerson("Nicholas");
      // 手工解除 globalPerson 的引用
      globalPerson = null;
    • 解除一個值的引用並不意味着自動回收該值所佔用的內存。解除引用的真正做用是讓值脫離執行環境,以便垃圾收集器下次運行時將其回收

四、總結

  • JavaScript 變量能夠用來保存兩種類型的值:基本類型值和引用類型值。基本類型的值源自如下 5種基本數據類型:Undefined、Null、Boolean、Number 和 String。基本類型值和引用類型值具備如下特色:
    • 基本類型值在內存中佔據固定大小的空間,所以被保存在棧內存中;
    • 從一個變量向另外一個變量複製基本類型的值,會建立這個值的一個副本;
    • 引用類型的值是對象,保存在堆內存中;
    • 包含引用類型值的變量實際上包含的並非對象自己,而是一個指向該對象的指針;
    • 從一個變量向另外一個變量複製引用類型的值,複製的實際上是指針,所以兩個變量最終都指向同
      一個對象;
  • 全部變量(包括基本類型和引用類型)都存在於一個執行環境(也稱爲做用域)當中,這個執行環境決定了變量的生命週期,以及哪一部分代碼能夠訪問其中的變量。如下是關於執行環境的幾點總結:
    • 執行環境有全局執行環境(也稱爲全局環境)和函數執行環境之分;
    • 每次進入一個新執行環境,都會建立一個用於搜索變量和函數的做用域鏈;
    • 函數的局部環境不只有權訪問函數做用域中的變量,並且有權訪問其包含(父)環境,乃至全局環境;
    • 全局環境只能訪問在全局環境中定義的變量和函數,而不能直接訪問局部環境中的任何數據;
    • 變量的執行環境有助於肯定應該什麼時候釋放內存;
  • JavaScript 是一門具備自動垃圾收集機制的編程語言,開發人員沒必要關心內存分配和回收問題。能夠對 JavaScript 的垃圾收集例程做以下總結:
    • 離開做用域的值將被自動標記爲能夠回收,所以將在垃圾收集期間被刪除;
    • 「標記清除」是目前主流的垃圾收集算法,這種算法的思想是給當前不使用的值加上標記,而後再回收其內存;
    • 另外一種垃圾收集算法是「引用計數」,這種算法的思想是跟蹤記錄全部值被引用的次數。JavaScript引擎目前都再也不使用這種算法;但在 IE 中訪問非原生 JavaScript 對象(如 DOM 元素)時,這種算法仍然可能會致使問題;
    • 當代碼中存在循環引用現象時,「引用計數」算法就會致使問題;
    • 解除變量的引用不只有助於消除循環引用現象,並且對垃圾收集也有好處。爲了確保有效地回收內存,應該及時解除再也不使用的全局對象、全局對象屬性以及循環引用變量的引用;
相關文章
相關標籤/搜索