你不知道的JavaScript之變量聲明提升和閉包

聲明提升和閉包

提升

JavaScript 是一門解釋型語言,原則上是不需要編譯的。但是它在代碼執(zhí)行之前會有一個編譯的流程,這個流程發(fā)生在代碼執(zhí)行的前一刻。

示例:

console.log(a)    // undefined
var a = 2
console.log(a)    // 2

在這段代碼中,按照常規(guī)邏輯,引擎通過作用域沒有找到,可能會拋出一個錯誤 ReferenceError: a is not defined,但是這種情況并沒有發(fā)生。
實際上,在代碼運行的前一刻,會執(zhí)行預(yù)編譯操作。當(dāng)遇到 var a = 2 這樣的語句時,會被拆分成兩部分,var a(變量聲明)+ a = 2(變量賦值)。其中,變量聲明會發(fā)生在預(yù)編譯階段,把 a 這個變量放到全局作用域中,沒有賦值,則為 undefined。
當(dāng)執(zhí)行第一個打印操作的時候,已經(jīng)完成了預(yù)編譯相關(guān)的處理,所以可以訪問a,得到undefined;然后開始執(zhí)行后面的賦值操作;等到第二個打印操作的時候已經(jīng)可以正常訪問 a 的值了。所以上述代碼也可以像下面這樣理解:

var a
console.log(a)
a = 2
console.log(2)

變量聲明提升,函數(shù)聲明整體提升

foo()    // foo
bar()    // TypeError: bar is not a function(此時bar為undefined)
function foo() {
  console.log(foo.name)
}
var bar = function () {
  console.log(bar.name)
}

變量聲明提升,var 聲明的變量在預(yù)編譯的時候會被提升到當(dāng)前執(zhí)行環(huán)境的頂部;
函數(shù)聲明整體提升,以函數(shù)聲明聲明函數(shù)的函數(shù),會被整體提升到當(dāng)前執(zhí)行環(huán)境的頂部,所以執(zhí)行語句可以寫在函數(shù)聲明前面。函數(shù)表達式不可以,因為函數(shù)表達式走的是變量聲明提升的規(guī)則。

同一個變量既賦值給了變量,又作為函數(shù)聲明的標(biāo)識符

console.log(foo)    // function
var foo = 1
console.log(foo)    // 1
function foo() {
  console.log(foo.name)
}
console.log(foo)
/**
 * 這里常規(guī)思路是 function,其實還是 1
 * 因為函數(shù)聲明這段代碼已經(jīng)被整體提升到了當(dāng)前執(zhí)行環(huán)境的頂部,已經(jīng)在前面執(zhí)行過了
 */ 

閉包

一個閉包的基本示例:

function foo() {
  let a = 3
  let tempFunc = function () {
    return a
  }
  return tempFunc
}
// 通過 bar 標(biāo)識符引用了 foo 內(nèi)部的函數(shù) tempFunc
let bar = foo()
console.log(bar())

在這里,函數(shù) bar 的詞法作用域能夠訪問 foo 的內(nèi)部作用域,這讓foo的內(nèi)部函數(shù)能夠在自己的詞法作用域外執(zhí)行,但是依然能夠訪問自身詞法作用域的變量。

在foo()執(zhí)行后, 通常會期待foo()的整個內(nèi)部作用域都被銷毀, 因為我們知道引擎有垃圾回收器用來釋放不再使用的內(nèi)存空間。 由于看上去foo()的內(nèi)容不會再被使用, 所以很自然地會考慮對其進行回收。
而閉包的“ 神奇”之處正是可以阻止這件事情的發(fā)生。 事實上內(nèi)部作用域依然存在, 因此沒有被回收。誰在使用這個內(nèi)部作用域?原來是bar()本身在使用。
拜bar()所聲明的位置所賜, 它擁有涵蓋foo()內(nèi)部作用域的閉包, 使得該作用域能夠一直存活,以供bar()在之后任何時間進行引用。
bar()依然持有對該作用域的引用,而這個引用就叫作閉包。
這個函數(shù)在定義時的詞法作用域以外的地方被調(diào)用。 閉包使得函數(shù)可以繼續(xù)訪問定義時的詞法作用域。

基本示例的變種:

let bar
function foo() {
  let a = 2
  let tempFunc = function () {
    return a
  }
  bar = tempFunc
}
foo()
console.log(bar())

無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外, 它都會持有對原始定義作用域的引用,無論在何處執(zhí)行這個函數(shù)都會使用閉包。

循環(huán)中的閉包

常見考題:

for (var i = 1; i <= 5; i ++) {
  setTimeout(function () {
    console.log(i)
  }, i*1000)
}
// 輸出什么?

正常情況下,我們對這段代碼行為的預(yù)期是分別輸出數(shù)字1~5,每秒一次,每次一個。
但實際上,這段代碼在運行時會以每秒一次的頻率輸出五次6。
首先解釋6是從哪里來的。 這個循環(huán)的終止條件是i不再<=5。條件首次成立時i的值是6。因此,輸出顯示的是循環(huán)結(jié)束時i的最終值。
仔細想一下, 這好像又是顯而易見的, 延遲函數(shù)的回調(diào)會在循環(huán)結(jié)束時才執(zhí)行。 事實上,當(dāng)定時器運行時即使每個迭代中執(zhí)行的是setTimeout(.., 0),所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才會被執(zhí)行,因此會每次輸出一個6出來。

所以,可以把上面的代碼,轉(zhuǎn)換成下面的形式:

// 這里的 i 的作用域是在全局,不是預(yù)期的只有循環(huá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)是同步的,而定時器里面的方法是異步調(diào)用的
 * 當(dāng)回調(diào)方法執(zhí)行的時候,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))
}
/**
 * 這里通過立即執(zhí)行函數(shù)給每次迭代都生成了一個新的作用域
 * 通過內(nèi)部聲明的變量j,把外部的i通過j傳入內(nèi)部作用域
 */

2、通過塊級作用域使其符合預(yù)期

for (let i = 1; i <= 5; i ++) {
  setTimeout(function () {
    console.log(i)
  }, i*1000)
}

此時,變量i在循環(huán)過程中不止被聲明一次,每次迭代都會聲明。 隨后的每個迭代都會使用上一個迭代結(jié)束時的值來初始化這個變量。

一句話總結(jié)閉包:當(dāng)函數(shù)可以記住并訪問所在的詞法作用域, 即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行, 這時就產(chǎn)生了閉包。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容