全網最通透的 “閉包” 認知 · 跨越語言

作者 | 小碼甲       責編 | 歐陽姝黎

閉包作爲前端面試的必考題目,常讓 1-3 年工作經驗的 JavaScripter 感到困惑,其實主流語言都有閉包。

今天我們深入聊一聊 [閉包], 查缺補漏!

  1. 以面試題 ·  投石問路 

  2. 以 C# 閉包 ·  庖丁解牛 

  3. 跨越語言 · 追本溯源  

    • 頭等函數   

    • 自由變量   

    • 詞法作用域

  4. 答面試題 · 返璞歸真

投石問路

===========

調用下面函數,輸出結果是什麼樣呢?

static void Closure1()
{
   for (int i = 0; i < 5; i++)
   {                 
     Task.Run(()=> Console.WriteLine(i));
   }
}
//  輸出:
5
5
5
5
5

是不是很意外?如何輸出原本預期的 0,1,2,3,4。

bingo, 加一個臨時變量就可以解決。

static void Closure2()
{
   for (int i = 0; i < 5; i++)
   {
      int j = i;
      Task.Run(() => Console.WriteLine(j));
   }
}
// 輸出:
3
0
1
4
2
//  多次執行的結果不一樣,但是總是會保持輸出 0,1,2,3,4 的亂序組合

以上閉包概念涉及到 Task 任務,理解起來更加複雜,我們來看一個基礎的 C# 閉包。

庖丁解牛

一個閉包就是一個 “捕獲” 了其生成的環境中、所引用的自由變量的函數。
這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。

 static void Closure()
 {
    var x = 1;
    Action action= () =>
      {
         var y = 1;
         var result = x + y;
         Console.WriteLine(result);
         x++;
      };
    action();
    action();
}
// 輸出:
  2
  3

我們首先定義了一個委託 action,它引用了 “x” 變量(x 變量既不是入參,也不是委託內的局部變量), 這個變量將被 action" 捕獲”,被自動添加到 action 的運行環境。

當我們執行 action 時,原始的 “x” 已經脫離了它被引用時的作用域環境,但是兩次執行能輸出 2,3 說明它脫離原引用環境仍然能用。

當你在代碼調試器(debugger)裏觀察 “action” 時,可以看到 C# 編譯器爲我們創建了一個 Target 屬性,裏面封裝了 x 變量:

源碼追溯,委託繼承自 Delegate 抽象類,Delegate 類有個 Target 屬性 (獲取當前委託調用實例方法的實例類) 。
至此可以猜想: 我們每次執行委託,實際是是執行某個匿名類上的實例方法。

都說了閉包是跨越語言的設計, 至少我知道 JavaScript C# Go 都有閉包。

追本溯源

閉包是詞法閉包的簡稱,維基百科上是這樣定義的:
“在計算機編程中,閉包是在詞法環境中綁定自由變量的頭等函數”

頭等函數

頭等函數 (First Class) 意味着語言將其視爲第一類數據類型的函數, 意味着你可以將函數分配給一個變量 (或作爲參數傳遞),然後像正常函數一樣調用。

很明顯,C# 常使用的委託(C# 委託的演進:匿名函數 -->lambda 表達式)是頭等函數。

Func<string,string> myFunc = delegate(string var1)
                                {
                                    return "some value";   
                                };
Func<string,string> myFunc = var1 => "some value";  
string myVar = myFunc("something");

自由變量

自由變量是在匿名函數 / lambda 表達式中被引用的變量,它不是函數的參數也不是函數的局部變量。

var myVar = "this is good";
Func<string,string> myFunc = delegate(string var1)
                                {
                                    return var1 + myVar;   
                                };

詞法作用域引用的自由變量,注意,是引用自由變量,並不是使用當時自由變量的值。

☺️通俗點, 就是告知這個變量環境,我這個匿名函數等會執行時要用到這個變量;如果我沒被銷燬,你不能銷燬我引用的自由變量。

我們再回過頭來看 [投石問路] 的面試題。

返璞歸真

首先你要知道:循環內開啓的 Task 任務,並不保證執行順序。

Demo1:輸出 5,5,5,5,5

這是因爲在 for 循環內,開啓了 5 個 Task 任務,每個任務均引用了自由變量 i (相對於每個任務執行環境,i 屬於全局變量);
for 循環先執行完,i=5, 5 個任務輸出時自然得到值 5。

爲什麼加上臨時變量就能輸出 "預期"?

Demo2:輸出亂序的 0,1,2,3,4

這是因爲 在 for 循環內,每次循環 j 均拷貝自當時的 i,每個任務均引用了自由變量 j (每個任務執行環境均維護了一個變量 j);
任務亂序執行時依舊能獲取本任務綁定的自由變量 j。

有這樣的認知,理解 JavaScript 閉包也就不難了。

總結

本文屏蔽語言差異,理清了 [閉包] 的概念核心: 頭等函數、自由變量,不僅能幫助我們應對多語種有關閉包的面試題, 也幫助我們瞭解 [閉包] 在通用語言中的設計初衷。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2dM3wm_tn4aQbliSTpXbzA