為什么有閉包?


之前一直認為寫博客是個可有可無的事情,前天一個電話面試問得我手足無措,發現很多以前知道的東西現在只能說出個大概,很久沒復習的緣故吧。而轉身去看的時候,又不知從何看起,頓時覺得有寫博客的必要。與日記不同,說不定路過的哪位大神會指出我的錯誤呢,有趣的討論還可以加深理解。

什么是閉包?

這個定義一倆句話說出來還真不容易,而且晦澀。 從字面詞來講的話就是一個包裹起來的封閉的東西。百度百科的解釋是:

閉包是指可以包含自由(未綁定到特定對象)變量的代碼塊;這些變量不是在這個代碼塊內或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義(局部變量)。“閉包” 一詞來源於以下兩者的結合:要執行的代碼塊(由於自由變量被包含在代碼塊中,這些自由變量以及它們引用的對象沒有被釋放)和為自由變量提供綁定的計算環境(作用域)

是不是有點晦澀啊? 哈哈。下面以javascript語言來演示一下閉包:

    var obj=(function(){
         var num=0;
         return {
            "getNum":function(){
                  return num;
             },
            "setNum":function(v){
                 num=v;
            }
         }
    })();
    console.log(obj.getNum());// 0
    obj.setNum(3);
    console.log(obj.getNum());//3

obj的值是一個立即執行的函數的返回值,在這個函數里面定義了一個num變量,初始值為0 。從javascript的語法規則來講,我們不可能在這個函數外面訪問到num,但是在這個例子里面卻可以通過obj來訪問,如果你用過C,會非常驚訝。這是一個閉包的例子,通過閉包可以將一些東西隱藏起來,只對外暴露想暴露的部分,類似於C++的類,私有成員不能被外部訪問。

先給出一個閉包的定義:

A closure is a pair consisting of the function code and the environment in which the function is created.鏈接

在函數式語言里可以把函數當成值傳來傳去,甚至可以在運行的時候創建函數。函數運行需要相應的代碼和運行環境(用來尋找執行所需要的各種值)。

為什么會有閉包?

a snippet of javascript code

   // environment  E1
   var x=1;
   var createFunc=function (y){       
        //environment E2
       return function(z){
         //environment E3
         return x+y+z;
       }
   }
   var func=createFunc(10);
   console.log(func(100));//111

func的值是一個運行時創建的函數,這個函數運行的時候需要用到三個值(x,y,z),不討論x和z,主要討論y。 大部分程序運行時都有一個運行棧,調用一個函數就在棧上放一個
record .

每一個記錄上包含存取鏈(控制數據訪問),控制鏈(函數調用關系,返回地址),局部變量。當函數執行完返回,相應的記錄就被刪除了。那么按這個邏輯上面那段代碼是不可能工作的。但該圖所示的原理並不適用所有的語言。javascript也是called-stack,但是有區別。上面代碼里標注了E1,E2,E3,代表三個不同的environment。E2的parent environment是E1,E3的parent environment是E2。func執行時尋找所需變量的值是從E3到E2,再到E1。但是對於y這個變量是作為createFunc的參數傳入,按照called-stack的運行方式,createFunc一旦返回,相應的記錄將從stack上面刪除,那么y將不可訪問。不同就在於此,當js發現閉包時會將相應的運行時環境保存到heap(堆)上,這里的運行時環境就是上面英文引用中的environment,js中稱之為scope chain.所以當func被賦值,不僅其對應的函數代碼被保存(可以通過func.toString()查看),相應的環境也被保存,即此時的E3,E2,E1。E2里面保存的是y的值10和指向其外部環境E1的指針。如果再次運行createFunc則會再創建一個獨立的E2。代碼中的func就是一個閉包(包含相應的code和運行環境),理論上講,javascript中的所有的函數都是閉包。

上面的閉包定義也適用於其他語言,不僅是javascript。

現在是在重寫這篇文章,以補充原文中的紕漏。並重新理一理閉包和GC的關系。閉包是解決函數式語言的一個問題的一種技術,這個問題就是如何保證將函數當做值創造並傳來傳去的時候函數仍能正確運行。

閉包與GC

(有些語言具有閉包特性但沒有GC,暫不討論。)閉包解決了函數式語言的一個問題,隨之而來出現了另一個問題---內存。“environment”保存在了堆上(可能創建很多environment),不再使用之后總需要去釋放,這個操作就是由GC負責。GC通過算法(引用計數、跟蹤(標記清掃、標記壓縮、標記拷貝))來決定是否釋放變量占用的相應內存,當他發現一個變量不在被引用則釋放相應的內存空間。GC設計的最初目的是將程序員從煩人的內存管理中解放。

上面說的environment或者是作用域鏈什么的,在底層看來就是用指針串起來的一個個內存塊,自然也就是GC管理的對象。“發現閉包並保存其運行環境”這一動作對GC來講就是將值從stack上復制到heap上,並修改指針;或者有些方式是在編譯階段就檢測到閉包,直接就在heap上申請相應的空間存放會被外部引用的變量(variables就構成了environment)。但這只是內存部分的操作,除此還要將運行環境綁定到函數變量上,這個就不是GC的工作了。之前說因為GC的工作原理必然會導致閉包的出現,但是忽略了綁定環境到函數變量這一操作。下面這段Golang代碼就不是閉包,如果返回的&z是一個函數或是包含函數那么就構成閉包了:

  func add(x,y int){
      z:=x+y
      return &z
  }
  func main(){
     result:=add(1,2)
     fmt.Println(*result)
  }

所以結論是:

閉包是為了解決函數式語言中一個問題的技術,GC是支持閉包實現的一個機制。

發表后

發表后得到網友的意見,長了見識,更正原文幾個論述:

  • 一是“閉包是某一類語言的現象”,這個表述不准確,語言設計者也許最初就設計了閉包這一特性,GC只不過支持這一特性的一種機制(GC主要還是用來方便內存管理)
  • 有閉包不一定有GC,感謝Lukexywang的評論,不排除其他實現可能性

最后貼倆個鏈接:
About closure, LexicalEnvironment and GC
How to implement closures without gc?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM