作用域鏈
(據(jù)我所知)所有的編程語言都存在作用域鏈。整個代碼存在全局作用域、函數(shù)作用以及塊級作用域。
var a = 1
function foo(){
var b = 2
function bar(){
console.log(a,b)
}
bar()
}
foo()
上述代碼將會打印1和2。
可以看到上面的代碼有三個作用域。全局作用域,foo函數(shù)內部的作用域,bar函數(shù)內部的作用域,它們的關系如下圖。

在函數(shù)bar中,可以訪問foo作用域和全局作用域的變量,這種作用域的包含關系就被稱為作用域鏈。
詞法作用域
一個函數(shù)的作用域有兩種設計方式
- 詞法作用域:根據(jù)函數(shù)被聲明的位置來決定它的作用域
- 動態(tài)作用域:根據(jù)函數(shù)被執(zhí)行的位置來決定它的作用域
參考下面的代碼:
function foo(){
var a = 3
function bar(){
console.log(a)
}
return bar
}
function foo1(){
var a = 4
var bar1 = foo()
bar1()
}
foo1()
其實我們可以明顯的看到,上述代碼實際執(zhí)行中就是bar函數(shù)被執(zhí)行了。我們也可以看到,bar被定義的時候是在foo函數(shù)內部,但是它被執(zhí)行的時候卻是在foo1內部。那么bar中的a到底是3還是4呢?
其實,JS和大多數(shù)編程語言一樣,也遵守詞法作用域的規(guī)則。所以上述代碼的結果為3。
作用域鏈和函數(shù)調用棧
很多人會把作用域鏈和函數(shù)調用棧弄混淆。
上述代碼函數(shù)調用棧的變化:

可以看到,在bar函數(shù)執(zhí)行的時候,foo函數(shù)已經出棧了。所以有的同學認為foo函數(shù)中的局部變量a也就跟著出棧、銷毀了。
其實作用鏈和函數(shù)調用棧沒有任何聯(lián)系,這兩個概念是獨立的系統(tǒng)。JS查找變量的值時候并不是按照函數(shù)調用棧來進行查找的,而是通過作用域鏈,而且查找時符合詞法作用域規(guī)則。
不同的引擎有不同的方式來實現(xiàn)這一點。比如在V8中,。。。。。

閉包
上述的現(xiàn)象也就是人們經常說的閉包:根據(jù)函數(shù)作用域鏈而不是函數(shù)調用棧來查找變量。只要是采取詞法作用域規(guī)則的函數(shù)式編程語言,都必定支持閉包。
這是JS
function foo(){
var a = 3
function bar(){
console.log(a)
}
return bar
}
var biu = foo()
biu() // 3
這是python
def foo():
a = 3
def bar():
print(a)
return bar
biu = foo()
biu() // 3
go
package main
import "fmt"
func foo() func(){
var a = 3
return func(){
fmt.Println(a)
}
};
func main(){
var biu = foo()
biu() // 3
}
可以看到,這些語言都支持閉包。當然,向C++,Java這種不允許將函數(shù)作為變量的語言,也就不再閉包的概念了。
閉包的應用
閉包在for -var中的應用
考慮這樣的代碼
for(var i = 0; i < 2; i++ ) {
setTimeout(() => {
console.log(i)
},10)
}
有的人希望隔10毫秒打印出0、1;但其實上述代碼會打印出2、2。
產生這種現(xiàn)象的原因是var聲明的變量沒有塊級作用域,回調函數(shù)被執(zhí)行的時候i的值已經是2了。
要解決上面的問題,可以將var修改為let,或者使用閉包。
for(var i = 0; i < 3; i++) {
(function(i){
return setTimeout(()=>{
console.log(i)
}, 10)
})(i)
}