JavaScript系列----做用域鏈和閉包

1.做用域鏈

 1.1.什麼是做用域

談起做用域鏈,咱們就不得不從做用域開始談起。由於所謂的做用域鏈就是由多個做用域組成的。那麼, 什麼是做用域呢?javascript

 1.1.1做用域是一個函數在執行時期的執行環境。

每個函數在執行的時候都有着其特有的執行環境,ECMAScript標準規定,在javascript中只有函數才擁有做用域。換句話,也就是說,JS中不存在塊級做用域。好比下面這樣:html

function getA() {
  if (false) {
    var a = 1;
  }
  console.log(a);  //undefined
}
getA();
function getB() {
  console.log(b);
}
getB();    // ReferenceError: b is not defined

  上面的兩段代碼,區別在於 :getA()函數中,有變量a的聲明,而getB()函數中沒有變量b的聲明。java

  另外還有一點,關於做用域中的聲明提早。閉包

1.1.2.做用域中聲明提早

在上面的getA()函數中,或許你還存在着疑惑,爲何a="undefined"呢,具體緣由就是由於做用域中的聲明提早:因此getA()函數和下面的寫法是等價的:函數

function getA(){
   var a;
  if(false){
    a=1
    };
  console.log(a);
}

既然提到變量的聲明提早,那麼只須要搞清楚三個問題便可:post

  1.什麼是變量this

      2.什麼是變量聲明spa

      3.聲明提早到何時。指針

什麼是變量?code

  變量包括兩種,普通變量和函數變量。 

  • 普通變量:凡是用var標識的都是普通變量。好比下面 :
    var x=1;               
    var object={};
    var  getA=function(){};  //以上三種均是普通變量,可是這三個等式都具備賦值操做。因此,要分清楚聲明和賦值。聲明是指 var x; 賦值是指 x=1; 

     

  • 函數變量:函數變量特指的是下面的這種,fun就是一個函數變量。
    function fun(){} ;// 這是指函數變量. 函數變量通常也說成函數聲明。

     
    相似下面這樣,不是函數聲明,而是函數表達式

    var getA=function(){}      //這是函數表達式
    var getA=function fun(){}; //這也是函數表達式,不存在函數聲明。關於函數聲明和函數表達式的區別,詳情見javascript系列---函數篇第二部分

 

什麼是變量聲明?

     變量有普通變量和函數變量,因此變量的聲明就有普通變量聲明和函數變量聲明。

  •  普通變量聲明
    var x=1; //聲明+賦值
    var object={};   //聲明+賦值

     上面的兩個變量執行的時候老是這樣的

    var x = undefined;      //聲明
    var object = undefined; //聲明
    x = 1;                  //賦值
    object = {};            //賦值

    關於聲明和賦值,請注意,聲明是在函數第一行代碼執行以前就已經完成,而賦值是在函數執行時期纔開始賦值。因此,聲明老是存在於賦值以前。並且,普通變量的聲明時期老是等於undefined.

  • 函數變量聲明
    函數變量聲明指的是下面這樣的:
    function getA(){}; //函數聲明

聲明提早到何時?

        全部變量的聲明,在函數內部第一行代碼開始執行的時候就已經完成。-----聲明的順序見1.2做用域的組成

1.2.做用域的組成

函數的做用域,也就是函數的執行環境,因此函數做用域內確定保存着函數內部聲明的全部的變量。

一個函數在執行時所用到的變量無外乎來源於下面三種:

1.函數的參數----來源於函數內部的做用域

2.在函數內部聲明的變量(普通變量和函數變量)----也來源於函數內部做用域

3.來源於函數的外部做用域的變量,放在1.3中講。

好比下面這樣:

var x = 1;
function add(num) () {
  var y = 1; 
  return x + num + y;   //x來源於外部做用域,num來源於參數(參數也屬於內部做用域),y來源於內部做用域。
}

        那麼一個函數的做用域究竟是什麼呢?

   在一個函數被調用的時候,函數的做用域纔會存在。此時,在函數尚未開始執行的時候,開始建立函數的做用域:

  函數做用域的建立步驟:

          1. 函數形參的聲明。

          2.函數變量的聲明

          3.普通變量的聲明。  

          4.函數內部的this指針賦值

             ......函數內部代碼開始執行!  

         因此,在這裏也解釋了,爲何說函數被調用時,聲明提早,在建立函數做用域的時候就會先聲明各類變量。

   關於變量的聲明,這裏有幾點須要強調

   1.函數形參在聲明的時候已經指定其形參的值。  

function add(num) {
  var num;
  console.log(num);   //1
}
add(1);

 

  2.在第二步函數變量的生命中,函數變量會覆蓋之前聲明過的同名聲明。

function add(num1, fun2) {
  function fun2() {
    var x = 2;
  }
  console.log(typeof num1); //function  
  console.log(fun2.toString()) //functon fun2(){ var x=2;}
}
add(function () {
}, function () {
  var x = 1
}); 

 

3.  在第三步中,普通變量的聲明,不會覆蓋之前的同名參數

function add(fun,num) {
  var fun,num;
  console.log(typeof fun) //function
  console.log(num);      //1
}
add(function(){},1);

 

   在全部的聲明結束後,函數纔開始執行代碼!!! 

 

 1.3.做用域鏈的組成

 

   在JS中,函數的能夠容許嵌套的。即,在一個函數的內部聲明另外一個函數

    相似這樣: 

function A(){
  var  a=1;
   function B(){  //在A函數內部,聲明瞭函數B,這就是所謂的函數嵌套。
         var b=2;   
   }
}

 

   對於A來講,A函數在執行的時候,會建立其A函數的做用域, 那麼函數B在建立的時候,會引用A的做用域,相似下面這樣

  

函數B在執行的時候,其做用域相似於下面這樣:

    從上面的兩幅圖中能夠看出,函數B在執行的時候,是會引用函數A的做用域的。因此,像這種函數做用域的嵌套就組成了所謂的函數做用域鏈。當在自身做用域內找不到該變量的時候,會沿着做用域鏈逐步向上查找,若在全局做用域內部仍找不到該變量,則會拋出異常。

2.什麼是閉包 

閉包的概念:有權訪問另外一個做用域的函數。

 這句話就告訴咱們,第一,閉包是一個函數。第二,閉包是一個可以訪問另外一個函數做用域。

那麼,相似下面這樣,

function A(){

  var a=1;
  
  function B(){  //閉包函數,函數b可以訪問函數a的做用域。因此,像相似這麼樣的函數,咱們就稱爲閉包
  
  }
}

 

因此,建立閉包的方式就是在一個函數的內部,建立另一個函數。那麼,當外部函數被調用的時候,內部函數也就隨着建立,這樣就造成了閉包。好比下面。

var fun = undefined;
function a() {
  var a = 1;
  fun = function () {
  }
}

 3.閉包所引發的問題

其實,理解什麼是閉包並不難,難的是閉包很容易引發各類各樣的問題。

3.1.變量污染

看下面的這道例題:

var funB,
funC;
(function() {
  var a = 1;
  funB = function () {
    a = a + 1;
    console.log(a);
  }
  funC = function () {
    a = a + 1;
    console.log(a);
  }
}());
funB();  //2
funC();  //3.

 

對於 funB和funC兩個閉包函數,不管是哪一個函數在運行的時候,都會改變匿名函數中變量a的值,這種狀況就會污染了a變量。

兩個函數的在運行的時候做用域以下圖:


這這幅圖中,變量a能夠被函數funB和funC改變,就至關於外部做用域鏈上的變量對內部做用域來講都是靜態的變量,這樣,就很容易形成變量的污染。還有一道最經典的關於閉包的例題:

var array = [
];
for (var i = 0; i < 10; i++) {
  var fun = function () {
    console.log(i);
  }
  array.push(fun);
}
var index = array.length;
while (index > 0) {
  array[--index]();
} //輸出結果 全是10;

想這種相似問題產生的根源就在於,沒有注意到外部做用域鏈上的全部變量均是靜態的。

因此,爲了解決這種變量的污染問題---而引入的閉包的另一種使用方式。

 那種它是如何解決這種變量污染的呢?  思想就是: 既然外部做用域鏈上的變量時靜態的,那麼將外部做用域鏈上的變量拷貝到內部做用域不就能夠啦!! 具體怎麼拷貝,固然是經過函數傳參的形式啊。

以第一道例題爲例:

var funB,funC;
(function () {
  var a = 1;
  (function () {
    funB = function () {
      a = a + 1;
      console.log(a);
    }
  }(a));
  (function (a) {
    funC = function () {
      a = a + 1;
      console.log(a);
    }
  }(a));
}());
funB()||funC();  //輸出結果全是2 另外也沒有改變做用域鏈上a的值。

  在函數執行時,內存的結構如圖所示:

由圖中內存結構示意圖可見,爲了解決閉包的這種變量污染的問題,而加了一層函數嵌套(經過匿名函數自執行),這種方式延長了閉包函數的做用域鏈。

 

3.2.內存泄露

內存泄露其實嚴格來講,就是內存溢出了,所謂的內存溢出,當時就是內存空間不夠用了啊。

那麼,閉包爲何會引發內存泄露呢?

var fun = undefined;
function A() {
  var a = 1;
  fun = function () {
  }
}

 

看上面的例題,只要函數fun存在,那麼函數A中的變量a就會一直存在。也就是說,函數A的做用域一直得不到釋放,函數A的做用域鏈也不能獲得釋放。若是,做用域鏈上沒有不少的變量,這種犧牲還無關緊要,可是若是牽扯到DOM操做呢?

var element = document.getElementById('myButton');
(function () {
  var myDiv = document.getElementById('myDiv')
  element.onclick = function () {
    //處理程序
  }
}())

 

像這樣,變量myDiv若是是一個佔用內存很大的DOM....若是持續這麼下去,內存空間豈不是一直得不到釋放。長此以往,變引發了內存泄露(也是就內存空間不足)。

相關文章
相關標籤/搜索