天天看點

淺析 JavaScript 中的 “閉包”

#javascript closure(閉包)

<a href="https://www.zhihu.com/question/27460623/answer/36747015">擴充: 什麼是 first-class ?</a>

first-class 指的是可以作為參數傳遞,可以使用<code>return</code>裡傳回,可以賦給變量的類型

second-class 該等級類型的值可以作為參數傳遞,但是不能從子程式裡傳回,也不能賦給變量

third-class 該等級類型的值連作為參數傳遞也不行

從概念上來看,維基百科的解釋更加偏向于理論層面的抽象概念,而百度百科的定義則偏重實際編碼中的實體。

那麼閉包(closure)究竟是什麼?

以 javascript 語言為例,談一談閉包。

首先,在 javascript 中幾乎所有類型都可為 first-class 類型 (包括function), 是以,javascript 中閉包是确定可構造出來的。

由于閉包 (closure)本身與作用域(scope)息息相關,是以有必要先談談 js 的作用域。

與衆多語言不同的是: javascript 預設并無塊級作用域,也就是說在花括号<code>{}</code>不能形成一個獨立的作用域(例如 java、c++ 中的作用域)。javascript是函數級作用域, 也就是每次建立一個 function 才會形成一個新的 “塊級“ 作用域。

例如:

假設 javascript 有塊級作用域,明顯<code>if</code>語句中将建立一個局部的變量<code>scope</code>, 在這個塊中會覆寫全局定義的<code>scope</code>值, 是以會首先輸出 “local”。但這時候塊中的局部變量并不會修改在這個塊外定義的變量 <code>scope</code>, 第二個<code>console</code>應該輸出 “global”。

可是實際上沒有這樣, 兩個<code>console</code>都會輸出 “local” ,效果和去掉了<code>{}</code>相同。

是以 js 沒有塊級作用域。

所謂函數作用域就是說:建立一個新的函數時,在函數體内部會生成新的局部作用域,其中的變量在聲明它們的函數體以及這個函數體嵌套的任意函數體内都是有定義的。

比如:

全局定義變量<code>scope</code>, 函數内部又定義一次<code>scope</code>, 那麼在函數内部的作用域中,舊的定義會被覆寫。 外部的仍然輸出 “global”。

來一個稍微複雜的函數作用域的例子吧:

回顧一下前文中的概念:閉包 是指可以包含自由(未綁定到特定對象)變量的代碼塊;這些變量不是在這個代碼塊内或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義(局部變量)。

上面的例子中,函數<code>f2</code>就是一個閉包,原因是:

<code>f2</code>中包含自由變量<code>a</code>;

<code>a</code>不是在<code>f2</code>的代碼塊内定義;

<code>a</code>不是在任何全局上下文中定義;

<code>a</code>是在函數<code>f1</code>的内部定義(局部變量),函數<code>f1</code>的内部即就是定義<code>f2</code>這個代碼塊的環境

由于在javascript語言中,隻有函數内部的子函數才能讀取局部變量,是以可以把閉包簡單了解成“能夠讀取其他函數内部變量的子函數”。

當函數<code>f1</code>的内部函數<code>f2</code>被函數<code>f1</code>外的一個變量引用的時候,就建立了一個閉包。

以最經典的<code>for</code>循環為例. 大家可以試試下面這段代碼,取自javascript 秘密花園循環中的閉包

首先說說為什麼最終輸出的是 10 次 10, 而不是你想象中的 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

因為<code>settimeout</code>是異步的!

你可以想象由于<code>settimeout</code>是異步的,是以我們将這個<code>for</code>循環拆成 2 個部分,第一個部分專門處理 <code>i</code>值的變化,第二個部分專門來做<code>settimeout</code>。是以我們可以得到如下代碼:

由于循環中的變量 <code>i</code>一直在變, 最終會變成 10, 而循環每每執行<code>settimeout</code>時, 其中的方法還隻是裝入延時執行的隊列,沒有真正運作, 等真正到時間執行時, <code>i </code>的值已經變成 10 了。<code>i</code> 變化的整個過程是瞬間完成的, 總之同步比異步要快, 就算<code>settimout</code>是 0 毫秒也一樣, 會先于你執行完成。

如何解決?閉包!

如果我們定義一個外部函數, 讓 <code>i</code> 作為參數傳入即可 “閉包” 我們要的變量了。

那麼為什麼<code>settimeout</code>中匿名<code>function</code>沒有形成閉包呢?

因為<code>settimeout</code>中的匿名<code>function</code>沒有将 <code>i</code> 作為參數傳入來固定這個變量的值,讓其保留下來,而是直接引用了外部作用域中的 <code>i</code>,是以<code> i</code> 變化時,也影響到了匿名<code>function</code>。

一個經典的閉包面試題:

由于閉包會使得函數中的變量會被更長時間儲存在記憶體中,消耗很大,是以不能濫用閉包,否則會造成網頁的性能問題,在ie中更是可能導緻記憶體洩露。解決方法是,在退出函數之前,将不使用的局部變量全部删除。

閉包會在父函數外部,改變父函數内部變量的值。是以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(public method),把内部變量當作它的私有屬性(private value),這時一定要小心,不要随便改變父函數内部變量的值。

繼續閱讀