閉包就那麼一兩句話誰都能背出來。可是閉包偏偏就是那種初學者十次面試八次可能會遇到,答不上來就是送命題、答得出來也不加分題。為了不讓我們前端開發從入門到放棄,我還是來談談我認為的 JS 裡面的閉包。
閉包是什麼
閉包建立一個詞法作用域,這個作用域裡面的變數被引用之後可以在這個詞法作用域外面被自由訪問,是一個函數和聲明該函數的詞法環境的組合
還有一種說法,閉包就是引用了自由變量的函數,這個自由變量與函數一同存在,即使脫離了創建它的環境。所以你常看到的說閉包就是綁定了上下文環境的函數,也大概是這個意思。只要想明白了,就覺得其實是很簡單的東西,並沒有多麼高深。
在下面的介紹中,我還是比較偏向閉包是一個函數和宣告該函數的詞法環境的組合這種解釋,所以也會基於這種解釋去闡述。
閉包其實是電腦科學裡面的一個概念,並不是JS裡面獨有的。閉包的概念出現在60年代,最早實現閉包的程式語言是Scheme。 ( 別問我 Scheme 是什麼,問了我也不知道,這段是從維基上抄的。)之後,閉包被廣泛使用於函數式程式語言。
JS裡面的閉包
現在,我就發大絕招了,徒手擼一個閉包。
function sayHello(name) { let str = `Hello,${name}`; function say() { console.log(str); } return say; } let myHello = sayHello('abby'); myHello(); // Hello,abby
上面這段程式碼,其實就形成了一個閉包,其中在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; function sayHello(name) { let str = `Hello,${name}`; say = function() { console.log(str); } } let myHello = sayHello('abby'); say(); // Hello,abby
在這個例子裡面,say和宣告它的詞法環境其實也形成了一個閉包,在它的作用域裡面持有了sayHello 這個函數裡面定義的str 變數的引用,因此也能在str變數定義的作用域之外存取它。只要弄清楚閉包的本質即可。
但是在 JS 裡面,最常用的形成閉包的方式便是在一個函數裡面嵌套另一個函數,另一個函數持有父作用域裡面定義的變數。
範例2:同一個呼叫函數產生同一個閉包環境,在裡面宣告的所有函數同時具有這個環境裡面自由變數的參考。
這句話說起來很繞,其實我給個很簡單的例子就可以了。
let get, up, down function setUp() { let number = 20 get = function() { console.log(number); } up = function() { number += 3 } down = function() { number -=2; } } setUp(); get(); // 20 up(); down(); get(); // 21
在這個例子裡面,我們用setUp這個函數產生了一個閉包環境,在這個環境裡面的三個函數共享了這個環境裡面的number 變數的引用,因此都可以對number 進行操作。
範例3:每一個呼叫函數都會建立不同的閉包環境。
還是給一個很簡單的例子。
function newClosure() { let array = [1, 2]; return function(num) { array.push(num); console.log(`array:${array}`); } } let myClosure = newClosure(); let yourClosure = newClosure(); myClosure(3); // array:1,2,3 yourClosure(4); // array:1,2,4 myClosure(5); // array:1,2,3,5
上面這個例子裡面, myClosure 和yourClosure 的賦值語句,也就是newClosure 這個函數被呼叫了兩次,因此創建了兩個不同的閉包環境,因此裡面的變數是互不影響的。
範例4:在迴圈裡面建立閉包
function newClosure() { for(var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }) } } newClosure(); // 5个5
打印的结果大家也知道是5个5,因为 setTimeout 里面的函数保持对 i 的引用,在setTimeout的回调函数被执行的时候这个循环早已经执行完成,这里我之前在另一篇文章里面做过更深入的介绍:深入浅出Javascript事件循环机制(上)。
这里我要说的是我们如何才能得到我们想要的01234,在这里有两种做法。
一种是 创建一个新的闭包对象,这样每个闭包对象里面的变量就互不影响。例如下面的代码种每次 log(i)都会创建不同的闭包对象,所有的回调函数不会指向同一个环境。
function log(i) { return function() { console.log(i); } } function newClosure() { for(var i = 0; i < 5; i++) { setTimeout(log(i)); } } newClosure(); // 0 1 2 3 4
另一种做法就是使用自执行函数,外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。写法如下:
function newClosure() { for(var i = 0; i < 5; i++) { (function(e) { setTimeout(function() { console.log(e); }) })(i) } } newClosure(); // 0 1 2 3 4
看看,写这么多,多累是不是,还是let省事,所以赶紧拥抱 es6 吧。。。
好了,这次是真的结束了,我所理解的闭包大概就是这样了,如果理解有所偏差,欢迎指出,谁当初不是从颗白菜做起的呢,学习前端的小伙伴们可以看看哦!
关于闭包:
以上是JavaScript中的閉包的詳細內容。更多資訊請關注PHP中文網其他相關文章!