聲明提升和閉包
提升
JavaScript 是一門解釋型語(yǔ)言鹿霸,原則上是不需要編譯的桨醋。但是它在代碼執(zhí)行之前會(huì)有一個(gè)編譯的流程重荠,這個(gè)流程發(fā)生在代碼執(zhí)行的前一刻。
示例:
console.log(a) // undefined
var a = 2
console.log(a) // 2
在這段代碼中晌端,按照常規(guī)邏輯,引擎通過(guò)作用域沒(méi)有找到恬砂,可能會(huì)拋出一個(gè)錯(cuò)誤 ReferenceError: a is not defined咧纠,但是這種情況并沒(méi)有發(fā)生。
實(shí)際上泻骤,在代碼運(yùn)行的前一刻漆羔,會(huì)執(zhí)行預(yù)編譯操作。當(dāng)遇到 var a = 2 這樣的語(yǔ)句時(shí)狱掂,會(huì)被拆分成兩部分演痒,var a(變量聲明)+ a = 2(變量賦值)。其中趋惨,變量聲明會(huì)發(fā)生在預(yù)編譯階段鸟顺,把 a 這個(gè)變量放到全局作用域中,沒(méi)有賦值器虾,則為 undefined讯嫂。
當(dāng)執(zhí)行第一個(gè)打印操作的時(shí)候,已經(jīng)完成了預(yù)編譯相關(guān)的處理兆沙,所以可以訪問(wèn)a欧芽,得到undefined;然后開(kāi)始執(zhí)行后面的賦值操作葛圃;等到第二個(gè)打印操作的時(shí)候已經(jīng)可以正常訪問(wèn) a 的值了渐裸。所以上述代碼也可以像下面這樣理解:
var a
console.log(a)
a = 2
console.log(2)
變量聲明提升,函數(shù)聲明整體提升
foo() // foo
bar() // TypeError: bar is not a function(此時(shí)bar為undefined)
function foo() {
console.log(foo.name)
}
var bar = function () {
console.log(bar.name)
}
變量聲明提升装悲,var 聲明的變量在預(yù)編譯的時(shí)候會(huì)被提升到當(dāng)前執(zhí)行環(huán)境的頂部昏鹃;
函數(shù)聲明整體提升,以函數(shù)聲明聲明函數(shù)的函數(shù)诀诊,會(huì)被整體提升到當(dāng)前執(zhí)行環(huán)境的頂部洞渤,所以執(zhí)行語(yǔ)句可以寫在函數(shù)聲明前面。函數(shù)表達(dá)式不可以属瓣,因?yàn)楹瘮?shù)表達(dá)式走的是變量聲明提升的規(guī)則载迄。
同一個(gè)變量既賦值給了變量,又作為函數(shù)聲明的標(biāo)識(shí)符
console.log(foo) // function
var foo = 1
console.log(foo) // 1
function foo() {
console.log(foo.name)
}
console.log(foo)
/**
* 這里常規(guī)思路是 function抡蛙,其實(shí)還是 1
* 因?yàn)楹瘮?shù)聲明這段代碼已經(jīng)被整體提升到了當(dāng)前執(zhí)行環(huán)境的頂部护昧,已經(jīng)在前面執(zhí)行過(guò)了
*/
閉包
一個(gè)閉包的基本示例:
function foo() {
let a = 3
let tempFunc = function () {
return a
}
return tempFunc
}
// 通過(guò) bar 標(biāo)識(shí)符引用了 foo 內(nèi)部的函數(shù) tempFunc
let bar = foo()
console.log(bar())
在這里,函數(shù) bar 的詞法作用域能夠訪問(wèn) foo 的內(nèi)部作用域粗截,這讓foo的內(nèi)部函數(shù)能夠在自己的詞法作用域外執(zhí)行惋耙,但是依然能夠訪問(wèn)自身詞法作用域的變量。
在foo()執(zhí)行后, 通常會(huì)期待foo()的整個(gè)內(nèi)部作用域都被銷毀绽榛, 因?yàn)槲覀冎酪嬗欣厥掌饔脕?lái)釋放不再使用的內(nèi)存空間湿酸。 由于看上去foo()的內(nèi)容不會(huì)再被使用, 所以很自然地會(huì)考慮對(duì)其進(jìn)行回收灭美。
而閉包的“ 神奇”之處正是可以阻止這件事情的發(fā)生推溃。 事實(shí)上內(nèi)部作用域依然存在, 因此沒(méi)有被回收届腐。誰(shuí)在使用這個(gè)內(nèi)部作用域铁坎?原來(lái)是bar()本身在使用。
拜bar()所聲明的位置所賜犁苏, 它擁有涵蓋foo()內(nèi)部作用域的閉包硬萍, 使得該作用域能夠一直存活,以供bar()在之后任何時(shí)間進(jìn)行引用傀顾。
bar()依然持有對(duì)該作用域的引用襟铭,而這個(gè)引用就叫作閉包碌奉。
這個(gè)函數(shù)在定義時(shí)的詞法作用域以外的地方被調(diào)用短曾。 閉包使得函數(shù)可以繼續(xù)訪問(wèn)定義時(shí)的詞法作用域。
基本示例的變種:
let bar
function foo() {
let a = 2
let tempFunc = function () {
return a
}
bar = tempFunc
}
foo()
console.log(bar())
無(wú)論通過(guò)何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外赐劣, 它都會(huì)持有對(duì)原始定義作用域的引用嫉拐,無(wú)論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。
循環(huán)中的閉包
常見(jiàn)考題:
for (var i = 1; i <= 5; i ++) {
setTimeout(function () {
console.log(i)
}, i*1000)
}
// 輸出什么魁兼?
正常情況下婉徘,我們對(duì)這段代碼行為的預(yù)期是分別輸出數(shù)字1~5,每秒一次咐汞,每次一個(gè)盖呼。
但實(shí)際上,這段代碼在運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出五次6化撕。
首先解釋6是從哪里來(lái)的几晤。 這個(gè)循環(huán)的終止條件是i不再<=5。條件首次成立時(shí)i的值是6植阴。因此蟹瘾,輸出顯示的是循環(huán)結(jié)束時(shí)i的最終值。
仔細(xì)想一下掠手, 這好像又是顯而易見(jiàn)的憾朴, 延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束時(shí)才執(zhí)行。 事實(shí)上喷鸽,當(dāng)定時(shí)器運(yùn)行時(shí)即使每個(gè)迭代中執(zhí)行的是setTimeout(.., 0)众雷,所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才會(huì)被執(zhí)行,因此會(huì)每次輸出一個(gè)6出來(lái)。
所以报腔,可以把上面的代碼株搔,轉(zhuǎn)換成下面的形式:
// 這里的 i 的作用域是在全局,不是預(yù)期的只有循環(huán)才能訪問(wèn)的
for (var i = 1; i <= 5; i ++) {}
setTimeout(function () {
console.log(i)
}, 1*1000)
setTimeout(function () {
console.log(i)
}, 2*1000)
setTimeout(function () {
console.log(i)
}, 3*1000)
setTimeout(function () {
console.log(i)
}, 4*1000)
setTimeout(function () {
console.log(i)
}, 5*1000)
/**
* 這里的循環(huán)是同步的纯蛾,而定時(shí)器里面的方法是異步調(diào)用的
* 當(dāng)回調(diào)方法執(zhí)行的時(shí)候纤房,i 已經(jīng)變成 6 了
*/
1、使用IIFE函數(shù)改造使其符合預(yù)期
for (var i = 1; i <= 5; i ++) {
(function (j) {
setTimeout(function () {
console.log(j)
}, j*1000)
}(i))
}
/**
* 這里通過(guò)立即執(zhí)行函數(shù)給每次迭代都生成了一個(gè)新的作用域
* 通過(guò)內(nèi)部聲明的變量j翻诉,把外部的i通過(guò)j傳入內(nèi)部作用域
*/
2炮姨、通過(guò)塊級(jí)作用域使其符合預(yù)期
for (let i = 1; i <= 5; i ++) {
setTimeout(function () {
console.log(i)
}, i*1000)
}
此時(shí),變量i在循環(huán)過(guò)程中不止被聲明一次碰煌,每次迭代都會(huì)聲明舒岸。 隨后的每個(gè)迭代都會(huì)使用上一個(gè)迭代結(jié)束時(shí)的值來(lái)初始化這個(gè)變量。
一句話總結(jié)閉包:當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域芦圾, 即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行蛾派, 這時(shí)就產(chǎn)生了閉包。