摘要:準確的說,是形成了的閉包。因此對象函數也是對象和控制流能通過閉包實現。這種方式的閉包不再具有引用透明性,即他不再是一個純函數。類閉包結構一些語言的特性能夠模擬出閉包的效果。
寫在開頭
?本來是很討厭談論閉包這個話題的,因為在這一方面我比較傾向于玉伯還有一些朋友的觀點,搞懂作用域才是最重要的,多帶帶談論閉包,真的意義不大。
?今天剛好在wiki上查其他東西的時候看到了,想了想以前也沒從比較科學的角度考慮這一問題,所以寫下這篇文章,也算是記錄一個翻譯+閱讀的過程,原文中涉及很多其他概念,理解起來并不容易,本人知識水平和翻譯水平都非常有限,所以如果翻譯得有錯誤,還望各位海涵,更歡迎各位批評指正~
關于原文原文來自wikipedia中關于閉包的解釋(英文),有興趣的同學可以閱讀原文。同時還有一份本身的譯文,但是個人感覺這個省略了一些東西,有興趣也可以看看。
譯文內容 閱讀前的提示?因為其中嵌套夾雜著許多其他概念,而這些概念可能又引出其他概念,所有這里提前約定下,下方譯文中的無序列表(即●開頭的內容)為對譯文中一些名詞所作的解釋。并且在譯文中對一些概念也會直接用超鏈接的方式鏈接到我認為解釋得比較好的頁面。
正文開始在程序語言中,閉包(也叫詞法閉包或者函數閉包)是那些實現了詞法作用域和命名綁定的把函數作為一等公民(原文這里叫做first-class functions)的語言中的一種技巧(或者說特性)。
詞法作用域:詞法作用域也叫靜態作用域,是指作用域在詞法解析階段就已經確定了,不會改變。這也是大多數語言采取的方式,JS也是如此,函數在他創建的地方運行,而不是調用的地方。
動態作用域:是指作用域在運行時才能確定。參看下面的例子,引自楊志的回答
var foo=1; function static(){ alert(foo); } !function(){ var foo=2; static(); }(); 在js中,會彈出1而非2,因為static的scope在創建時,記錄的foo是1。 如果js是動態作用域,那么他應該彈出2
在評論中賀師俊還提到,eval 和 with可以產生動態作用域的效果:
比如 with(o) { console.log(x) } 這個x實際有可能是 o.x 。所以這就不是靜態(詞法)作用域了。
var x = 0; void function(code){ eval(code); console.log(x) }("var x=1") 不過注意eval在strict模式下被限制了,不再能形成動態作用域了。
命名綁定:在程序語言中,命名綁定是一種關聯,他將實體(數據或者說是代碼)與標識符聯系或者說對應起來。一個標識符與一個對象綁定是指他持有這個對象的引用。機器語言沒有這個概念,但是程序語言實現了這一點。綁定與作用域密切相關,作用域決定了某個名稱或者說是標識符到底與哪個對象相關聯。
first-class functions:在計算機科學中,如果一門語言把函數當作一等公民來對待,我們稱他為first-class functions。具體來說,就是函數可以作為參數傳遞和返回,可以將它們作為變量聲明,可以將它們存儲在數據結構中(比如我們常用的obj.xxx = somefunc)。同時,有些語言還支持匿名函數(看到這里你應該知道JS是屬于這類的)。在把函數當作一等公民的語言中,函數名沒有任何特別的地方,它們就像普通變量名一樣。(這里感覺原文的重點是告訴大家不要把函數想得太過復雜,所以大家也就不要舉諸如函數名有length,name這些屬性來反駁了)。
實際中來說,閉包是一種記錄,他將函數與他的上下文環境存儲起來:它是一種將函數內使用到的自由變量與他的值或者閉包被創建時那些指向其他地方的引用關聯起來的映射。注意這里的使用二字很重要(原文為variables that are used locally),我們可以看下方的代碼:
function fn1() { var a = 1; function fn2() { debugger; console.log(a); } fn2(); } fn1();
這里我們在fn2中使用a,可以看到右圖中fn1形成了閉包。準確的說,是fn1形成了fn2的閉包。
如果我們不使用a,那么就形成不了閉包。
這里糾正一個誤區,很多人認為必須要返回函數才能形成閉包,比如上面必須要在fn1中返回fn2,然后fn1()()這樣調用才會形成閉包,其實通過上面的截圖我們可以發現并不是這樣。
同時要注意,識別閉包在詞法分析階段就已經確定了,意思是說即使我們可以肯定用不到a,fn1也會識別為fn2的閉包,因為我們"使用"了a。如下所示:
自由變量:在計算機程序中,自由變量是指在一個函數中即不是局部變量也不是函數參數的變量。他與非局部變量是同義詞。
非局部變量:是指未定義在本作用域或者說當前作用域里的變量。也就是我們常說的外部變量。舉例來說,在函數func中使用變量x,卻沒有在func中聲明(即在其他作用域中聲明的),對于func的作用域來說,x就是非局部變量。
var x = 1; function func() { console.log(x); }應用
閉包被用作實現連續式風格,并且在這種風格中隱藏狀態。因此對象(函數也是對象)和控制流能通過閉包實現。在一些語言中,閉包發生于在一個函數內定義另一個函數,在內部函數中我們引用了外部函數里的局部變量(就是上圖中的例子)。在運行時,當外部函數運行的時候,一個閉包就形成了,他由內部函數的代碼以及任何內部函數中指向外部函數局部變量的引用組成。
連續式風格(continuation-passing style,簡稱CPS):在函數式編程中,CPS是一種編程風格,在這種風格中,控制流以一種續延的形式傳遞。這個概念真的很難解釋,包括我自己也不是太明白,所以建議搜索或者參考怎樣理解 Continuation-passing style以及尾遞歸與Continuation以及續延(continuation)以及函數式編程中cps是什么意思以及Continuation-passing style原文
個人建議結合原文然后再去看尾遞歸那一篇理解效果會比較好。這個概念真的很有趣,對FP來說意義重大,有興趣的朋友可以詳細去了解
連續式風格與直接式風格相對。
直接式風格(direct style):也是語言中常用的風格。他是順序程序設計中常用的,在這之中,控制流通過運行下一行被子程序調用實現顯示的傳遞,或者通過像return, yield, await這樣的結構實現。
CPS與direct style對比的Example,摘自wiki For example in Dart(例子以這種語言書寫), 一個循環動畫可能以下面形式書寫 Continuation-passing style(CPS風格) var running = true; // Set false to stop. tick(time) { context.clearRect(0, 0, 500, 500); context.fillRect(time % 450, 20, 50, 50); if (running) window.animationFrame.then(tick); } window.animationFrame.then(tick); 在CPS中,在下一幀中異步調用window.animationFrame,然后調用回調(tick)函數。 這個回調函數需要在尾部再次調用,也就是要形成尾遞歸
Direct style(直接式風格) var running = true; // Set false to stop. while (running) { var time = await window.animationFrame; context.clearRect(0, 0, 500, 500); context.fillRect(time % 450, 20, 50, 50); } 在直接式風格中,異步調用window.animationFrame簡單的yield控制流,然后繼續執行。 一個while循環可以代替遞歸調用
在主流語言中,CPS常常發生在將閉包作為函數參數傳遞的時候,因此直接式風格更簡單的意味著函數返回了一個值,而不是攜帶了一個函數參數。
控制流:在計算機科學中,控制流(也稱作流控制)是一種順序,在這種順序中,個體的語句,指令,函數調用以命令式執行或者解析,它強調控制流,這是與聲明式有差異的地方。可以回想一下我們常畫的程序執行流程圖,就是控制流的一種體現。關于命令式與聲明式,可以參考命令式與聲明式的區別-1,命令式與聲明式的區別-2
中斷和信號是低等級的改變控制流的機制(應該就是指break,continue,throw這一類),但是通常發生時被當作一種對外部刺激或者事件(也可能異步發生)的響應,而不是一個內聯控制流語句的執行。在機器語言或者匯編語言層面,控制流常常通過程序計數器PC來改變。對一些CPU而言,唯一可用的控制流指令就是條件指令(類似于if)和非條件分支指令(原文為also called jumps,就是我們常說的goto)。
閉包能被用作與函數的私有變量相關聯,讓外部調用呈現連續性(比如一個高階函數實現累加)。私有變量只能被內部函數訪問到,其他任何地方都訪問不到這個變量。
因此,在有狀態語言中,閉包能被用來實現狀態表示和信息隱藏(可以理解為私有變量),因此,閉包內的局部變量的生命周期是不確定的,所以創建的一個變量在函數被下一次調用時仍然可用。這種方式的閉包不再具有引用透明性,即他不再是一個純函數。
退出閉包在其他詞法作用域結構中,閉包是存在很多區別的。比如return,break 和 continue語句。一般來說,這些結構被認為脫離延續(原文為escape continuation,異常處理就屬于這類),即脫離一個封閉的語句(如break和continue,從函數遞歸調用的角度講,這些指令被認為需要循環結構才能工作)。
在一些語言中,例如ECMAScript,return指向了詞法閉包語句建立的最內層的continuation,在閉包中return將控制流轉移到調用它的代碼。
然而,在Smalltalk語言中,對于方法調用,有一個表面上很相似的操作符^,它調用建立的escape continuation,忽略任何中間的嵌套閉包。一個特定閉包中escape continuation只能在達到閉包代碼結束的時候被顯式調用。下面的例子展示了這之間的區別:
"Smalltalk" foo | xs | xs := #(1 2 3 4). xs do: [:x | ^x]. ^0 bar Transcript show: (self foo printString) "prints 1"
// ECMAScript function foo() { var xs = [1, 2, 3, 4]; xs.forEach(function (x) { return x; }); return 0; } alert(foo()); // prints 0
上面的代碼片段描述了在Smalltalk中的^操作符與JS中的return操作符的行為并不是相同的。在上面的JS中,return x將會離開內層閉包并開始forEach循環的下一次迭代,而在Smalltalk中,^x將會終止循環并從foo方法返回。
類閉包結構一些語言的特性能夠模擬出閉包的效果。包括那些面向對象的語言,如JAVA,C++,OC,C#,D,有這方面興趣的朋友可以看看原文,這里我們選出原文中提到的關于JAVA的部分作為介紹:
Java中可以將類定義在方法內部。我們把這叫做局部類(包括方法內部類和匿名內部類)。當這些類沒有名稱時,我們把它們叫做匿名類或者匿名內部類。一個局部類中可以引用閉包類中的變量,或者閉包方法中的final變量。
class CalculationWindow extends JFrame { private volatile int result; ... public void calculateInSeparateThread(final URI uri) { // "new Runnable() { ... }"是一個實現了Runnable接口的匿名內部類 new Thread( new Runnable() { void run() { // 他可以訪問局部的final變量 calculate(uri); // 他可以訪問閉包類的成員變量 result = result + 10; } } ).start(); } }
隨著JAVA8支持lambda表達式,上面的代碼可以改寫成如下形式:
class CalculationWindow extends JFrame { private volatile int result; ... public void calculateInSeparateThread(final URI uri) { // 下面的形如 code () -> { /* code */ } 就是一個閉包 new Thread(() -> { calculate(uri); result = result + 10; }).start(); } }
局部類是內部類的一種,他們被聲明在方法體中。Java也支持在閉包類中聲明非靜態內部類(就是我們常說的成員內部類)。他們都叫做內部類。他們在閉包類中定義,也完全能夠訪問閉包類的實例。由于他們與實例相綁定,一個內部類也許要使用特殊的語法才能被實例化(即必須先實例化外部類,再通過外部類實例化內部類,當然靜態內部類不需要這樣,這里指的是成員內部類)。
public class EnclosingClass { /* 定義成員內部類 */ public class InnerClass { public int incrementAndReturnCounter() { return counter++; } } private int counter; { counter = 0; } public int getCounter() { return counter; } public static void main(String[] args) { EnclosingClass enclosingClassInstance = new EnclosingClass(); /* 通過外部類的實例來實例化內部類 */ EnclosingClass.InnerClass innerClassInstance = enclosingClassInstance.new InnerClass(); for(int i = enclosingClassInstance.getCounter(); (i = innerClassInstance.incrementAndReturnCounter()) < 10;) { System.out.println(i); // 在運行之后,會打印0到9。 } } }
從Java8起,Java也將函數作為一等公民。Lambda表達式是一種具體體現,它被當作Function
public static void main(String[] args) { Function寫在結尾length = s -> s.length(); // 原文這里的length沒有括號,明顯是錯誤的 System.out.println( length.apply("Hello, world!") ); // Will print 13. }
在準備翻譯之前,沒有想到其中有很多其他概念,通過這些概念,也學習到了很多其他方面的知識,原始概念大多來自于自然科學(以數學為主,這里推薦一篇從20世紀數學危機到圖靈機到命令式與FP),且大部分都在在上世紀6,70年代就已經提出。
無論發揚這些理論的先行者,還是正在學習也許會發揚下一個理論的我們,還是最初那些提出這些概念的前輩,我們都確實是在站在巨人的肩膀上。
由于水平和精力有限,也只是對其中一些概念做了簡單的介紹,也希望拋磚引玉,歡迎各位補充~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/80789.html
摘要:原文鏈接恰當地學習適合第一次編程和非的程序員持續時間到周前提無需編程經驗繼續下面的課程。如果你沒有足夠的時間在周內完成全部的章節,學習時間盡力不要超過周。你還不是一個絕地武士,必須持續使用你最新學到的知識和技能,盡可能地經常持續學習和提高。 原文鏈接:How to Learn JavaScript Properly 恰當地學習 JavaScript (適合第一次編程和非 JavaSc...
摘要:本文作為第三篇,將會討論另一個開發者容易忽視的重要主題內存管理。我們也會提供一些關于如何處理內存泄露的技巧。這是當前整型和雙精度的大小。然而,這是一組可以收集的內存空間的近似值。 本文轉載自:眾成翻譯譯者:Leslie Wang審校: 為之漫筆鏈接:http://www.zcfy.cc/article/4211原文:https://blog.sessionstack.com/how-j...
摘要:盡管特定環境下有各種各樣的設計模式,開發者還是傾向于使用一些習慣性的模式。原型設計模式依賴于原型繼承原型模式主要用于為高性能環境創建對象。對于一個新創建的對象,它將保持構造器初始化的狀態。這樣做主要是為了避免訂閱者和發布者之間的依賴。 2016-10-07 每個JS開發者都力求寫出可維護、復用性和可讀性高的代碼。隨著應用不斷擴大,代碼組織的合理性也越來越重要。設計模式為特定環境下的常見...
摘要:閉包引起的內存泄漏總結從理論的角度將由于作用域鏈的特性中所有函數都是閉包但是從應用的角度來說只有當函數以返回值返回或者當函數以參數形式使用或者當函數中自由變量在函數外被引用時才能成為明確意義上的閉包。 文章同步到github js的閉包概念幾乎是任何面試官都會問的問題,最近把閉包這塊的概念梳理了一下,記錄成以下文章。 什么是閉包 我先列出一些官方及經典書籍等書中給出的概念,這些概念雖然...
摘要:函數式編程前端掘金引言面向對象編程一直以來都是中的主導范式。函數式編程是一種強調減少對程序外部狀態產生改變的方式。 JavaScript 函數式編程 - 前端 - 掘金引言 面向對象編程一直以來都是JavaScript中的主導范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數式編程越來越多得受到開發者的青睞。函數式編程是一種強調減少對程序外部狀態產生改變的方式。因此,...
閱讀 3408·2021-09-22 16:00
閱讀 3452·2021-09-07 10:26
閱讀 2989·2019-08-30 15:55
閱讀 2858·2019-08-30 13:48
閱讀 1366·2019-08-30 12:58
閱讀 2162·2019-08-30 11:15
閱讀 945·2019-08-30 11:08
閱讀 525·2019-08-29 18:41