JS 經典考題 1

前言

前兩篇的 JS 概念篇,分別整理了 scope 和 closure
這篇就來整理一個 javascript 面試常見的題目

經典問題

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

// 5,5,5,5,5

第一個重點是
setTimeout 會將 console.log 函式放在暫存區
for loop 結束後一共會放五個 console.log 在那邊

第二個重點是 var
在上一篇中我們提到 var 變數的生存範圍是 function
所以i的 scope 就會在 for loop 之外
所以我們可以理解成 for loop 的每個循環
都會共用同一個i (指reference)

閉包會參照環境變數值,而不是複製其值

而最後當程式要來處理暫存區的任務時
由於 local variable 中沒有i
便往上找 function variable i
而 loop 也已經結束,i已經從 0 變成 5 了
可想而知就會印出五次 5

解法1: let

這題有很多解法
下面這個應該是最簡單的了吧

1
2
3
4
5
6
7
for (let i = 0; i < 5; i++) { // 把 var 換成 let
setTimeout(function() {
console.log(i)
}, 0)
}

// 0,1,2,3,4

當我們改用 let 時
i的生存範圍便限制在每個 loop block 當中
有幾個迴圈,i就會產生幾次
setTimeout 在 block 中找得到i,便會記住每個 loop 的i
而不會像原本一樣由於在區域變數中找不到
就往上層找函數變數
這是 ES6 之後的新解法
利用 let 生存範圍的特性

解法2: 閉包

我們可以利用閉包的特性鎖住每個i

1
2
3
4
5
6
7
8
9
10
for (var i = 0; i < 5; i++) {
const timer = (n) => { // 記得定義 parameter n
setTimeout(function() {
console.log(n)
}, 0)
}
timer(i) // 記得傳入 i
}

// 0,1,2,3,4

每次運行timer(i)時會產生一個閉包
同時每次都會產生新的參數n
用來儲存當下傳入的i
所以會得到正確的結果

解法3: IIFE

Immediately-Involked Function Expression (IIFE)
立即呼叫函式表達式
定義函式之後立刻呼叫的寫法
它具有儲存當下環境的特性
是形成閉包的作法之一
以非常白話來解釋:

讓函式立刻運行,而且不用取名

1
2
3
4
5
6
7
8
9
for (var i = 0; i < 5; i++) {
(function(value) {
setTimeout(function() {
console.log(value)
}, 0)
})(i)
}

// 0,1,2,3,4

追根究柢,這其實跟方法二幾乎一樣
只是寫法不同而已

結論

這個題目 tricky 的部分在於
原本應該要利用閉包的特性鎖住每個i
卻又因為閉包只會參照位置
而導致非同步函式事後取到錯誤的值

本篇所用的三個解法的中心思想是

不要參照外部變數

只要不要參照外部變數,讓每次迴圈都產生一個變數來用
便能避免事後才取到改變後的變數

老實說到現在還沒辦法肯定的說我清楚了
但藉由思考這個題目,對於 scope, closure 肯定會比以前更有概念
也更了解 javascript 的運作方式