天天看點

JavaScript作用域和閉包

作用域和閉包在JavaScript裡非常重要。但是在我最初學習JavaScript的時候,卻很難了解。這篇文章會用一些例子幫你了解它們。

JavaScript作用域和閉包

我們先從作用域開始。

JavaScript的作用域限定了你可以通路哪些變量。有兩種作用域:全局作用域,局部作用域。

在所有函數聲明或者大括号之外定義的變量,都在全局作用域裡。

不過這個規則隻在浏覽器中運作的JavaScript裡有效。如果你在Node.js裡,那麼全局作用域裡的變量就不一樣了,不過這篇文章不讨論Node.js。

一旦你聲明了一個全局變量,那麼你在任何地方都可以使用它,包括函數内部。

盡管你可以在全局作用域定義變量,但我們并不推薦這樣做。因為可能會引起命名沖突,兩個或更多的變量使用相同的變量名。如果你在定義變量時使用了const或者let,那麼在命名有沖突時,你就會收到錯誤提示。這是不可取的。

如果你定義變量時使用的是var,那第二次定義會覆寫第一次定義。這也會讓代碼更難調試,也是不可取的。

是以,你應該盡量使用局部變量,而不是全局變量

在你代碼某一個具體範圍内使用的變量都可以在局部作用域内定義。這就是局部變量。

JavaScript裡有兩種局部作用域:函數作用域和塊級作用域。

我們從函數作用域開始。

當你在函數裡定義一個變量時,它在函數内任何地方都可以使用。在函數之外,你就無法通路它了。

比如下面這個例子,在sayHello函數内的hello變量:

你在使用大括号時,聲明了一個const或者let的變量時,你就隻能在大括号内部使用這一變量。

在下例中,hello隻能在大括号内使用。

塊級作用域是函數作用域的子集,因為函數是需要用大括号定義的,(除非你明确使用return語句和箭頭函數)。

當使用function定義時,這個函數都會被提升到目前作用域的頂部。是以,下面的代碼是等效的:

使用函數表達式定義時,函數就不會被提升到變量作用域的頂部。

因為這裡有兩個變量,函數提升可能會導緻混亂,是以就不會生效。是以一定要在使用函數之前定義函數。

在分别定義的不同的函數時,雖然可以在一個函數裡調用一個函數,但一個函數依然不能通路其他函數的作用域内部。

下面這例,second就不能通路firstFunctionVariable這一變量。

如果在函數内部又定義了函數,那麼内層函數可以通路外層函數的變量,但反過來則不行。這樣的效果就是詞法作用域。

外層函數并不能通路内部函數的變量。

如果把作用域的機制可視化,你可以想象有一個雙向鏡(單面透視玻璃)。你能從裡面看到外面,但是外面的人不能看到你。

JavaScript作用域和閉包

函數作用域就像是雙向鏡一樣。你可以從裡面向外看,但是外面看不到你。

嵌套的作用域也是相似的機制,隻是相當于有更多的雙向鏡。

JavaScript作用域和閉包

多層函數就意味着多個雙向鏡。

了解前面關于作用域的部分,你就能了解閉包是什麼了。

你在一個函數内建立另一個函數時,就相當于建立了一個閉包。内層函數就是閉包。通常情況下,為了能夠使得外部函數的内部變量可以通路,一般都會傳回這個閉包。

因為内部函數是傳回值,是以你可以簡化函數聲明的部分:

因為閉包可以通路外層函數的變量,是以他們通常有兩種用途:

減少副作用

建立私有變量

當你在函數傳回值時執行某些操作時,通常會發生一些副作用。副作用在很多情況下都會發生,比如Ajax調用,逾時處理,或者哪怕是console.log的輸出語句:

當你使用閉包來控制副作用時,你實際上是需要考慮哪些可能會混淆代碼工作流程的部分,比如Ajax或者逾時。

要把事情說清楚,還是看例子比較友善:

比如說你要給為你朋友慶生,做一個蛋糕。做這個蛋糕可能花1秒鐘的時間,是以你寫了一個函數記錄在一秒鐘以後,記錄做完蛋糕這件事。

為了讓代碼簡短易讀,我使用了ES6的箭頭函數:

如你所見,做蛋糕帶來了一個副作用:一次延時。

更進一步,比如說你想讓你的朋友能選擇蛋糕的口味。那麼你就給做蛋糕makeCake這個函數加了一個參數。

是以當你調用這個函數時,一秒後這個新口味的蛋糕就做好了。

但這裡的問題是,你并不想立刻知道蛋糕的味道。你隻需要知道時間到了,蛋糕做好了就行。

要解決這個問題,你可以寫一個prepareCake的功能,儲存蛋糕的口味。然後,在傳回在内部調用prepareCake的閉包makeCake。

從這裡開始,你就可以在你需要的時調用,蛋糕也會在一秒後立刻做好。

這就是使用閉包減少副作用:你可以建立一個任你驅使的内層閉包。

前面已經說過,函數内的變量,在函數外部是不能通路的既然不能通路,那麼它們就可以稱作私有變量。

然而,有時候你确實是需要通路私有變量的。這時候就需要閉包的幫助了。

這個例子裡的saySecretCode函數,就在原函數外暴露了secretCode這一變量。是以,它也被成為特權函數。

Chrome和Firefox的開發者工具都使我們能很友善的調試在目前作用域内可以通路的各種變量一般有兩種方法。

第一種方法是在代碼裡使用debugger關鍵詞。這能讓浏覽器裡運作的JavaScript的暫停,以便調試。

下面是prepareCake的例子:

打開Chrome的開發者工具,定位到Source頁下(或者是Firefox的Debugger頁),你就能看到可以通路的變量了。

JavaScript作用域和閉包

使用debugger調試prepareCake的作用域。

你也可以把debugger關鍵詞放在閉包内部。注意對比變量的作用域:

JavaScript作用域和閉包

調試閉包内部作用域

第二種方式是直接在代碼相應位置加斷點,點選對應的行數就可以了。

JavaScript作用域和閉包

通過斷點調試作用域

閉包和作用域并不是那麼難懂。一旦你使用雙向鏡的思維去了解,它們就非常簡單了。

當你在函數裡聲明一個變量時,你隻能在函數内通路。這些變量的作用域就被限制在函數裡了。

如果你在一個函數内又定義了内部函數,那麼這個内部函數就被稱作閉包。它仍可以通路外部函數的作用域。

有問題就直接問吧。我盡量早點回複你們的問題。

如果你喜歡本文,也許你會喜歡我在部落格和訂閱郵件裡寫的其他前端開發相關的文章。我剛建立自己的新品牌,(而且是免費的哦!)一個email的課程:JavaScript Roadmap。(希望你喜歡!)

本文作者:佚名

來源:51CTO

繼續閱讀