JavaScript 變數、作用域及記憶體詳解

trigkit4發表於2016-09-06

基本型別值有:undefined,NUll,Boolean,Number和String,這些型別分別在記憶體中佔有固定的大小空間,他們的值儲存在棧空間,我們通過按值來訪問的。

(1)值型別:數值、布林值、null、undefined。
(2)引用型別:物件、陣列、函式。

如果賦值的是引用型別的值,則必須在堆記憶體中為這個值分配空間。由於這種值的大小不固定(物件有很多屬性和方法),因此不能把他們儲存到棧記憶體中。但記憶體地址大小是固定的,因此可以將記憶體地址儲存在棧記憶體中。

<script type="text/javascript”>
var box = new Object();  //建立一個引用型別
var box = "trigkit4";   //基本型別值是字串
box.age = 21;    //基本型別值新增屬性很怪異,因為只有物件才可以新增屬性。
alert(box.age);  //不是引用型別,無法輸出;
</script>

簡而言之,堆記憶體存放引用值,棧記憶體存放固定型別值。“引用”是一個指向物件實際位置的指標。

在這裡需注意的是,引用指向的是具體的物件,而不是另一個引用。

這裡的物件可以是字串物件,數字物件,陣列物件等

<script type="text/javascript">
    var man = new Object();//man指向了棧記憶體的空間地址
    man.name = "Jack";
    var man2 = man;//man2獲得了man的指向地址

    alert(man2.name);//兩個都彈出Jack
    alert(man.name);
</script>

複製變數值

再看下面這個例子:

<script type="text/javascript">
    var man = new Object();//man指向了棧記憶體的空間地址
    man.name = "Jack";
    var man2 = man;//man2獲得了man的指向地址

    man2.name = "ming";//因為他們都指向同一個object,同一個name,不管修改誰,大家都修改了
    alert(man2.name);//兩個都彈出ming
    alert(man.name);
</script>

由以上可以得出:在變數複製方面,基本型別和引用型別也有所不同,基本型別複製的是值本身,而引用型別複製的是地址。

傳遞引數

ECMAScript中,所有函式的引數都是按值傳遞的,

<script type="text/javascript">
     function box(num){      //按值傳遞
         num+=10;
         return num;
     }

     var num = 10;
     var result = box(num);
     alert(result);  //如果是按引用傳遞,那麼函式裡的num會成為類似全域性變數,把外面的number替換掉
     alert(num);    //也就是說,最後應該輸出20(這裡輸出10)
</script>

js沒有按引用傳遞的,如果存在引用傳遞的話,那麼函式內的變數將是全域性變數,在外部也可以訪問。但這明顯是不可能的。

執行環境及作用域

執行環境是javascript中最為重要的概念之一,執行環境定義了變數或函式有權訪問其他資料。

全域性執行環境是最外圍的執行環境,在web瀏覽器中,全域性執行環境是window物件,因此,所有的全域性變數的函式都是作為window的屬性和方法建立的。

<script type="text/javascript">
      var name = "Jack";           //定義全域性變數
      function setName(){
          return "trigkit4";
      }

      alert(window.name);        //全域性變數,最外圍,屬於window屬性
      alert(window.setName());  //全域性函式,最外圍,屬於window方法
</script>

當執行環境內的程式碼執行完畢後,該環境被銷燬,儲存其中的變數和函式也隨之銷燬,如果是全域性環境,需所有程式執行完畢或網頁完畢後才會銷燬。

去掉var的區域性變數

<script type="text/javascript">
      var name = "Jack";
      function setName(){
          name = "trigkit4";   //去掉var變成了全域性變數
      }

      setName();
      alert(name);//彈出trigkit4
</script>

通過傳參,也是區域性變數

<script type="text/javascript">
      var name = "Jack";
      function setName(name){    //通過傳參,也是區域性變數
          alert(name);
      }

      setName("trigkit4");//彈出trigkit4
      alert(name);//彈出Jack
</script>

函式體內還包含函式,只有這個函式才可以訪問內一層的函式

<script type="text/javascript">
     var name = "Jack";
      function setName(){
          function setYear(){    //setYear()方法的作用域在setName()內
              return 21;
          }
      }
      alert(setYear());//無法訪問,出錯 
</script>

可以通過如下方法進行訪問:

<script type="text/javascript">
     var name = "Jack";
      function setName(){
          function setYear(){    //setYear()方法的作用域在setName()內
              return 21;
          }
          return setYear();
      }
      alert(setName()); //彈出21
</script>

再一個作用域例子:

<script type="text/javascript">
     var name = "Jack";
      function setName(){
          function setYear(){    //setYear()方法的作用域在setName()內
              var b = "hi";     //變數b的作用域在setYear()內
              return 21;
          }
          alert(b);//無法訪問 
      }
</script>

當程式碼在一個環境中執行的時候,就會形成一種叫做作用域鏈的東西,它的用途是保證對執行環境中有訪問許可權的變數和函式進行有序訪問(指按照規則層次來訪問),作用域鏈的前端,就是執行環境的變數物件。

作用域

變數沒有在函式內宣告或者宣告的時候沒有帶var就是全域性變數,擁有全域性作用域,window物件的所有屬性擁有全域性作用域;在程式碼任何地方都可以訪問,函式內部宣告並且以var修飾的變數就是區域性變數,只能在函式體內使用,函式的引數雖然沒有使用var但仍然是區域性變數。

沒有塊級作用域

// if語句:

<script type="text/javascript">
if(true){                        //if語句的花括號沒有作用域的功能。

var box = "trigkit4";
}
alert(box);//彈出 trigkit4
</script>

for迴圈語句也是如此。

變數的查詢

在變數的查詢中,訪問區域性變數要比全域性變數來得快,因此不需要向上搜尋作用域鏈。

如下例子:

<script type="text/javascript">
     var name = "Jack";
      function setName(){
           var name = "trigkit4";
           return name;  //從底層向上搜尋變數
    }
    alert(setName());      
</script>

每個環境都可以向上搜尋作用域鏈,以查詢變數和函式名;但任何環境都不能通過向下搜尋作用域鏈而進入另一個執行環境。在這裡,如果去掉var name = "trigkit4",那麼將彈出“Jack”

記憶體問題

javascript具有自動垃圾回收機制,一旦資料不再使用,可以將其設為”null”來釋放引用

迴圈引用

一個很簡單的例子:一個DOM物件被一個Javascript物件引用,與此同時又引用同一個或其它的Javascript物件,這個DOM物件可能會引發記憶體洩露。這個DOM物件的引用將不會在指令碼停止的時候被垃圾回收器回收。要想破壞迴圈引用,引用DOM元素的物件或DOM物件的引用需要被賦值為null

閉包

在閉包中引入閉包外部的變數時,當閉包結束時此物件無法被垃圾回收(GC)。

var a = function() {
  var largeStr = new Array(1000000).join('x');
  return function() {
    return largeStr;
  }
}();

DOM洩露

當原有的COM被移除時,子結點引用沒有被移除則無法回收。

var select = document.querySelector;
var treeRef = select('#tree');

//在COM樹中leafRef是treeFre的一個子結點
var leafRef = select('#leaf'); 
var body = select('body');

body.removeChild(treeRef);

//#tree不能被回收入,因為treeRef還在
//解決方法:
treeRef = null;

//tree還不能被回收,因為葉子結果leafRef還在
leafRef = null;

//現在#tree可以被釋放了。

Timers計(定)時器洩露

定時器也是常見產生記憶體洩露的地方:

for (var i = 0; i < 90000; i++) {
  var buggyObject = {
    callAgain: function() {
      var ref = this;
      var val = setTimeout(function() {
        ref.callAgain();
      }, 90000);
    }
  }

  buggyObject.callAgain();
  //雖然你想回收但是timer還在
  buggyObject = null;
}

除錯記憶體

Chrome自帶的記憶體除錯工具可以很方便地檢視記憶體使用情況和記憶體洩露,在 Timeline -> Memory 點選record即可。

相關文章