談談JavaScript的詞法環境和閉包(一)

一個資深的同事在我出發去面試前告誡我,問JS知識點的時候千萬別主動提閉包,它就是一個坑啊!坑啊!啊!html

閉包確實是js的難點和重點,其實也沒那麼可怕,關鍵是機制的理解,能夠和函數一塊兒單獨拿出來講說,其實關於閉包的解釋不少文章都寫得比較詳細了,這篇文章就做爲本身學習過程的記錄吧。react

閉包的概念

首先明確一下閉包的概念:面試

MDN (Mozilla Develop Network) 上的對閉包的定義:編程

閉包是指可以訪問自由變量的函數 (變量在本地使用,但在閉包中定義)。換句話說,定義在閉包中的函數能夠「記憶」它被建立時候的環境。segmentfault

分析:數組

  • 閉包由函數和與其相關的引用環境(詞法環境)的組合而成閉包

  • 閉包容許函數訪問其引用環境(詞法環境)中的變量(又稱自由變量)編程語言

  • 廣義上來講,全部JS的函數均可以稱爲閉包,由於JS函數在建立時保存了當前的詞法環境函數

仍是很拗口有木有,一臉懵逼的時候就應該從基礎的概念開始找,因此咱們來談談詞法環境。學習

詞法環境的概念

定義(摘自wiki百科)。

詞法環境是一個用於定義特定變量和函數標識符在ECMAScript代碼的詞法嵌套結構上關聯關係的規範類型。一個詞法環境由一個環境記錄項和可能爲空的外部詞法環境引用構成。

變量做用域

通常來講,在編程語言中都有變量做用域的概念,每一個變量都有本身的生命週期和做用範圍。
做用域有兩種解析方式:

  1. 靜態做用域
    又稱爲詞法做用域,在編譯階就能夠決定變量的引用,由程序定義的位置決定,和代碼執行順序無關,用嵌套的方式解析。

  2. 動態做用域
    在程序運行時候,和代碼的執行順序決定。用動態棧動態管理。

var x = 10;
function getX() {
    alert(x);
}
function foo() {
    var x = 20;
    getX();
}
foo();
  1. 在靜態做用域下:
    全局做用域下有x, getX, foo三個變量,getXfoo都有本身的做用域。執行foo函數的時候,getX()被執行,可是getX的定義位置在全局做用域下的,取到的x是10,而不是20

  2. 在動態做用域下:
    運行這段代碼時,先把x=10getXfoo按順序壓棧,而後執行foo函數,在函數中把x=20壓棧,而後執行getX(),此時距離棧頂最近的x值爲20,所以alert的值也是20

JavaScript使用的變量做用域是靜態做用域。JS中做用域簡單分爲兩部分:全局做用域和函數做用域。ES5中使用詞法環境管理靜態做用域。

詞法環境包含兩部分

  • 環境記錄

    • 形參

    • 函數聲明

    • 變量

    • 其它...

  • 對外部詞法環境的引用(outer)

環境記錄初始化

一段JS代碼執行以前,會對環境記錄進行初始化(聲明提早),即將函數的形參、函數聲明和變量先放入函數的環境記錄中,特別須要注意的是:

如下面這段代碼爲例,解析環境記錄初始化和代碼執行的過程:

var x = 10;
function foo(y) {
    var z  = 30;
    function bar(q) {
        return x + y + z + q;
    }
    return bar;
}
var bar = foo(20);
bar(40);
  • step1:初始化全局環境

全局環境
環境記錄(record) foo: <function>
x: undefined(聲明變量而非定義變量)
bar: undefined(聲明變量而非定義變量)
外部環境(outer) null
  • step2: 執行x=10

全局環境
環境記錄(record) foo: <function>
x: 10()
bar: undefined(聲明變量而非定義變量)
外部環境(outer) null
  • step3:執行var bar = foo(20)語句以前,將foo函數的環境記錄初始化

foo 環境
環境記錄(record) y: 20(定義形參)
bar: <function>
z: undefined(聲明變量而非定義變量)
外部環境(outer) 全局環境
  • step4:執行var bar = foo(20)語句,變量bar接收foo函數中返回的bar函數

foo 環境
環境記錄(record) y: 20
bar: <function>
z: 30(定義z)
外部環境(outer) 全局環境
  • step5:執行bar函數以前,初始化bar的詞法環境

bar環境
環境記錄(record) q: 40(定義形參q)
外部環境(outer) foo環境
  • step6:在foo函數內執行bar函數

x + y + z + q = 10 + 20 + 30 + 40 = 100

其實說了那麼多,也是想強調一點:形參的值在環境初始化的時候就賦值了!所以形參的做用之一就是保存外部變量的值

一道閉包的面試題

查了一下關於閉包的面試題,用具體的例子說明閉包的應用場景。
最多見的答案來自於《JavaScript高級程序設計(第3版)》p181:

例子:

function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        }
    }
    return result;
}

這個函數返回了長度爲10的函數數組,假設咱們調用函數數組的第3個函數,在控制檯中輸入creacteFunctions()[2](),即執行函數數組裏面的第三個函數,creacteFunctions()返回函數數組,[2]是取第三個函數的引用,最後一個()是執行第三個函數,返回結果卻並非預期的2,而是10.

所以,爲了可以讓閉包的行爲符合預期,須要建立一個匿名函數:

function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function (num) {
            return function() {
                return num;
            }
        }(i);
    }
    return result;
}

此時在控制檯中輸入creacteFunctions()[2](),即執行函數數組裏面的第三個函數,返回的就是預期中的2
有了詞法環境的初始化過程,這裏也就很是容易理解了。匿名函數的形參num保存了每次執行的i的值。在function(num){...}(i)這個結構中,i做爲形參num的實際值執行這個匿名函數,所以每次循環中的num直接初始化爲i的值。
爲了更清楚的提取這部分結構,咱們將匿名函數命名爲helper:

var helper = function (num) {
    return function() {
        return num;
    }
}

用helper函數重寫第二段代碼:

var helper = function (num) {
    return function() {
        return num;
    }
}
function creacteFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = helper(i);
    }
    return result;
}

在控制檯中輸入creacteFunctions()[2](),輸出的也是預期中的2

未完待續哦,閉包能夠講的東西太多啦!

一句話總結

真正理解了做用域也就理解了閉包.

相關文章
相關標籤/搜索