【譯】看權威的wikipedia如何解釋閉包

寫在開頭

 原本是很討厭談論閉包這個話題的,由於在這一方面我比較傾向於玉伯還有一些朋友的觀點,搞懂做用域纔是最重要的,單獨談論閉包,真的意義不大。html

 今天恰好在wiki上查其餘東西的時候看到了,想了想之前也沒從比較科學的角度考慮這一問題,因此寫下這篇文章,也算是記錄一個翻譯+閱讀的過程,原文中涉及不少其餘概念,理解起來並不容易,本人知識水平和翻譯水平都很是有限,因此若是翻譯得有錯誤,還望各位海涵,更歡迎各位批評指正~編程

關於原文

原文來自wikipedia中關於閉包的解釋(英文),有興趣的同窗能夠閱讀原文。同時還有一份自己的譯文,可是我的感受這個省略了一些東西,有興趣也能夠看看。數據結構

譯文內容

閱讀前的提示

 由於其中嵌套夾雜着許多其餘概念,而這些概念可能又引出其餘概念,全部這裏提早約定下,下方譯文中的無序列表(即●開頭的內容)爲對譯文中一些名詞所做的解釋。而且在譯文中對一些概念也會直接用超連接的方式連接到我認爲解釋得比較好的頁面。閉包

正文開始

閉包

在程序語言中,閉包(也叫詞法閉包或者函數閉包)是那些實現了詞法做用域命名綁定的把函數做爲一等公民(原文這裏叫作first-class functions)的語言中的一種技巧(或者說特性)。app

  • 詞法做用域:詞法做用域也叫靜態做用域,是指做用域在詞法解析階段就已經肯定了,不會改變。這也是大多數語言採起的方式,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);
    }

應用

閉包被用做實現連續式風格,而且在這種風格中隱藏狀態。所以對象(函數也是對象)和控制流能經過閉包實現。在一些語言中,閉包發生於在一個函數內定義另外一個函數,在內部函數中咱們引用了外部函數裏的局部變量(就是上圖中的例子)。在運行時,當外部函數運行的時候,一個閉包就造成了,他由內部函數的代碼以及任何內部函數中指向外部函數局部變量的引用組成。

連續式風格與直接式風格相對。

  • 直接式風格(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中的局部類與lambda函數

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<T, U>類型,其中T是輸入,U是輸出。表達式能被他的apply方法調用,而不是標準的call方法。

public static void main(String[] args) {
  Function<String,Integer> length = s -> s.length(); // 原文這裏的length沒有括號,明顯是錯誤的
  
  System.out.println( length.apply("Hello, world!") ); // Will print 13.
}

寫在結尾

在準備翻譯以前,沒有想到其中有不少其餘概念,經過這些概念,也學習到了不少其餘方面的知識,原始概念大多來自於天然科學(以數學爲主,這裏推薦一篇從20世紀數學危機到圖靈機到命令式與FP),且大部分都在在上世紀6,70年代就已經提出。

不管發揚這些理論的先行者,仍是正在學習也許會發揚下一個理論的咱們,仍是最初那些提出這些概念的前輩,咱們都確實是在站在巨人的肩膀上。

因爲水平和精力有限,也只是對其中一些概念作了簡單的介紹,也但願拋磚引玉,歡迎各位補充~

相關文章
相關標籤/搜索