徹底理解閉包實現(xiàn)原理

前言
閉包對于一個長期寫 Java 的開發(fā)者來說估計鮮有耳聞,我在寫 Python 和 Go 之前也是沒怎么了解,光這名字感覺就有點"神秘莫測",這篇文章的主要目的就是從編譯器的角度來分析閉包,徹底搞懂閉包的實現(xiàn)原理。
函數(shù)一等公民
一門語言在實現(xiàn)閉包之前首先要具有的特性就是:First class function 函數(shù)是第一公民。
簡單來說就是函數(shù)可以像一個普通的值一樣在函數(shù)中傳遞,也能對變量賦值。
先來看看在 Go 里是如何編寫的:
這里體現(xiàn)了閉包的兩個重要特性,第一個自然就是函數(shù)可以作為值返回,同時也能賦值給變量。
第二個就是在閉包函數(shù) f1() 對閉包變量 varInner 的訪問,每個閉包函數(shù)的引用都會在自己的函數(shù)內(nèi)部保存一份閉包變量 varInner,這樣在調(diào)用過程中就不會互相影響。
從打印的結(jié)果中也能看出這個特性。
作用域
閉包之所以不太好理解的主要原因是它不太符合自覺。

本質(zhì)上就是作用域的關(guān)系,當(dāng)我們調(diào)用 f1()? 函數(shù)的時候,會在棧中分配變量 varInner?,正常情況下調(diào)用完畢后 f1? 的棧會彈出,里面的變量 varInner 自然也會銷毀才對。
但在后續(xù)的 f2()? 和 f3()? 調(diào)用的時,卻依然能訪問到 varInner,就這點不符合我們對函數(shù)調(diào)用的直覺。
但其實換個角度來看,對 innerFun? 來說,他能訪問到 varExternal? 和 varInner? 變量,最外層的 varExternal 就不用說了,一定是可以訪問的。
但對于 varInner 來說就不一定了,這里得分為兩種情況;重點得看該語言是靜態(tài)/動態(tài)作用域。
就靜態(tài)作用域來說,每個符號在編譯器就確定好了樹狀關(guān)系,運(yùn)行時不會發(fā)生變化;也就是說 varInner? 對于 innerFun 這個函數(shù)來說在編譯期已經(jīng)確定可以訪問了,在運(yùn)行時自然也是可以訪問的。
但對于動態(tài)作用域來說,完全是在運(yùn)行時才確定訪問的變量是哪一個。
恰好 Go? 就是一個靜態(tài)作用域的語言,所以返回的 innerFun? 函數(shù)可以一直訪問到 varInner 變量。
實現(xiàn)閉包
但 Go 是如何做到在 f1() 函數(shù)退出之后依然能訪問到 f1() 中的變量呢?
這里我們不妨大膽假設(shè)一下:
首先在編譯期掃描出哪些是閉包變量,也就是這里的 varInner,需要將他保存到函數(shù) innerFun() 中。
運(yùn)行時需要判斷出 f2 是一個函數(shù),而不是一個變量,同時得知道它所包含的函數(shù)體是 innerFun() 所定義的。
接著便是執(zhí)行函數(shù)體的 statement 即可。
而當(dāng) f3 := f1() 重新賦值給 f3 時,在 f2 中累加的 varInner 變量將不會影響到 f3,這就得需要在給 f3 賦值的重新賦值一份閉包變量到 f3 中,這樣便能達(dá)到互不影響的效果。
閉包掃描
GScript 本身也是支持閉包的,所以把 Go 的代碼翻譯過來便長這樣:
可以看到運(yùn)行結(jié)果和 Go 的一樣,所以我們來看看 GScript 是如何實現(xiàn)的便也能理解 Go 的原理了。
先來看看第一步掃描閉包變量:

allVariable := c.allVariable(function)查詢所有的變量,包括父 scope 的變量。
scopeVariable := c.currentScopeVariable(function)查詢當(dāng)前 scope 包含下級所有 scope 中的變量,這樣一減之后就能知道閉包變量了,然后將所有的閉包變量存放進(jìn)閉包函數(shù)中。
閉包賦值
之后在 return innerFun 處,將閉包變量的數(shù)據(jù)賦值到變量中。

閉包函數(shù)調(diào)用
在這里每一次賦值時,都會把 f1() 返回函數(shù)復(fù)制到變量 f2/f3 中,這樣兩者所包含的閉包變量就不會互相影響。

在調(diào)用函數(shù)變量時,判斷到該變量是一個函數(shù),則直接返回函數(shù)。
之后直接調(diào)用該函數(shù)即可。
函數(shù)式編程
接下來便可以利用 First class function 來試試函數(shù)式編程:
這個有點類似于 Java 中流的 map 函數(shù),將函數(shù)作為值傳遞進(jìn)去,后續(xù)支持匿名函數(shù)后會更像是函數(shù)式編程,現(xiàn)在必須得先定義一個函數(shù)變量再進(jìn)行傳遞。
除此之外在 GScript 中的 http 標(biāo)準(zhǔn)庫也利用了函數(shù)是一等公民的特性:
在綁定路由時,handle 便是一個函數(shù),使用的時候直接傳遞業(yè)務(wù)邏輯的 handle 即可:
總結(jié)
總的來說閉包具有以下特性:
- 函數(shù)需要作為一等公民。
 - 編譯期掃描出所有的閉包變量。
 - 在返回閉包函數(shù)時,為閉包變量賦值。
 - 每次創(chuàng)建新的函數(shù)變量時,需要將閉包數(shù)據(jù)復(fù)制進(jìn)去,這樣閉包變量才不會互相影響。
 - 調(diào)用函數(shù)變量時,需要判斷為函數(shù),而不是變量。
 

可以在 Playground 中體驗閉包函數(shù)打印裴波那切數(shù)列的運(yùn)用。
本文相關(guān)資源鏈接
- GScript 源碼:https://github.com/crossoverJie/gscript。
 - Playground 源碼:https://github.com/crossoverJie/gscript-homepage。
 















 
 
 






 
 
 
 