1 從一個尷尬的例子說起
<ul class="wrap">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var addEvent = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function () {
console.log(i)
}
}
}
var wrap = document.querySelectorAll('.wrap > li')
addEvent(wrap)
</script>
複製程式碼
- 結果如大家所想,點選每個
li
,都會列印3
addEvent
函式的本意,是想傳遞給每個事件處理函式:一個唯一的 i 。
而沒有按照預期的原因是:事件處理函式繫結的是變數 i 本身,而不是函式在構造時的變數 i 的值。
2 解決
2.1 最常規的解決
利用的是物件屬性不變性。
var addEvent = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].index = i
nodes[i].onclick = function () {
console.log(this.index)
}
}
}
複製程式碼
2.2 es6
利用的是
let
宣告的變數,僅在塊級作用域有效。
i 只在本輪迴圈有效。所以每次迴圈的 i 都是一個新的變數。JavaScript引擎內部會記住上一輪迴圈的值,作為初始化本輪變數 i 的基礎。
而對於
var
,因為宣告的變數是全域性的,也就是說,使用的是同一個變數。最終在列印輸出時,使用的就是全域性變數 i ,所以點選時,都是 3。
let addEvent = function (nodes) {
for (let i = 0; i < nodes.length; i++) {
nodes[i].onclick = function () {
console.log(i)
}
}
}
複製程式碼
2.3 閉包(終於到了。。。)
如下所示,事件處理函式中,又返回了一個輔助函式。
- 因為閉包的特性,
- 該輔助函式,可以訪問他被建立時所處的上下文環境(可以簡單理解為:外部函式作用域。因為函式中宣告的變數或是傳遞給函式的變數,是存放在執行上下文中,而執行上下文又掛靠在自己的作用域中。)
- 該輔助函式,訪問的是外部函式中的實際變數,而不是複製後的值。
所以,每次傳遞給
handlers
函式的引數 i 都會放到handlers
的執行上下文中, 而每次呼叫函式,都會建立一個執行上下文物件,因為,互不干擾。
var addEvent = function (nodes) {
var handlers = function (i) {
return function () {
console.log(i)
}
}
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = handlers(i)
}
}
複製程式碼
3 其他問題
3.1 關於for
迴圈,使用let
和var
還有區別:
let
,設定迴圈變數的那部分,是父級作用域;而迴圈體內部是單獨的子作用域
如下所示,會連續輸出3個 a 。首先
let
是不能重複宣告的,但該例中並沒有報錯。因為第一次迴圈結束,i++時,i 依舊是 0
for (let i = 0; i < 3; i++) {
let i = 'a'
console.log(i)
}
// a
// a
// a
複製程式碼
var
,因為通過var
宣告的是全域性變數,並且可以重複宣告,所以當 i++ 時,變為了 a++, 則第二次迴圈時,i 為 NaN,就結束了迴圈。
for (var i = 0; i < 3; i++) {
var i = 'a'
console.log(i)
}
// a
複製程式碼
3.2 建立函式的問題
- 要避免在迴圈中建立函式。
回來最開始的地方,
為了給每個節點都繫結了事件處理函式,在迴圈中進行繫結。
這樣,每次都會建立一個新的匿名函式,帶來的只有無味的計算,還容易引起混淆,正如這個例子所示。
var addEvent = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function () {
console.log(i)
}
}
}
複製程式碼