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


1.作用域鏈

 1.1.什么是作用域

談起作用域鏈,我們就不得不從作用域開始談起。因為所謂的作用域鏈就是由多個作用域組成的。那么, 什么是作用域呢?

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

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

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的聲明。

  另外還有一點,關於作用域中的聲明提前。

1.1.2.作用域中聲明提前

在上面的getA()函數中,或許你還存在着疑惑,為什么a="undefined"呢,具體原因就是因為作用域中的聲明提前:所以getA()函數和下面的寫法是等價的:

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

既然提到變量的聲明提前,那么只需要搞清楚三個問題即可:

  1.什么是變量

      2.什么是變量聲明

      3.聲明提前到什么時候。

什么是變量?

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

  • 普通變量:凡是用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....如果持續這么下去,內存空間豈不是一直得不到釋放。久而久之,變引起了內存泄露(也是就內存空間不足)。

    

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM