全網(wǎng)最通透的“閉包”認(rèn)知 · 跨越語言
閉包作為前端面試的必考題目,常讓1-3年工作經(jīng)驗的JavaScripter感到困惑,其實主流語言都有閉包。
今天我們深入聊一聊[閉包], 查缺補(bǔ)漏!
1. 以面試題 · 投石問路 2. 以C#閉包 · 庖丁解牛 3. 跨越語言 ·追本溯源 ? 頭等函數(shù) ?自由變量 ?詞法作用域4. 答面試題 · 返璞歸真
1. 投石問路
調(diào)用下面函數(shù),輸出結(jié)果是什么樣呢?
static void Closure1(){for (int i = 0; i < 5; i++){Task.Run(()=> Console.WriteLine(i));}}// 輸出:55555
是不是很意外?如何輸出原本預(yù)期的 0,1,2,3,4。
bingo, 加一個臨時變量就可以解決。
static void Closure2(){for (int i = 0; i < 5; i++){int j = i;Task.Run(() => Console.WriteLine(j));}}// 輸出:30142// 多次執(zhí)行的結(jié)果不一樣,但是總是會保持輸出 0,1,2,3,4 的亂序組合
以上閉包概念涉及到 Task任務(wù),理解起來更加復(fù)雜,我們來看一個基礎(chǔ)的C#閉包。
2. 庖丁解牛
一個閉包就是一個“捕獲”了其生成的環(huán)境中、所引用的自由變量的函數(shù)。
這個被引用的自由變量將和這個函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。
static void Closure(){var x = 1;Action action= () =>{var y = 1;var result = x + y;Console.WriteLine(result);x++;};action();action();}// 輸出:23
我們首先定義了一個委托action,它引用了“x”變量(x變量既不是入?yún)ⅲ膊皇俏袃?nèi)的局部變量), 這個變量將被action"捕獲”,被自動添加到action 的運(yùn)行環(huán)境。
當(dāng)我們執(zhí)行action時,原始的“x”已經(jīng)脫離了它被引用時的作用域環(huán)境,但是兩次執(zhí)行能輸出2,3 說明它脫離原引用環(huán)境仍然能用。
當(dāng)你在代碼調(diào)試器(debugger)里觀察“action”時,可以看到C#編譯器為我們創(chuàng)建了一個Target屬性,里面封裝了 x 變量:

源碼追溯,委托繼承自
Delegate抽象類,Delegate類有個Target屬性(獲取當(dāng)前委托調(diào)用實例方法的實例類) 。
至此可以猜想: 我們每次執(zhí)行委托,實際是是執(zhí)行某個匿名類上的實例方法。
都說了閉包是跨越語言的設(shè)計, 至少我知道 JavaScript C# Go都有閉包。
3. 追本溯源
閉包是詞法閉包的簡稱,維基百科上是這樣定義的:
“在計算機(jī)編程中,閉包是在詞法環(huán)境中綁定自由變量的頭等函數(shù)”。
頭等函數(shù)
頭等函數(shù)( First Class)意味著語言將其視為第一類數(shù)據(jù)類型的函數(shù), 意味著你可以將函數(shù)分配給一個變量(或作為參數(shù)傳遞),然后像正常函數(shù)一樣調(diào)用。
很明顯,C#常使用的委托(C#委托的演進(jìn):匿名函數(shù)-->lambda表達(dá)式)是頭等函數(shù)。
Func<string,string> myFunc = delegate(string var1){return "some value";};Func<string,string> myFunc = var1 => "some value";string myVar = myFunc("something");
自由變量
自由變量是在匿名函數(shù)/lambda表達(dá)式中被引用的變量,它不是函數(shù)的參數(shù)也不是函數(shù)的局部變量。
var myVar = "this is good";Func<string,string> myFunc = delegate(string var1){return var1 + myVar;};
詞法作用域引用的自由變量,注意,是引用自由變量,并不是使用當(dāng)時自由變量的值。
??通俗點, 就是告知這個變量環(huán)境,我這個匿名函數(shù)等會執(zhí)行時要用到這個變量;如果我沒被銷毀,你不能銷毀我引用的自由變量。
我們再回過頭來看[投石問路]的面試題。
4. 返璞歸真
首先你要知道:循環(huán)內(nèi)開啟的Task任務(wù),并不保證執(zhí)行順序。
Demo1:輸出5,5,5,5,5
這是因為在 for循環(huán)內(nèi),開啟了5個Task任務(wù),每個任務(wù)均引用了自由變量i (相對于每個任務(wù)執(zhí)行環(huán)境,i 屬于全局變量);
for循環(huán)先執(zhí)行完,i=5, 5個任務(wù)輸出時自然得到值5。

為什么加上臨時變量就能輸出"預(yù)期"?
Demo2:輸出亂序的0,1,2,3,4
這是因為 在for循環(huán)內(nèi),每次循環(huán)j均拷貝自當(dāng)時的i,每個任務(wù)均引用了自由變量 j (每個任務(wù)執(zhí)行環(huán)境均維護(hù)了一個變量j);
任務(wù)亂序執(zhí)行時依舊能獲取本任務(wù)綁定的自由變量j。

有這樣的認(rèn)知,理解JavaScript 閉包也就不難了。
# 總結(jié)本文屏蔽語言差異,理清了[閉包]的概念核心: 頭等函數(shù)、自由變量,不僅能幫助我們應(yīng)對多語種有關(guān)閉包的面試題, 也幫助我們了解[閉包]在通用語言中的設(shè)計初衷。


原創(chuàng)不易 點個在看支持下~
