1. 程式人生 > >談一談閉包

談一談閉包

每次開啟Atom準備寫文章的時候, 都要糾結如何開頭… 煩~~

今天這篇文章我們來探討一下閉包, 因為我在查閱很多資料時, 發現這些文章對於閉包的理解很多都是有出入的, 所以今天我們來探討一下什麼才是閉包. 當然, 這篇文章大多數是概念性的東西, 程式碼演示可能會涉及到幾種不同的語言實現, 不過我會在程式碼開頭標識出是哪種語言. 另外, 本文除了探討閉包, 還可能會出現譬如柯里化等概念, 因為在這些概念裡應用閉包可能是對閉包的作用的很好的說明. 最後需要說明的: 本文僅僅是我對閉包的理解, 當然可能存在不合理的地方, 不對的地方希望有人可以在評論中給我指出.

什麼是閉包

好, 下面開始進入主題, 首先一個問題, 什麼是閉包? 對於我們這些習慣了指令式程式設計, 尤其是java這種完全面向物件的語言的人, 閉包可能是一個很陌生的概念, 閉包時常在函式式語言中被提及, 那閉包到底是一個什麼概念? 下面對閉包的解釋來自維基百科.

在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。

閉包更多強調的是引用環境, 函式在定義時因為使用到了自由變數, 函式的呼叫和定義時的引用環境不同, 所以它必須要清楚的瞭解到自己定義時的引用環境, 在呼叫的時候需要切換到它定義的引用環境中執行, 這樣就形成了閉包.

更通俗的講, 因為函式引用了自由變數, 當它執行的環境和定義的環境不同時, 在執行它的地方就不僅僅是拿到這個函式這麼簡單了, 因為它必須要清楚它引用的自由變數, 所以最好的方式是把此時的引用環境和函式本身一併返回, 這樣就是閉包.

所以, 閉包 = 函式 + 引用環境

分解一下就是閉包形成的必要條件.

  1. 函式引用自由變數
  2. 函式的執行環境和自由變數的宣告環境不同

只有符合了以上兩點, 函式的執行點才不僅僅需要函式本身, 而是函式+引用環境.

兩個重點是自由變數, 引用環境, 但就這幾個字可能對於一些對閉包一知半解的人來說確是很難理解, 因為在大部分人印象裡閉包應該是這樣的.

// kotlin
list.map { println it}

這樣的一個例項中沒有任何地方提及到自由變數. 其實這壓根和閉包沒有任何關係, 這僅僅是一種語法-lambda表示式, 很多人將lambda表示式想當然的認為是閉包. 再來看看下面語法.

// groovy
android {
  compileSdkVersion 24
}

對於搞android的人來說這行程式碼並不陌生, 很多人把這個也理解成閉包, 和上面kotlin的程式碼一樣, 其實這和閉包也沒有任何關係.

那什麼樣的才算閉包? 上面提到了, 引用了自由變數的函式, 這句話裡有一個名詞-自由變數, 那什麼又是自由變數呢? 理解了這個名詞後才能繼續理解這句話.

在某個作用域內使用了其他作用域宣告的變數, 那該變數就是自由變數

上面的概念是是我自己寫的, 可能有點難理解, 來看看程式碼

// javascript
var a = 10;
function add5() {
  return a + 5;
}

上面的變數a即為自由變數, a變數不是在函式add5的作用域中宣告的, 卻在函式add5中被使用.

好了, 知道了什麼是自由變數, 那下面我們來看看什麼是閉包.

// javascript
function add(a) {
  return function(b) {
    return a + b;
  };
}

var add10 = add(10);
var result = add10(5);

上面的例子是最典型的閉包, add函式返回了一個函式, 這個匿名函式引用了add函式的一個a變數, 所以a變數是一個自由變數. 而且, 這個匿名函式的執行點不在變數a宣告的作用域內, 根據上面的概念, 這樣就形成了閉包.

再來看看下面的程式碼. 下面的程式碼來自apple官方的swift文件中閉包章節.

// swift
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

這樣的算不算閉包? 30s思考一下, 在心裡有答案了再往下看…










當然不是, 雖然是來自官方的文章, 但這裡確實是對概念的錯誤理解. 將上面的程式碼普通化來看看.

// swift
func sorter(s1: String, s2: String) -> Bool {
  return s1 > s2
}

reversedNames = names.sorted(sorter)

一目瞭然, 上面的程式碼絲毫沒有看到對自由變數的引用, 這裡僅僅算是高階函式罷了… 所以很多時候官方文件也是會誤導人的…

相似的程式碼, 再來看看kotlin官方文件上閉包的例項

// kotlin
var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

這是不是形成閉包了? 把它普通化來看看, 畢竟去除語法糖才更適合人理解.

// kotlin
var sum = 0
var filter: (Int) -> Bool = { a -> a > 0 }
var each: (Int) -> Int = { a -> sum + a }

ints.filter(filter).forEach(each)

主要看這個each函式, 很明顯, 這裡有對自由變數a的引用, 而且each函式的定義和執行時的引用環境不同, 所以上面的例項確實形成了閉包. 不過我還是要吐槽一下kotlin的文件, 它將閉包放在了Lambda章節裡, 雖然例項程式碼確實是形成了閉包, 但肯定會去一些人產生誤導作用, 畢竟是兩個毫無關係的概念.

閉包的作用

通過概念和例項程式碼, 很明顯閉包的存在改變了變數的生命週期, 大部分情況下它可以將自由變數的生命週期延遲到閉包函式的執行, 而函式式中最重要的一個思想是儘可能多使用純函式(純函式是指對於相同的輸入必定有相同的輸入的函式), 在純函式中如果想要保持一個變數, 那閉包肯定是最佳選擇. 來看一下例項.

// javascript
function nameBy(lastName) {
  return function(firstName) {
    retrn firstName + " " + lastName;
  }
}

var group = nameBy("Jordan")
var michael = group("Michael")
var susan = group("Susan")

另外一個函式式中重要的概念-柯里化大部分情況下也離不開閉包的支援, 什麼是柯里化?

柯里化(英語:Currying),是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。

其實上面的大部分例子中已經實現了柯里化, 柯里化的好處就是大大的提高了函式的靈活性.

// javascript
var add = (x, y) => x + y

// user
alert(add(10, 20))
alert(add(10, 30))
alert(add(10, 40))

上面對add函式的呼叫其實都是10+y的形式, 很多時候我們為了封裝, 又對10+y這樣的式子進行封裝.

// javascript
var add = (x, y) => x + y
var add10 = (y) => 10 + y

// user
alert(add10(20))
alert(add10(30))
alert(add10(40))

這裡有一個不好的地方就是add10這個函式的封裝只能適用於10+y, 雖然實現了柯里化, 但是對於使用者來說靈活性不夠, 其實這裡我們可以利用閉包對add函式稍加改造, 既方便使用又不失靈活性

// javascript
var add = (x) => (y) => x + y

// user
var add10 = add(10)
alert(add10(20))
alert(add10(30))
alert(add10(40))

再來看, 上面例項中的函式add的定義其實不太符合我們人類的思想, 這方面, 其實很多函數語言程式設計語言中已經實現了自動柯里化, 來看個例項.

// elm
add x y =
 x + y

add 10 20 // result is 30
add10 = add 10
add10 20 // result is 30

這些支援自動柯里化的語言可以根據實參的個數推斷出返回的型別, 所以可以很輕鬆的實現柯里化, 而不必在我們定義函式的時候過多考慮. 其實javascript也可以實現自動柯里化, 大家可以參考我的一個demo-https://github.com/qibin0506/js-curry.

好了, 雖然說了這麼多關於柯里化的東西, 其實都是對閉包的一些應用, 可以幫助大家去理解閉包. 最後留幾個程式碼片段, 大家判斷一下是否實現了閉包.

// 片段 1
function f1() {
  return function() {
    return "hello world"
  }
}

f1()()
// 片段 2
function f1() {
  return function(arg) {
    return arg + 10
  }
}

f1()(20)
// 片段 3
var name = "qibin";
var f = function(name) {
  return name + " HI"
}

f(name)
// 片段 4
function someFunction() {
    var name = "qibin"
    var f = funtion() {
      return name + " HI"
    }
    f()
}
someFunction()
// 片段 5
var getName = function() {
  var name = "qibin"
  var f = function() {
    return name + " HI"
  }

  prntName(f)
}

function prntName(f) {
  console.log(f())
}