摘要:閉包在計算機科學(xué)中,閉包是詞法閉包的簡稱,是引用了自由變量的函數(shù)。所以,有另一種說法認為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體。通過閉包完成了私有的成員和方法的封裝。
閉包
在計算機科學(xué)中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數(shù)。這個被引用的自由變量將和這個函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。所以,有另一種說法認為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體。 --- 維基百科
其實這段引用已經(jīng)說明了閉包的本質(zhì):引用了自由變量的函數(shù),自由變量將和這個函數(shù)一同存在——這是理解閉包的關(guān)鍵。
一 原理解釋函數(shù)式編程語言的基礎(chǔ)是lambda演算,而閉包又是從函數(shù)式編程衍生而來。下面先從Lambda演算理解下函數(shù)式思維。(不感興趣可直接跳過)
Lambda演算非形式化的描述Lambda演算是一套用于研究函數(shù)定義、應(yīng)用和遞歸的形式系統(tǒng)。它包括一條變換規(guī)則(變量替換)和一條函數(shù)定義方式,Lambda演算之通用在于,任何一個可計算函數(shù)都能用這種形式來表達和求值。因而,它是等價于圖靈機的。盡管如此,Lambda演算強調(diào)的是變換規(guī)則的運用,而非實現(xiàn)它們的具體機器??梢哉J為這是一種更接近軟件而非硬件的方式。Lambda演算對函數(shù)式編程語言有巨大的影響,比如Lisp和Haskell。
在lambda演算中,每個表達式都代表一個函數(shù),這個函數(shù)有一個參數(shù),并且返回一個值。不論是參數(shù)和返回值,也都是一個單參的函數(shù)。可以這么說,lambda演算中,只有一種“類型”,那就是這種單參函數(shù)。
注:在函數(shù)式編程語言中,函數(shù)可是一等公民。
函數(shù)是通過λ表達式匿名地定義的,這個表達式說明了此函數(shù)將對其參數(shù)進行什么操作。例如,“加2”函數(shù)f(x)= x + 2可以用lambda演算表示為λx.x + 2 (或者λy.y + 2,參數(shù)的取名無關(guān)緊要)
λ演算中函數(shù)只有一個參數(shù),那有兩個參數(shù)的函數(shù)怎么表達呢?可以通過lambda演算這么表達:一個單一參數(shù)的函數(shù)的返回值又是一個單一參數(shù)的函數(shù)(閉包嗎?)。
例如,函數(shù)f(x, y) = x + y可以寫作:
λx.λy.x + y ----->λx. (λ y. + x y)
上面這個轉(zhuǎn)化就叫currying,它展示了,我們?nèi)绾螌崿F(xiàn)加法(假設(shè)+這個符號已經(jīng)具有相加的功能)。
其實就是我們現(xiàn)在意義上的閉包——你調(diào)用一個函數(shù),這個函數(shù)返回另一個函數(shù),返回的函數(shù)中存儲保留了調(diào)用函數(shù)的變量。currying是閉包的鼻祖。(如果理解困難,下面會用編程語言實現(xiàn)上面的演算)
閉包解釋閉包被廣泛使用于函數(shù)式編程語言,慢慢很多命令式語言也開始支持閉包。在函數(shù)中可以(嵌套)定義另一個函數(shù)時,如果內(nèi)部的函數(shù)引用了外部的函數(shù)的變量,則可能產(chǎn)生閉包。運行時,一旦外部的函數(shù)被執(zhí)行,一個閉包就形成了,閉包中包含了內(nèi)部函數(shù)的代碼,以及所需外部函數(shù)中的變量的引用。
典型的支持閉包的語言中,通常將函數(shù)當作第一類對象——在這些語言中,函數(shù)通常有下列特性:
- 可以將函數(shù)賦值給一個變量
- 函數(shù)可以作為參數(shù)傳遞
- 函數(shù)的返回值可以是一個函數(shù)
例如以下Scheme(Lisp的一個方言)代碼:
(define (f x) (lambda (y) (+ x y)))
在這個例子中,lambda表達式(lambda (y) (+ x y))出現(xiàn)在函數(shù)f中。當這個lambda表達式被執(zhí)行時,Scheme創(chuàng)造了一個包含此表達式以及對x變量的引用的閉包,其中x變量在lambda表達式中是自由變量。
下面是用ECMAScript (JavaScript)寫的同一個例子:
function f(x){ return function(y) { return x + y; }; }
其中f返回的匿名函數(shù)與其自由變量x組成了一個閉包。
上述代碼在nodeJS環(huán)境中執(zhí)行:
console.log( (f(7)) (2) ); //9
首先用第一個參數(shù)(7)代替最外層函數(shù)的參數(shù)(x),然后用第二個參數(shù)(2)代替第二層函數(shù)的參數(shù)(y),最終得到計算結(jié)果。
注意:這個運算執(zhí)行了兩個函數(shù):f和匿名函數(shù)。f的作用域為(f 7),這就是說,當(f 7)執(zhí)行后,f這個函數(shù)就結(jié)束了,而x是f的私有變量,理論上x應(yīng)該被釋放了,然后x在f函數(shù)執(zhí)行結(jié)束后并沒有被釋放,而是繼續(xù)被匿名函數(shù)繼續(xù)使用。支持這種機制的語言稱為支持閉包機制(在函數(shù)中可以(嵌套)定義另一個函數(shù)時,如果內(nèi)部的函數(shù)引用了外部函數(shù)的變量,即使已經(jīng)離開了外部函數(shù)的環(huán)境,自由變量(外部函數(shù)的變量)也和內(nèi)部函數(shù)一同存在,則產(chǎn)生閉包)。
二 閉包的實現(xiàn)通過上面的原理解釋我們提出了這樣一個問題(雖然這樣的問題不干擾你理解閉包):
如果一個函數(shù)定義在棧中,那么當函數(shù)返回時,定義在函數(shù)中的局部變量就不復(fù)存在了, 那為什么內(nèi)部的函數(shù)可以訪問外部函數(shù)的變量?即使外部函數(shù)執(zhí)行完,外部函數(shù)的變量也能和內(nèi)部函數(shù)一同存在?
下面以JavaScript閉包實現(xiàn)舉例。
javascript
上面是一個使用閉包的簡單示例,代碼執(zhí)行完畢后,函數(shù)對象并不會被垃圾回收機制回收1,函數(shù)內(nèi)的臨時變量能夠得以長期存在,而這個變量只能夠被閉包函數(shù)修改,在外部是無法訪問和修改的。(這個其實前面已經(jīng)說過,這里有點啰嗦)
JavaScript中將作用域鏈描述為一個對象列表,不是綁定的棧。每次調(diào)用JavaScript函數(shù)的時候(函數(shù)也是對象),都會為之創(chuàng)建一個新的對象用來保存局部變量,把這個對象添加至作用域鏈中。
每個函數(shù)關(guān)聯(lián)都有一個執(zhí)行上下文場景(Execution Context) ,然后執(zhí)行環(huán)境會創(chuàng)建一個活動對象(call object),該對象包含了兩個重要組件,環(huán)境記錄,和外部引用(指針)。環(huán)境記錄包含了函數(shù)內(nèi)部聲明的局部變量和參數(shù)變量,外部引用指向了外部函數(shù)對象的上下文執(zhí)行場景。這樣的數(shù)據(jù)結(jié)構(gòu)就構(gòu)成了一個單向的鏈表,每個引用都指向外層的上下文場景。最后形成如下圖的結(jié)構(gòu):
注:此圖盜用,此圖網(wǎng)站已不能打開。
如上圖和代碼所示:a返回函數(shù)b的引用給c,函數(shù)b的作用域鏈包含了對函數(shù)a的活動對象的引用,也就是說b可以訪問到a中定義的所有變量和函數(shù)。函數(shù)b被c引用,函數(shù)b又依賴函數(shù)a,因此函數(shù)a在返回后不會被GC回收。
三 閉包的作用以上述代碼為例:
- 保護函數(shù)內(nèi)的變量安全:函數(shù)a中i只有函數(shù)b才能訪問,而無法通過其他途徑訪問到,因此保護了i的安全;
- 在內(nèi)存中維持一個變量:函數(shù)a中i的一直存在于內(nèi)存中,因此每次執(zhí)行c(),都會給i自加1;
- 通過保護變量的安全實現(xiàn)JS私有屬性和私有方法(不能被外部訪問)。
Singleton 單件:(盜用理解Javascript的閉包的例子)
var singleton = function () { var privateVariable; function privateFunction(x) { ...privateVariable... } return { firstMethod: function (a, b) { ...privateVariable... }, secondMethod: function (c) { ...privateFunction()... } }; }();
這個單件通過閉包來實現(xiàn)。通過閉包完成了私有的成員和方法的封裝。匿名主函數(shù)返回一個對象。對象包含了兩個方法,方法1可以方法私有變量,方法2訪問內(nèi)部私有函數(shù)。需要注意的地方是匿名主函數(shù)結(jié)束的地方的"()’,如果沒有這個"()’就不能產(chǎn)生單件。因為匿名函數(shù)只能返回了唯一的對象,而且不能被其他地方調(diào)用。這個就是利用閉包產(chǎn)生單件的方法。
四 概念混淆之匿名函數(shù)匿名函數(shù)指的是沒有函數(shù)名稱的函數(shù),它和閉包沒有關(guān)系,只是閉包中函數(shù)可以通過匿名函數(shù)編寫,當然匿名函數(shù)不止會出現(xiàn)在閉包中。
如下面一個javascript代碼示例:
var f = function(name){ //函數(shù)體 };
函數(shù)表達式是創(chuàng)建了一個匿名函數(shù),然后將匿名函數(shù)賦值給一個變量f。
參考理解Javascript的閉包
閉包漫談(從抽象代數(shù)及函數(shù)式編程角度)
維基百科-閉包
維基百科-λ演算
編程語言的基石——Lambda calculus
注:Javascript的垃圾回收機制:
在Javascript中,如果一個對象不再被引用,那么這個對象就會被GC回收。如果兩個對象互相引用,而不再被第3者所引用,那么這兩個互相引用的對象也會被回收。因為函數(shù)a被b引用,b又被a外的c引用,這就是為什么函數(shù)a執(zhí)行后不會被回收的原因。??
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/85785.html
摘要:大名鼎鼎的閉包面試必問。閉包的作用是什么??吹介]包在哪了嗎閉包到底是什么五年前,我也被這個問題困擾,于是去搜了并總結(jié)下來。關(guān)于閉包的謠言閉包會造成內(nèi)存泄露錯。閉包里面的變量明明就是我們需要的變量,憑什么說是內(nèi)存泄露這個謠言是如何來的因為。 本文為饑人谷講師方方原創(chuàng)文章,首發(fā)于 前端學(xué)習(xí)指南。 大名鼎鼎的閉包!面試必問。請用自己的話簡述 什么是「閉包」。 「閉包」的作用是什么。 首先...
摘要:完美的閉包,對,閉包就這么簡單。這僅僅是閉包的一部分,閉包利用函數(shù)作用域達到了訪問外層變量的目的。此時一個完整的閉包實現(xiàn)了,的垃圾回收機制由于閉包的存在無法銷毀變量。 1.閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。 上面這段話來自 javascript 高級程序設(shè)計 第三版 P178 。作者說閉包是一個函數(shù),它有訪問另一個函數(shù)作用域中的變量的能力。 2.函數(shù)訪問它被創(chuàng)建時所處的...
摘要:到底什么是閉包這個問題在面試是時候經(jīng)常都會被問,很多小白一聽就懵逼了,不知道如何回答好。上面這么說閉包是一種特殊的對象。閉包的注意事項通常,函數(shù)的作用域及其所有變量都會在函數(shù)執(zhí)行結(jié)束后被銷毀。從而使用閉包模塊化代碼,減少全局變量的污染。 閉包,有人說它是一種設(shè)計理念,有人說所有的函數(shù)都是閉包。到底什么是閉包?這個問題在面試是時候經(jīng)常都會被問,很多小白一聽就懵逼了,不知道如何回答好。這個...
閱讀 1699·2021-11-24 09:39
閱讀 3160·2021-11-22 15:24
閱讀 3104·2021-10-26 09:51
閱讀 3293·2021-10-19 11:46
閱讀 2901·2019-08-30 15:44
閱讀 2228·2019-08-29 15:30
閱讀 2548·2019-08-29 15:05
閱讀 788·2019-08-29 10:55