全網最通透的 “閉包” 認知 · 跨越語言
作者 | 小碼甲 責編 | 歐陽姝黎
閉包作爲前端面試的必考題目,常讓 1-3 年工作經驗的 JavaScripter 感到困惑,其實主流語言都有閉包。
今天我們深入聊一聊 [閉包], 查缺補漏!
-
以面試題 · 投石問路
-
以 C# 閉包 · 庖丁解牛
-
跨越語言 · 追本溯源
• 頭等函數
• 自由變量
• 詞法作用域
-
答面試題 · 返璞歸真
投石問路
===========
調用下面函數,輸出結果是什麼樣呢?
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