我還是想談談JS裡面的閉包

其實自己不太寫閉包了的,就那麼一兩句話誰都能背出來。可是閉包偏偏就是那種初學者十次面試八次可能會遇到,答不上來就是送命題、答得出來也不加分題。為了不讓我們前端開發從入門到放棄,我還是來談談我認為的 JS 裡面的閉包。

閉包是什麼

閉包創建一個詞法作用域,這個作用域裡面的變數被引用之後可以在這個詞法作用域外面被自由訪問,是一個函數和聲明該函數的詞法環境的組合

還有一種說法,閉包就是引用了自由變數的函數,這個自由變數與函數一同存在,即使脫離了創建它的環境。所以你常看到的說閉包就是綁定了上下文環境的函數,也大概是這個意思。只要想明白了,就覺得其實是很簡單的一個東西,並沒有多麼高深。

在下面的介紹中,我還是比較偏向閉包是一個函數和聲明該函數的詞法環境的組合這種解釋,所以也會基於這種解釋去闡述。

閉包其實是計算機科學裡面的一個概念,並不是JS裡面獨有的。閉包的概念出現於60年代,最早實現閉包的程序語言是Scheme。(別問我 Scheme 是什麼,問了我也不知道,這段是從維基上抄的。)之後,閉包被廣泛使用於函數式編程語言。

JS裡面的閉包

現在,我就發大絕招了,徒手擼一個閉包。

function sayHello(name) {n let str = `Hello,${name}`;n function say() {n console.log(str);n }n return say;n}nnlet myHello = sayHello(abby);nmyHello(); // Hello,abby n

上面這段代碼,其實就形成了一個閉包,其中在 sayHello 這個函數裡面定義的函數 say 和其聲明它的詞法環境就形成了一個閉包,因為它引用了sayHello 裡面定義的一個變數 str,並且將 say 這個函數 return 了出去,這樣在 sayHello 這個函數的外面也能訪問它裡面定義的變數 str,就好像 say 這個函數和這個變數綁定了一樣。

看到這裡可能會疑問為什麼在外部還能訪問到這個變數呢,因為在有些語言中,一般認為函數的局部變數只在函數的執行期間可訪問。說到這裡又不得不說到執行環境,不太了解的朋友可能先去看我這篇文章:你不知道的執行上下文。其實當執行到let myHello = sayHello(abby);這段代碼的時候按理會銷毀掉 sayHello()的執行環境,但是這裡卻沒有,原因是因為 sayHello() 返回的是一個函數,這個函數裡面的 str 引用了外部的變數 str,如果銷毀了就找不到了,因此 sayHello() 這個函數的執行環境會一直在內存中,所以會有閉包會增加內存開銷balabala之類的。

其實說到這裡,閉包就應該是說完了的。。。但是畢竟是話很多的小仙女嘛,我們再來看幾個例子,鞏固一下。

舉個栗子

栗子1: 閉包並不是一定需要 return 某個函數

雖然常見的閉包都是 return 出來一個函數,但是閉包並不一定非要 return,return 出一個函數只是為了能在作用域範圍之外訪問一個變數,我們用另一種方式也能做到,比如:

let say;nfunction sayHello(name) {n let str = `Hello,${name}`;n say = function() {n console.log(str);n }n}nlet myHello = sayHello(abby);nsay(); // Hello,abby n

在這個例子裡面,say和聲明它的詞法環境其實也形成了一個閉包,在它的作用域裡面持有了 sayHello 這個函數裡面定義的 str 變數的引用,因此也能在 str 變數定義的作用域之外訪問它。只要弄清楚閉包的本質即可。

但是在 JS 裡面,最常用的形成閉包的方式便是在一個函數裡面嵌套另一個函數,另一個函數持有父作用域裡面定義的變數。

栗子2: 同一個調用函數生成同一個閉包環境,在裡面聲明的所有函數同時具有這個環境裡面自由變數的引用。

這句話說起來很繞,其實我給個很簡單的例子就可以了。

let get, up, downnfunction setUp() {n let number = 20n get = function() {n console.log(number);n }n up = function() {n number += 3n }n down = function() {n number -=2;n }n}nsetUp();nget(); // 20nup();ndown();nget(); // 21n

在這個例子裡面,我們用setUp這個函數生成了一個閉包環境,在這個環境裡面的三個函數共享了這個環境裡面的 number 變數的引用,因此都可以對 number 進行操作。

栗子3: 每一個調用函數都會創建不同的閉包環境。

還是給一個很簡單的例子。

function newClosure() {n let array = [1, 2];n return function(num) {n array.push(num);n console.log(`array:${array}`);n }n}nlet myClosure = newClosure();nlet yourClosure = newClosure();nmyClosure(3); // array:1,2,3nyourClosure(4); // array:1,2,4nmyClosure(5); // array:1,2,3,5 n

上面這個例子裡面, myClosure 和 yourClosure 的賦值語句,也就是 newClosure 這個函數被調用了兩次,因此創建了兩個不同的閉包環境,因此裡面的變數是互不影響的。

栗子4: 在循環裡面創建閉包

在 let 被引入之前,一個常見的錯誤就是在循環中創建閉包,例如:

function newClosure() {n for(var i = 0; i < 5; i++) {n setTimeout(function() {n console.log(i);n })n }n}nnewClosure(); // 5個5 n

列印的結果大家也知道是5個5,因為 setTimeout 裡面的函數保持對 i 的引用,在setTimeout的回調函數被執行的時候這個循環早已經執行完成,這裡我之前在另一篇文章裡面做過更深入的介紹:深入淺出Javascript事件循環機制(上)。

這裡我要說的是我們如何才能得到我們想要的01234,在這裡有兩種做法。

一種是 創建一個新的閉包對象,這樣每個閉包對象裡面的變數就互不影響。例如下面的代碼種每次 log(i)都會創建不同的閉包對象,所有的回調函數不會指向同一個環境。

function log(i) {n return function() {n console.log(i);n }n}nnfunction newClosure() {n for(var i = 0; i < 5; i++) {n setTimeout(log(i));n }n}nnewClosure(); // 0 1 2 3 4n

另一種做法就是使用自執行函數,外部的匿名函數會立即執行,並把 i 作為它的參數,此時函數內 e 變數就擁有了 i 的一個拷貝。當傳遞給 setTimeout 的匿名函數執行時,它就擁有了對 e 的引用,而這個值是不會被循環改變的。寫法如下:

function newClosure() {n for(var i = 0; i < 5; i++) {n (function(e) {n setTimeout(function() {n console.log(e);n })n })(i) n }n}nnewClosure(); // 0 1 2 3 4n

看看,寫這麼多,多累是不是,還是let省事,所以趕緊擁抱 es6 吧。。。

好了,這次是真的結束了,我所理解的閉包大概就是這樣了,如果理解有所偏差,歡迎指出,誰當初不是從顆白菜做起的呢

如果看完之後還是很懵的話,那就找個有陽光的午後,在窗邊沏一杯西湖龍井,然後再把這篇文章和下面的參考鏈接看個千百遍。。如果都看了,還是實在理解不了的話:

非官方勸退:前端開發,從入門到放棄

參考鏈接:

閉包

閉包的概念、形式與應用

閉包

閉包和引用

深入理解javascript原型和閉包(15)——閉包


推薦閱讀:

開箱即用的網站可訪問性提升指南
利用Dawn工程化工具實踐MobX數據流管理方案
Yarn vs npm:你需要知道的一切
如何用 Vue.js 實現一個建站應用
已放棄了遊戲開發,不知道選擇什麼開發?

TAG:前端开发 | 前端工程师 |