十個用圖表解釋JavaScript 閉包的面試題

語言: CN / TW / HK

你準備好了嗎?我們現在要開始了。

每個題目都有一個程式碼片段,你需要說出這段程式碼的輸出是什麼。

1、範圍

在說閉包之前,我們必須瞭解作用域的概念,它是理解閉包的基石。

此程式碼段的輸出是什麼?

var a = 10
function foo(){
   console.log(a)
}
foo()

這很簡單,相信所有人都知道輸出結果是10。

  • 預設情況下,有一個全域性範圍。
  • 本地作用域由函式或程式碼塊建立。

當執行 console.log(a) 時,JavaScript 引擎將首先在函式 foo 建立的本地範圍內查詢 a。當 JavaScript 引擎找不到 a 時,它會嘗試在其外部作用域(即全域性作用域)中查詢 a。然後事實證明a的值為10。

2、 區域性作用域

var a = 10
function foo(){
   var a = 20
   console.log(a)
}
a = 30
foo()

在這段程式碼中,變數 a 也存在於 foo 的範圍內。所以當執行 console.log(a) 時,JavaScript 引擎可以直接從本地作用域獲取 a 的值。

所以輸出是 20 。

記住:當 JavaScript 引擎需要查詢一個變數的值時,它會首先在本地範圍內查詢,如果沒有找到該變數,它會繼續在上層範圍內查詢。

3、詞法作用域

var a = 10
function foo(){
   console.log(a)
}
function bar() {
   var a = 20
   foo()
}
bar()

這個問題容易出錯,也是面試中經常出現的問題,你可以考慮一下。

簡單地說,JavaScript 實現了一種名為詞法作用域(或靜態作用域)的作用域機制。它被稱為詞法(或靜態),因為引擎僅通過檢視 JavaScript 原始碼來確定範圍的巢狀,無論它在哪裡呼叫。

所以輸出是 10 :

4、修改詞法作用域

如果我們將程式碼片段更改為:

var a = 10
function bar() {
 var a = 20
 function foo(){
   console.log(a)
 }
 foo()
}
bar()

輸出是什麼?

foo 範圍成為 bar 範圍的子範圍:

當 JavaScript 引擎在 Foo 作用域中沒有找到 a 時,它會首先從 Foo 作用域的父作用域,也就是 Bar 作用域中尋找 a,它確實找到了 a。

所以輸出是 20:

好了,以上就是關於範圍的一些基本挑戰,相信你能順利通過。現在我們開始進入閉包的部分。

5、 閉包

function outerFunc() {
 let a = 10;
 function innerFunc() {
   console.log(a);
 }
 return innerFunc;
}
let innerFunc = outerFunc();
innerFunc()

輸出是什麼?這段程式碼會丟擲異常嗎?

在詞法範圍內,innerFunc 仍然可以訪問 a,即使在其詞法範圍之外執行。

換句話說,innerFunc 從其詞法範圍中記住(或關閉)變數 a。

換句話說,innerFunc 是一個閉包,因為它在變數 a 的詞法範圍內關閉。

因此,這段程式碼不會丟擲異常,而是輸出 10。

6、 IIFE

(function(a) {
 return (function(b) {
   console.log(a);
 })(1);
})(0);

此程式碼片段使用 JavaScript 立即呼叫函式表示式 (IIFE)。

我們可以簡單地將這段程式碼翻譯成這樣:

function foo(a){
 function bar(b){
   console.log(a)
 }
 return bar(1)
}
foo(0)

所以輸出是 0 。

閉包的一個經典應用是隱藏變數。

比如現在要寫一個計數器,基本的寫法是這樣的:

let i = 0
function increase(){
 i++
 console.log(`courrent counter is ${i}`)
 return i
}
increase()
increase()
increase()

可以這樣寫,但是在全域性範圍內會多出一個變數i,這樣就不好了。

這時候,我們可以使用閉包來隱藏這個變數。

let increase = (function(){
 let i = 0
 return function(){
   i++
   console.log(`courrent counter is ${i}`)
   return i
 }
})()
increase()
increase()
increase()

這樣,變數 i 就隱藏在區域性範圍內,不會汙染全域性環境。

7、多重宣告和使用

let count = 0;
(function() {
 if (count === 0) {
   let count = 1;
   console.log(count);
 }
 console.log(count);
})();

在這個程式碼片段中,有兩個 count 的宣告和三個 count 的用法。這是一個難題,你應該仔細考慮。

首先,我們要知道if程式碼塊也建立了一個區域性作用域,上面的作用域大致是這樣的。

  • Function Scope 沒有宣告自己的計數,所以我們在這個作用域中使用的計數是全域性作用域的計數。
  • If Scope 聲明瞭自己的計數,所以我們在這個作用域中使用的計數就是當前作用域的計數。

或在此圖中:

所以輸出是 1 , 0 :

8、呼叫多個閉包

function cr
eateCounter(){
let i = 0
return function(){
   i++
return i
 }
}
let increase1 = createCounter()
let increase2 = createCounter()
console.log(increase1())
console.log(increase1())
console.log(increase2())
console.log(increase2())

這裡需要注意的是,increase1和increase2是通過不同的函式呼叫createCounter建立的,它們不共享記憶體,它們的i是獨立的,不同的。

所以輸出是 1 , 2 , 1 , 2 。

9、返回函式

function createCounter() {
 let count = 0;
  function increase() {  
   count++;
 }
 let message = `Count is ${count}`;
 function log() {
   console.log(message);
 }
 return [increase, log];
}
const [increase, log] = createCounter();
increase();  
increase();  
increase();  
log();

這段程式碼很容易理解,但是有個陷阱:message其實是一個靜態字串,它的值固定為Count為0,當我們呼叫increase或者log時不會改變。

所以每次呼叫 log 函式,輸出結果總是 Count is 0 。

如果您希望 log 函式及時檢查 count 的值,請將 message 移入 log :

function createCounter() {
 let count = 0;
  function increase() {  
   count++;
 }
-  let message = `Count is ${count}`;
 function log() {
+  let message = `Count is ${count}`;
   console.log(message);
 }
 return [increase, log];
}
const [increase, log] = createCounter();
increase();  
increase();  
increase();  
log();

10、非同步閉包

for (var i = 0; i < 5; i++) {
 setTimeout(function () {
   console.log(i);
 }, 0)
}

輸出是什麼?

上面的程式碼等價於:

var i = 0;
setTimeout(function(){
 console.log(i);
},0)
i = 1;
setTimeout(function(){
 console.log(i);
},0)
i = 2;
setTimeout(function(){
 console.log(i);
},0)
i = 3;
setTimeout(function(){
 console.log(i);
},0)
i = 4;
setTimeout(function(){
 console.log(i);
},0)
i = 5

而且我們知道JavaScript會先執行同步程式碼,然後再執行非同步程式碼。所以每次執行console.log(i)時,i的值已經變成了5。

所以輸出是 5 , 5 , 5 , 5 , 5 。

如果我們想要程式碼輸出 0 , 1 , 2 , 3 , 4 ,需要怎麼操作?

使用閉包的解決方案是:

for ( var i = 0 ; i < 5 ; ++i ) {
 (function(cacheI){
     setTimeout(function(){
         console.log(cacheI);
     },0)
 })(i)
}  ;

上面的程式碼等價於:

var i = 0;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
i = 1;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
i = 2;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
i = 3;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)
i = 4;
(function(cacheI){setTimeout(function(){
 console.log(cacheI);
},0)})(i)

我們通過 JavaScript 立即呼叫的函式表示式建立函式範圍。i 的值是通過閉包儲存的。

恭喜你,到這裡,你已經學會了這些面試挑戰題。

希望在開發面試中,閉包相關的問題不會再困擾你了。

最後,感謝你的閱讀,如果你覺得有用的話,請點讚我,關注我,並將其分享給你的身邊做開發的朋友,也許能夠幫助到他。