JavaScript變量or循環中的var和let詳解
在for循環中使用var聲明初始化帶來的問題
// 一道經典面試題: var funcs = []; for (var i = 0; i < 3; i++) { funcs[i] = function() { console.log("My value: " + i) }; } for (var j = 0; j < 3; j++) { funcs[j](); } /* 輸出結果: > My value: 3 > My value: 3 > My value: 3 */
會出現這種現象的原因就是:
- var聲明的作用域是函數作用域而不是塊級作用域,因此在for循環的循環體之外依然能訪問到在初始化for循環時定義的var變量。
- 且在循環結束後訪問時,訪問到的var變量是已經完成循環後的值。
解決方法
使用閉包
ES5時代的解決辦法就是通過IIFE創建一個閉包,把變量在函數體內保存起來,再執行函數時就不會去訪問外層的var變量瞭。
var funcs = []; for (var i = 0; i < 3; i++) { // 1. 閉包 funcs[i] = (function (i) { return function () { console.log("My value: " + i); }; })(i); } for (var j = 0; j < 3; j++) { funcs[j](); }
使用let變量初始化
let聲明是塊級作用域,循環體內的變量不會泄露到塊語句之外。
因此在循環結束後再去訪問變量i時,沒有外層作用域變量的幹擾,訪問到的自然就是函數體內保存下來的變量值。
var funcs = []; // 2. let for (let i = 0; i < 3; i++) { funcs[i] = function() { console.log("My value: " + i); }; } for (var j = 0; j < 3; j++) { funcs[j](); }
從這裡也可以看出,使用var來初始化for循環本身就是違反直覺的。
用來初始化for循環的變量理應是for循環的局部變量,在循環結束以後這個變量就應該沒有意義瞭才對。
但是如果使用var來初始化,由於var聲明的變量的作用域是函數作用域,這個初始化變量就和for循環處於同一作用域瞭,不受for循環的限制。
本應是for循環的局部變量,卻暴露在瞭和for循環同層的作用域,且變量值已經被循環次數改變,自然會影響循環結束後其他代碼對該變量的訪問。
而如果使用let來初始化for循環,就不會有這個困擾瞭,因為let聲明的作用域是塊級作用域,這個初始化變量會如願成為for循環的局部變量。
for循環怎麼處理用let和var聲明的初始化變量?
先上結論:
- 用var初始化時,for循環會直接使用創建的var初始化變量;
- 用let初始化時,圓括號會自成一個作用域,for循環會將圓括號內的變量值往循環體內傳遞。
首先看第一個結論,規范是這麼說的:
可以看到,規范對於var初始化變量沒有什麼特別的處理,直接就拿來用瞭。 此時這個變量就是個普通的var變量,和for循環處於同一作用域。
我們用代碼來佐證一下:
var funcs = []; for (var i = 0; i < 3; i++) { // !!!重復聲明瞭一個同名的var變量 var i = 5; console.log("My value: " + i); } /* 隻會輸出一次: > My value: 5 */
var可以重復聲明且值會覆蓋,因此在循環體內再聲明一個var i = 5
,循環變量被作沒瞭,會直接跳出for循環。
var funcs = []; for (var i = 0; i < 3; i++) { // 用let聲明瞭一個和循環變量同名的變量 let i = 5; console.log("My value: " + i); } /* 一共輸出瞭3次: > My value: 5 > My value: 5 > My value: 5 */
初始化var變量在函數作用域,循環體內的let變量在塊作用域,循環體內優先訪問塊作用域裡的let變量,因此循環體內的i值會被覆蓋。
又由於var變量實際上處於let變量的外層作用域,因此let變量沒有重復聲明,不會報錯;var變量也會如期完成自己作為循環變量的使命。
再看第二個結論,同樣是先看規范:
很明顯可以發現,使用let來初始化會比使用var多瞭一個叫perIterationLets
的東西。
perIterationLets
是什麼?
從規范上可以看到,perIterationLets
來源於LexicalDeclaration(詞法聲明)
裡的boundNames
。
而這個LexicalDeclaration(詞法聲明)
,其實就是我們用來初始化的let聲明。
可以理解為,如果我們用let聲明來初始化for循環,for循環內部不會像直接使用var變量一樣來直接使用let變量,而是會先把let變量收集起來,以某種形式轉換為perIterationLets
,再傳遞給循環體。
perIterationLets
是被用來做什麼的?
從規范上可以看到,我們的let變量以perIterationLets
的身份,作為參數被傳進瞭ForBodyEvaluation
,也就是循環體裡。
在循環體裡,perIterationLets
隻做瞭一件事情,那就是作為CreatePerIterationEnvironment
的參數:
從字面上理解,CreatePerIterationEnvironment
意思就是每次循環都要創建的環境。
要註意,這個環境不是{...}
裡的那些執行語句所處的環境。 {...}
裡的執行語句是statement
,在規范裡可以看到,stmt
有自己的事情要做。
這個環境是屬於圓括號的作用域,也就是我們定義的let初始化變量所在的作用域。
再看看每次循環都要創建的環境被用來幹嘛瞭:
逐步分析一下方法:CreatePerIterationEnvironment
這個
- 首先,把當前執行上下文的詞法環境保存下來,作為
lastIterationEnv(上一次循環時的環境)
; - 創建一個和
lastIterationEnv
同級的新作用域,作為thisIterationEnv(本次循環的環境)
; - 遍歷我們定義的let初始化變量,也就是
perIterationLets
,在thisIterationEnv(本次循環的環境)
裡創建一個同名的可變綁定,找到它們在lastIterationEnv(上一次循環時的環境)
裡的終值,作為這個同名綁定的初始值; - 最後,將
thisIterationEnv(本次循環的環境)
交還給執行上下文。
簡而言之就是,for循環會在迭代之前創建一個和初始化變量同名的變量,並使用之前迭代的終值將這個變量初始化以後,再交還給執行上下文。
用偽代碼理解一下這個過程就是:
到這裡又有一個問題,既然把圓括號內的變量向循環體裡傳遞瞭,那如果在循環體裡又重復聲明瞭一個同名變量,算不算重復聲明,會不會報錯?
答案是不會。
因為CreatePerIterationEnvironment
在執行時,在新環境裡創建的是一個可變的綁定,因此如果在循環體內重復聲明一個名字為i
的變量,隻是會影響循環體內執行語句對i
值的訪問。
var funcs = []; for (let i = 0; i < 3; i++) { // !!!用let聲明瞭一個和循環變量同名的變量 let i = 5; console.log("My value: " + i); } /* 一共輸出瞭3次: > My value: 5 > My value: 5 > My value: 5 */
總結
在for循環中使用var聲明來初始化的話,循環變量會暴露在和for循環同一作用域下,導致循環結束後還能訪問到循環變量,且訪問到的變量值是經過循環迭代後的值。
解決這個問題的方法如下:
- 使用閉包將循環變量的值作為局部變量保存起來;
- 使用ES6的let聲明,將循環變量的作用域限制在for循環內部,初始化變量始終是for循環的局部變量,不能在外界被訪問到。
for循環是怎麼處理用let和var聲明的初始化變量的?
- 用var初始化時,for循環會直接使用創建的var初始化變量;
- 用let初始化時,圓括號會自成一個作用域,for循環會將圓括號內的變量值往循環體內傳遞。
到此這篇關於JavaScript變量or循環中的var和let詳解的文章就介紹到這瞭,更多相關JS var和let內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- JavaScript進階知識點作用域詳解
- JavaScript Reduce使用詳解
- JS中switch的四種寫法示例
- JavaScript三大重點同步異步與作用域和閉包及原型和原型鏈詳解
- 前端 JavaScript運行原理