JavaScript中詞法做用域、閉包與跳出閉包

本文從屬於筆者的JavaScript 入門與最佳實踐系列文章,同時,本部份內容也概括於筆者的個人校招準備之路:從Web前端到服務端應用架構這篇綜述。javascript

大部分人都會作錯的經典JS閉包面試題
how-do-javascript-closures-workhtml

Lexical Scope:詞法做用域

functions are executed using the scope chain that was in effect when they were defined前端

通常來講,在編程語言裏咱們常見的變量做用域就是詞法做用域與動態做用域(Dynamic Scope),絕大部分的編程語言都是使用的詞法做用域。詞法做用域注重的是所謂的Write-Time,即編程時的上下文,而動態做用域以及常見的this的用法,都是Run-Time,即運行時上下文。詞法做用域關注的是函數在何處被定義,而動態做用域關注的是函數在何處被調用。JavaScript是典型的詞法做用域的語言,即一個符號參照到語境中符號名字出現的地方,局部變量缺省有着詞法做用域。此兩者的對比能夠參考以下這個例子:java

function foo() {
    console.log( a ); // 2 in Lexical Scope ,But 3 in Dynamic Scope
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;

bar();

看一個實例以下:git

var scope = "I am global";
function whatismyscope(){
   var scope = "I am just a local";
   function func() {return scope;}
   return func;
}

whatismyscope()()

該代碼片最終輸出的結果是:github

I am just a local

Closure

閉包自己是含有自由變量的代碼塊,在JavaScript中咱們經常使用的閉包則是自己的詞法做用域與變量保留相結合的表現,首先回顧下一個基本的詞法做用域的用法:web

function init() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  displayName();
}
init();

函數 init() 建立了一個局部變量 name,而後定義了名爲 displayName() 的函數。displayName() 是一個內部函數——定義於 init() 以內且僅在該函數體內可用。displayName() 沒有任何本身的局部變量,然而它能夠訪問到外部函數的變量,便可以使用父函數中聲明的 name 變量。注意,這裏是直接執行外部的init函數,下面看一個閉包的例子:面試

function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

運行這段代碼的效果和以前的 init() 示例徹底同樣:字符串 "Mozilla" 將被顯示在一個 JavaScript 警告框中。其中的不一樣 — 也是有意思的地方 — 在於 displayName() 內部函數在執行前被從其外圍函數中返回了。這段代碼看起來彆扭卻能正常運行。一般,函數中的局部變量僅在函數的執行期間可用。一旦 makeFunc() 執行事後,咱們會很合理的認爲 name 變量將再也不可用。雖然代碼運行的沒問題,但實際並非這樣的。這個謎題的答案是 myFunc 變成一個閉包了。 閉包是一種特殊的對象。它由兩部分構成:函數,以及建立該函數的環境。環境由閉包建立時在做用域中的任何局部變量組成。在咱們的例子中,myFunc 是一個閉包,由 displayName 函數和閉包建立時存在的 "Mozilla" 字符串造成。編程

避免閉包

在真實的開發中咱們經常會使用閉包這一變量保留的特性來傳遞變量到異步函數中,不過閉包也每每會使程序出乎咱們的控制,譬如在下面這個簡單的循環中,咱們本但願可以打印出0~9這幾個數:閉包

for(var i = 0;i < 10;i++){
   setTimeout(()=>{console.log(i),1000})
}

不過全部輸入的i的值都是10,這與咱們的指望產生了很大的誤差。所以咱們在部分狀況下須要破壞閉包而獲取真實的變量值。

將異步獲取值保留到新增的閉包中

咱們能夠考慮加一層閉包,將i以函數參數形式傳遞給內層函數:

function init3() {     
      var pAry = document.getElementsByTagName("p");     
      for( var i=0; i<pAry.length; i++ ) {     
       (function(arg){         
           pAry[i].onclick = function() {         
              alert(arg);     
           };     
       })(i);//調用時參數     
      }     
    }

或者在新增的閉包中將i以局部變量形式傳遞給內部函數中:

function init4() {     
      var pAry = document.getElementsByTagName("p");     
      for( var i=0; i<pAry.length; i++ ) {       
        (function () {     
          var temp = i;//調用時局部變量     
          pAry[i].onclick = function() {       
            alert(temp);       
          }     
        })();     
      }     
    }

將變量值保留到做用域以外

在DOM環境中,咱們能夠將變量值存儲到要操做的DOM對象中:

function init() {     
      var pAry = document.getElementsByTagName("p");     
      for( var i=0; i<pAry.length; i++ ) {     
         pAry[i].i = i;     
         pAry[i].onclick = function() {     
            alert(this.i);     
         }     
      }     
    }

也能夠將變量i保存在匿名函數自己:

function init2() {     
      var pAry = document.getElementsByTagName("p");     
      for( var i=0; i<pAry.length; i++ ) {       
       (pAry[i].onclick = function() {     
            alert(arguments.callee.i);     
        }).i = i;     
      }     
    }

相關文章
相關標籤/搜索