對詞法環境和執行上下文不太瞭解的朋友,建議先閱讀系列文章的前兩篇,有助於理解本文,連接 -> 深刻ECMAScript系列目錄地址(持續更新中...)git
首先咱們來看一個例子(來自冴羽大大的博客JavaScript深刻之詞法做用域和動態做用域):github
var scope = 'global scope'
function checkscope(){
var scope = 'local scope'
function f(){
return scope
}
return f()
}
checkscope()
複製代碼
var scope = 'global scope'
function checkscope(){
var scope = 'local scope'
function f(){
return scope
}
return f
}
checkscope()()
複製代碼
這裏就不賣關子了,兩段代碼的運行結果都是local scope
。這是JavaScript做用域機制決定的。閉包
做用域:指程序源代碼中定義變量的區域。是規定代碼對變量訪問權限的規則。ecmascript
你們可能據說過JavaScript採用的是詞法做用域(靜態做用域),沒據說過也沒有關係,很好理解,意思就是函數的做用域在函數定義的時候就肯定了,也就是說函數的做用域取決於函數在哪裏定義,和函數在哪裏調用並沒有關係。異步
由以前的文章深刻ECMAScript系列(二):執行上下文咱們可知:任意的JavaScript可執行代碼(包括函數)被執行時,會建立新的執行上下文及其詞法環境。函數
既然詞法環境是在代碼塊運行時才建立的,那爲何又說函數的做用域在函數定義的時候就肯定了呢?這就牽扯到了函數的聲明及調用了。post
在以前的文章深刻ECMAScript系列(二):執行上下文中說過,代碼塊內的函數聲明在標識符實例化及初始化階段就會被初始化並分配相應的函數體。ui
在這個階段還會會給函數設置一個內置屬性[[Environment]]
,指向函數聲明時所在的執行上下文的詞法環境。this
當聲明過的函數被調用時,會建立新的執行上下文和新的詞法環境,這個新建立的詞法環境的對外部詞法環境的引用outer
屬性將會指向函數的[[Environment]]
內置屬性,也就是函數聲明時所在的執行上下文的詞法環境。spa
而變量的查找又是經過詞法環境及其外部引用進行的,因此說函數的做用域取決於函數在哪裏定義,和函數在哪裏調用並沒有關係。
總結一下,兩個關鍵點:
[[Environment]]
,指向函數聲明時所在的執行上下文的詞法環境。outer
都指向函數的內置屬性[[Environment]]
。因此說函數的做用域取決於函數在哪裏定義,和函數在哪裏調用並沒有關係。
咱們回頭看文章開頭的兩個例子:
var scope = 'global scope'
function checkscope(){
var scope = 'local scope'
function f(){
return scope
}
}
// function f
f: {
[[ECMAScriptCode]]: ..., // 函數體代碼
[[Environment]]: { // 函數f 定義時所在執行上下文的詞法環境,也就是函數checkscope運行時建立的詞法環境
EnvironmentRecord: { // 環境記錄上綁定了變量scope和函數f
scope: 'local scope',
f: Function f
},
outer: { // 外部詞法環境引用指向全局詞法環境
EnvironmentRecord: { // 全局環境記錄上綁定了變量scope和函數checkscope
scope: 'global scope',
checkscope: Function checkscope
},
outer: null // 全局詞法環境無外部詞法環境引用
}
},
... // 其餘屬性
}
複製代碼
函數f
定義在函數checkscope
內部,因此函數f
不論在函數checkscope
的內部調用,仍是做爲返回值返回後在外部調用,其詞法環境的外部引用永遠是函數checkscope
運行時建立的詞法環境,變量scope
也只用往外尋找一層詞法環境,在函數checkscope
運行時建立的詞法環境中找到,值爲'local scope'
,不用再往外查找。因此上面兩個例子的運行結果都是local scope
。
首先看看MDN上對閉包的定義:
閉包:閉包是函數和聲明該函數的詞法環境的組合。
從理論角度來講:全部的JavaScript函數都是閉包。 由於函數聲明時會設置一個內置屬性[[Environment]]
來記錄當前執行上下文的詞法環境。
從實踐角度來講: 咱們平時所說的閉包應該叫「有意義的閉包」:
在Dmitry Soshnikov的文章中描述具備如下特色的函數叫作閉包:
自由變量: 在函數中使用,但既不是函數參數也不是函數的局部變量的變量。
我本身的理解是如下兩點:
最簡單的閉包就是父函數內返回一個函數,返回函數內引用了父函數內變量:
var scope = 'global scope'
function checkscope(){
var scope = 'local scope'
function f(){
return scope
}
return f
}
var closure = checkscope()
closure()
複製代碼
將開頭的第二個例子稍微變一下,調用checkscope
會返回一個函數,咱們將其賦值給closure
,此時closure
函數就是一個閉包,因爲它是在調用checkscope
時建立的,內置屬性[[Environment]]
指向調用checkscope
時建立的詞法環境,所以不管在何處調用closure
函數,返回結果是'local scope'
。
我理解閉包的本質做用就兩點,任何閉包的應用都離不開這兩點:
關於延長變量的生命週期,本質實際上是延長詞法環境的生命週期,通常函數的詞法環境在函數返回後就被銷燬,可是閉包會保存對建立時所在詞法環境的引用,即使建立時所在的執行上下文被銷燬,但建立時所在詞法環境依然存在,以達到延長變量的生命週期的目的。
經過閉包能夠模擬塊級做用域,很經典的例子就是for循環中使用定時器延遲打印的問題。
// ES6以前無塊級做用域,多個定時器內的回調函數引用同一個i
// for循環爲同步,定時器內函數爲異步,循環結束後i已經變爲4
// 定時期內函數觸發時訪問變量i都是4
// 理解的關鍵在於for循環內代碼是同步的,包括setTimtout自己
// 可是setTimeout定時器內的回調函數是異步的
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i)
}, i * 1000)
}
複製代碼
// 使用當即執行函數,將i做爲參數傳入,可保存變量i的實時值
for(var i = 1; i <= 3; i++){
(i => {
setTimeout(() => {
console.log(i)
}, i * 1000)
})(i)
}
// 如下代碼可達到相同效果
for(var i = 1; i <= 3; i++){
(() => {
var j = i
setTimeout(() => {
console.log(j)
}, j * 1000)
})()
}
// 如下代碼也可達到相同效果
for(var i = 1; i <= 3; i++){
var closure = (function() {
var j = i
return () => {
console.log(j)
}
})()
setTimeout(closure, i * 1000)
}
複製代碼
閉包模擬塊級做用域瞭解便可,畢竟ES6以後咱們有了let
來實現塊級做用域,實現塊級做用域的具體原理詳見深刻ECMAScript系列(二):執行上下文
模塊模式是指將全部的數據和功能都封裝在一個函數內部(私有的),只向外暴露一個包含多個屬性方法的對象或函數。
var counter = (function() {
var privateCounter = 0
function changeBy(val) {
privateCounter += val
}
return {
increment: function() {
changeBy(1)
},
decrement: function() {
changeBy(-1)
},
value: function() {
return privateCounter;
}
}
})()
複製代碼
另外例如underscore
等一些js庫的實現也使用到了閉包。
(function(){
var root = this;
var _ = {};
root._ = _;
// 外部不可訪問的方法
function tool() {
// ...
}
// 外部可訪問的方法
_.xxx = function() {
tool()
// ...
}
})()
複製代碼
柯里化的目的在於避免頻繁調用具備相同參數函數的同時,又可以輕鬆的重用。
// 假設咱們有一個求長方形面積的函數
function getArea(width, height) {
return width * height
}
// 若是咱們碰到的長方形的寬總是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
// 咱們可使用閉包柯里化這個計算面積的函數
function getArea(width) {
return height => {
return width * height
}
}
const getTenWidthArea = getArea(10)
// 以後碰到寬度爲10的長方形就能夠這樣計算面積
const area1 = getTenWidthArea(20)
// 並且若是遇到寬度偶爾變化也能夠輕鬆複用
const getTwentyWidthArea = getArea(20)
複製代碼
其餘例如計數器、延遲調用、回調等閉包的應用這裏就不作過多講解,其核心思想仍是建立私有變量 和 延長變量的生命週期。
function fun(n,o){
console.log(o);
return {
fun: function(m){
return fun(m,n);
}
};
}
var a = fun(0); // ?
a.fun(1); // ?
a.fun(2); // ?
a.fun(3); // ?
var b = fun(0).fun(1).fun(2).fun(3); // ?
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?
複製代碼
運用咱們以前總結的知識來分析一下:
function fun(n,o){
console.log(o);
return {
fun: function(m){
return fun(m,n);
}
};
}
// 運行fun(0),未傳入第二個參數,故打印undefined,最後返回一個對象,內有一個fun方法
// (注意此方法與外部fun函數不一樣,下同)
var a = fun(0); // undefined
// 對象內fun方法爲閉包,記錄對fun(0)執行時的詞法環境,內部綁定一個參數n,值爲0
// 將返回對象賦值於a,執行a.fun(x)時,無論傳入的第一個參數是什麼
// 第二個參數n都將在以前fun(0)執行時的詞法環境內找到,值爲0
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0
// 每次調用fun函數都會返回一個對象
// 對象內又一個fun方法,爲閉包,記錄建立該對象及對象方法時的詞法環境
// 故每次調用對象的fun方法,內部執行fun函數時的第二個參數總會在建立該對象時的詞法環境內找到
// 值即爲建立該對象的函數的第一個參數
// 因此除了第一次打印值爲undefined,其他皆爲上次調用fun時傳入的第一個參數
var b = fun(0).fun(1).fun(2).fun(3); // undefined
// 0
// 1
// 2
// 相似上面的分析,c爲一個對象,有一個fun方法,爲閉包
// 該閉包記錄了建立它時的詞法環境,上面有兩個綁定,{n: 1, o: 0}
// 因此c.fun(x)相似調用時,不論傳參是什麼,都將打印1
// 須要注意fun(0)調用時打印了undefined,fun(0).fun(1)調用時打印了0
var c = fun(0).fun(1); // undefined
// 0
c.fun(2); // 1
c.fun(3); // 1
複製代碼
OK,本篇文章就寫到這裏,相信你們對於閉包也有了必定本身的理解。關於深刻ECMAScript系列文章以後的主題你們也能夠在評論區留言討論。
歡迎前往閱讀系列文章,若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。
菜鳥一枚,若是有疑問或者發現錯誤,能夠在相應的 issues 進行提問或勘誤,與你們共同進步。