
英文 | https://betterprogramming.pub/10-javascript-closure-challenges-explained-with-diagrams-c964110805e7
翻譯 | 楊小愛
閉包是函數式程式設計中的核心概念之一,是每個 JavaScript 開發人員必備的知識。在這裡,我準備了 10 個關于閉包的面試挑戰題,這些基本都是面試中經常被問到的。
你準備好了嗎?我們現在要開始了。
每個題目都有一個代碼片段,你需要說出這段代碼的輸出是什麼。
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 的值是通過閉包儲存的。
恭喜你,到這裡,你已經學會了這些面試挑戰題。
希望在開發面試中,閉包相關的問題不會再困擾你了。
最後,感謝你的閱讀,如果你覺得有用的話,請點贊我,關注我,并将其分享給你的身邊做開發的朋友,也許能夠幫助到他。