JavaScript 是壹種非常面向函數的語言。它給了我們很大的自由度。在 JavaScript 中,我們可以隨時創建函數,可以將函數作爲參數傳遞給另壹個函數,並在完全不同的代碼位置進行調用。
我們已經知道函數可以訪問其外部的變量。
但是,如果在函數被創建之後,外部變量發生了變化會怎樣?函數會獲得新值還是舊值?
如果將函數作爲參數(argument)傳遞並在代碼中的另壹個位置調用它,該函數將訪問的是新位置的外部變量嗎?
讓我們通過本文來學習這些相關知識,以了解在這些場景以及更複雜的場景下到底會發生什麽。
我們將在這探討壹下 let/const
在 JavaScript 中,有三種聲明變量的方式:let
,const
(現代方式),var
(過去留下來的方式)。
在本文的示例中,我們將使用 let
聲明變量。
用 const
聲明的變量的行爲也相同(譯注:與 let
在作用域等特性上是相同的),因此,本文也涉及用 const
進行變量聲明。
舊的 var
與上面兩個有著明顯的區別,我們將在 老舊的 "var" 中詳細介紹。
如果在代碼塊 {...}
內聲明了壹個變量,那麽這個變量只在該代碼塊內可見。
例如:
{ // 使用在代碼塊外不可見的局部變量做壹些工作 let message = "Hello"; // 只在此代碼塊內可見 alert(message); // Hello } alert(message); // Error: message is not defined
我們可以使用它來隔離壹段代碼,該段代碼執行自己的任務,並使用僅屬于自己的變量:
{ // 顯示 message let message = "Hello"; alert(message); } { // 顯示另壹個 message let message = "Goodbye"; alert(message); }
這裏如果沒有代碼塊則會報錯
請注意,如果我們使用 let
對已存在的變量進行重複聲明,如果對應的變量沒有單獨的代碼塊,則會出現錯誤:
// 顯示 message let message = "Hello"; alert(message); // 顯示另壹個 message let message = "Goodbye"; // Error: variable already declared alert(message);
對于 if
,for
和 while
等,在 {...}
中聲明的變量也僅在內部可見:
if (true) { let phrase = "Hello!"; alert(phrase); // Hello! } alert(phrase); // Error, no such variable!
在這兒,當 if
執行完畢,則下面的 alert
將看不到 phrase
,因此會出現錯誤。(譯注:就算下面的 alert
想在 if
沒執行完成時去取 phrase
(雖然這種情況不可能發生)也是取不到的,因爲 let
聲明的變量在代碼塊外不可見。)
太好了,因爲這就允許我們創建特定于 if
分支的塊級局部變量。
對于 for
和 while
循環也是如此:
for (let i = 0; i < 3; i++) { // 變量 i 僅在這個 for 循環的內部可見 alert(i); // 0,然後是 1,然後是 2 } alert(i); // Error, no such variable
從視覺上看,let i
位于 {...}
之外。但是 for
構造在這裏很特殊:在其中聲明的變量被視爲塊的壹部分。
如果壹個函數是在另壹個函數中創建的,該函數就被稱爲“嵌套”函數。
在 JavaScript 中很容易實現這壹點。
我們可以使用嵌套來組織代碼,比如這樣:
function sayHiBye(firstName, lastName) { // 輔助嵌套函數使用如下 function getFullName() { return firstName + " " + lastName; } alert( "Hello, " + getFullName() ); alert( "Bye, " + getFullName() ); }
這裏創建的 嵌套 函數 getFullName()
是爲了更加方便。它可以訪問外部變量,因此可以返回全名。嵌套函數在 JavaScript 中很常見。
更有意思的是,可以返回壹個嵌套函數:作爲壹個新對象的屬性或作爲結果返回。之後可以在其他地方使用。不論在哪裏調用,它仍然可以訪問相同的外部變量。
下面的 makeCounter
創建了壹個 “counter” 函數,該函數在每次調用時返回下壹個數字:
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2
盡管很簡單,但稍加變型就具有很強的實際用途,比如,用作 隨機數生成器 以生成用于自動化測試的隨機數值。
這是如何運作的呢?如果我們創建多個計數器,它們會是獨立的嗎?這裏的變量是怎麽回事?
理解這些內容對于掌握 JavaScript 的整體知識很有幫助,並且對于應對更複雜的場景也很有益處。因此,讓我們繼續深入探究。
前方高能!
壹大波深入的技術講解即將到來。
盡管我很想避免編程語言的壹些底層細節,但是如果沒有它們,我們就無法完整地理解詞法作用域,所以我們這就開始吧!
爲了使內容更清晰,這裏將分步驟進行講解。
在 JavaScript 中,每個運行的函數,代碼塊 {...}
以及整個腳本,都有壹個被稱爲 詞法環境(Lexical Environment) 的內部(隱藏)的關聯對象。
詞法環境對象由兩部分組成:
環境記錄(Environment Record) —— 壹個存儲所有局部變量作爲其屬性(包括壹些其他信息,例如 this
的值)的對象。
對 外部詞法環境 的引用,與外部代碼相關聯。
壹個“變量”只是 環境記錄 這個特殊的內部對象的壹個屬性。“獲取或修改變量”意味著“獲取或修改詞法環境的壹個屬性”。
舉個例子,這段沒有函數的簡單的代碼中只有壹個詞法環境:
這就是所謂的與整個腳本相關聯的 全局 詞法環境。
在上面的圖片中,矩形表示環境記錄(變量存儲),箭頭表示外部引用。全局詞法環境沒有外部引用,所以箭頭指向了 null
。
隨著代碼開始並繼續運行,詞法環境發生了變化。
這是更長的代碼:
右側的矩形演示了執行過程中全局詞法環境的變化:
當腳本開始運行,詞法環境預先填充了所有聲明的變量。
最初,它們處于“未初始化(Uninitialized)”狀態。這是壹種特殊的內部狀態,這意味著引擎知道變量,但是在用 let
聲明前,不能引用它。幾乎就像變量不存在壹樣。
然後 let phrase
定義出現了。它尚未被賦值,因此它的值爲 undefined
。從這壹刻起,我們就可以使用變量了。
phrase
被賦予了壹個值。
phrase
的值被修改。
現在看起來都挺簡單的,是吧?
變量是特殊內部對象的屬性,與當前正在執行的(代碼)塊/函數/腳本有關。
操作變量實際上是操作該對象的屬性。
詞法環境是壹個規範對象
“詞法環境”是壹個規範對象(specification object):它只存在于 語言規範 的“理論”層面,用于描述事物是如何工作的。我們無法在代碼中獲取該對象並直接對其進行操作。
但 JavaScript 引擎同樣可以優化它,比如清除未被使用的變量以節省內存和執行其他內部技巧等,但顯性行爲應該是和上述的無差。
壹個函數其實也是壹個值,就像變量壹樣。
不同之處在于函數聲明的初始化會被立即完成。
當創建了壹個詞法環境(Lexical Environment)時,函數聲明會立即變爲即用型函數(不像 let
那樣直到聲明處才可用)。
這就是爲什麽我們甚至可以在聲明自身之前調用壹個以函數聲明(Function Declaration)的方式聲明的函數。
例如,這是添加壹個函數時全局詞法環境的初始狀態:
正常來說,這種行爲僅適用于函數聲明,而不適用于我們將函數分配給變量的函數表達式,例如 let say = function(name)...
。
在壹個函數運行時,在調用剛開始時,會自動創建壹個新的詞法環境以存儲這個調用的局部變量和參數。
例如,對于 say("John")
,它看起來像這樣(當前執行位置在箭頭標記的那壹行上):
在這個函數調用期間,我們有兩個詞法環境:內部壹個(用于函數調用)和外部壹個(全局):
內部詞法環境與 say
的當前執行相對應。它具有壹個單獨的屬性:name
,函數的參數。我們調用的是 say("John")
,所以 name
的值爲 "John"
。
外部詞法環境是全局詞法環境。它具有 phrase
變量和函數本身。
內部詞法環境引用了 outer
。
當代碼要訪問壹個變量時 —— 首先會搜索內部詞法環境,然後搜索外部環境,然後搜索更外部的環境,以此類推,直到全局詞法環境。
如果在任何地方都找不到這個變量,那麽在嚴格模式下就會報錯(在非嚴格模式下,爲了向下兼容,給未定義的變量賦值會創建壹個全局變量)。
在這個示例中,搜索過程如下:
對于 name
變量,當 say
中的 alert
試圖訪問 name
時,會立即在內部詞法環境中找到它。
當它試圖訪問 phrase
時,然而內部沒有 phrase
,所以它順著對外部詞法環境的引用找到了它。
讓我們回到 makeCounter
這個例子。
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter();
在每次 makeCounter()
調用的開始,都會創建壹個新的詞法環境對象,以存儲該 makeCounter
運行時的變量。
因此,我們有兩層嵌套的詞法環境,就像上面的示例壹樣:
不同的是,在執行 makeCounter()
的過程中創建了壹個僅占壹行的嵌套函數:return count++
。我們尚未運行它,僅創建了它。
所有的函數在“誕生”時都會記住創建它們的詞法環境。從技術上講,這裏沒有什麽魔法:所有函數都有名爲 [[Environment]]
的隱藏屬性,該屬性保存了對創建該函數的詞法環境的引用。
因此,counter.[[Environment]]
有對 {count: 0}
詞法環境的引用。這就是函數記住它創建于何處的方式,與函數被在哪兒調用無關。[[Environment]]
引用在函數創建時被設置並永久保存。
稍後,當調用 counter()
時,會爲該調用創建壹個新的詞法環境,並且其外部詞法環境引用獲取于 counter.[[Environment]]
:
現在,當 counter()
中的代碼查找 count
變量時,它首先搜索自己的詞法環境(爲空,因爲那裏沒有局部變量),然後是外部 makeCounter()
的詞法環境,並且在哪裏找到就在哪裏修改。
在變量所在的詞法環境中更新變量。
這是執行後的狀態:
如果我們調用 counter()
多次,count
變量將在同壹位置增加到 2
,3
等。
閉包
開發者通常應該都知道“閉包”這個通用的編程術語。
閉包 是指壹個函數可以記住其外部變量並可以訪問這些變量。在某些編程語言中,這是不可能的,或者應該以壹種特殊的方式編寫函數來實現。但如上所述,在 JavaScript 中,所有函數都是天生閉包的(只有壹個例外,將在 "new Function" 語法 中講到)。
也就是說:JavaScript 中的函數會自動通過隱藏的 [[Environment]]
屬性記住創建它們的位置,所以它們都可以訪問外部變量。
在面試時,前端開發者通常會被問到“什麽是閉包?”,正確的回答應該是閉包的定義,並解釋清楚爲什麽 JavaScript 中的所有函數都是閉包的,以及可能的關于 [[Environment]]
屬性和詞法環境原理的技術細節。
通常,函數調用完成後,會將詞法環境和其中的所有變量從內存中刪除。因爲現在沒有任何對它們的引用了。與 JavaScript 中的任何其他對象壹樣,詞法環境僅在可達時才會被保留在內存中。
但是,如果有壹個嵌套的函數在函數結束後仍可達,則它將具有引用詞法環境的 [[Environment]]
屬性。
在下面這個例子中,即使在(外部)函數執行完成後,它的詞法環境仍然可達。因此,此詞法環境仍然有效。
例如:
function f() { let value = 123; return function() { alert(value); } } let g = f(); // g.[[Environment]] 存儲了對相應 f() 調用的詞法環境的引用
請注意,如果多次調用 f()
,並且返回的函數被保存,那麽所有相應的詞法環境對象也會保留在內存中。下面代碼中有三個這樣的函數:
function f() { let value = Math.random(); return function() { alert(value); }; } // 數組中的 3 個函數,每個都與來自對應的 f() 的詞法環境相關聯 let arr = [f(), f(), f()];
當詞法環境對象變得不可達時,它就會死去(就像其他任何對象壹樣)。換句話說,它僅在至少有壹個嵌套函數引用它時才存在。
在下面的代碼中,嵌套函數被刪除後,其封閉的詞法環境(以及其中的 value
)也會被從內存中刪除:
function f() { let value = 123; return function() { alert(value); } } let g = f(); // 當 g 函數存在時,該值會被保留在內存中 g = null; // ……現在內存被清理了
正如我們所看到的,理論上當函數可達時,它外部的所有變量也都將存在。
但在實際中,JavaScript 引擎會試圖優化它。它們會分析變量的使用情況,如果從代碼中可以明顯看出有未使用的外部變量,那麽就會將其刪除。
在 V8(Chrome,Edge,Opera)中的壹個重要的副作用是,此類變量在調試中將不可用。
打開 Chrome 浏覽器的開發者工具,並嘗試運行下面的代碼。
當代碼執行暫停時,在控制台中輸入 alert(value)
。
function f() { let value = Math.random(); function g() { debugger; // 在 Console 中:輸入 alert(value); No such variable! } return g; } let g = f(); g();
正如妳所見的 —— No such variable! 理論上,它應該是可以訪問的,但引擎把它優化掉了。
這可能會導致有趣的(如果不是那麽耗時的)調試問題。其中之壹 —— 我們可以看到的是壹個同名的外部變量,而不是預期的變量:
let value = "Surprise!"; function f() { let value = "the closest value"; function g() { debugger; // 在 console 中:輸入 alert(value); Surprise! } return g; } let g = f(); g();
V8 引擎的這個特性妳真的應該知道。如果妳要使用 Chrome/Edge/Opera 進行代碼調試,遲早會遇到這樣的問題。
這不是調試器的 bug,而是 V8 的壹個特別的特性。也許以後會被修改。妳始終可以通過運行本文中的示例來進行檢查。
重要程度: 5
函數 sayHi 使用外部變量。當函數運行時,將使用哪個值?
let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; sayHi(); // 會顯示什麽:"John" 還是 "Pete"?
這種情況在浏覽器和服務器端開發中都很常見。壹個函數可能被計劃在創建之後壹段時間後才執行,例如在用戶行爲或網絡請求之後。
因此,問題是:它會接收最新的修改嗎?
答案:Pete。
函數將從內到外依次在對應的詞法環境中尋找目標變量,它使用最新的值。
舊變量值不會保存在任何地方。當壹個函數想要壹個變量時,它會從自己的詞法環境或外部詞法環境中獲取當前值。
重要程度: 5
下面的 makeWorker
函數創建了另壹個函數並返回該函數。可以在其他地方調用這個新函數。
它是否可以從它被創建的位置或調用位置(或兩者)訪問外部變量?
function makeWorker() { let name = "Pete"; return function() { alert(name); }; } let name = "John"; // 創建壹個函數 let work = makeWorker(); // 調用它 work(); // 會顯示什麽?
會顯示哪個值?“Pete” 還是 “John”?
答案:Pete.
下方代碼中的函數 work()
在其被創建的位置通過外部詞法環境引用獲取 name
:
所以這裏的結果是 "Pete"
。
但如果在 makeWorker()
中沒有 let name
,那麽將繼續向外搜索並最終找到全局變量,正如我們可以從上圖中看到的那樣。在這種情況下,結果將是 "John"
。
重要程度: 5
在這兒我們用相同的 makeCounter
函數創建了兩個計數器(counters):counter
和 counter2
。
它們是獨立的嗎?第二個 counter 會顯示什麽?0,1
或 2,3
還是其他?
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); let counter2 = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter2() ); // ? alert( counter2() ); // ?
答案是:0,1。
函數 counter
和 counter2
是通過 makeCounter
的不同調用創建的。
因此,它們具有獨立的外部詞法環境,每壹個都有自己的 count
。
重要程度: 5
這裏通過構造函數創建了壹個 counter 對象。
它能正常工作嗎?它會顯示什麽呢?
function Counter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new Counter(); alert( counter.up() ); // ? alert( counter.up() ); // ? alert( counter.down() ); // ?
當然行得通。
這兩個嵌套函數都是在同壹個詞法環境中創建的,所以它們可以共享對同壹個 count 變量的訪問:
function Counter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new Counter(); alert( counter.up() ); // 1 alert( counter.up() ); // 2 alert( counter.down() ); // 1
重要程度: 5
看看下面這個代碼。最後壹行代碼的執行結果是什麽?
let phrase = "Hello"; if (true) { let user = "John"; function sayHi() { alert(`${phrase}, ${user}`); } } sayHi();
答案:error。
函數 sayHi
是在 if
內聲明的,所以它只存在于 if
中。外部是沒有 sayHi
的。
重要程度: 4
編寫壹個像 sum(a)(b) = a+b
這樣工作的 sum
函數。
是的,就是這種通過雙括號的方式(並不是錯誤)。
舉個例子:
sum(1)(2) = 3 sum(5)(-1) = 4
爲了使第二個括號有效,第壹個(括號)必須返回壹個函數。
就像這樣:
function sum(a) { return function(b) { return a + b; // 從外部詞法環境獲得 "a" }; } alert( sum(1)(2) ); // 3 alert( sum(5)(-1) ); // 4
重要程度: 4
下面這段代碼的結果會是什麽?
let x = 1; function func() { console.log(x); // ? let x = 2; } func();
P.S. 這個任務有壹個陷阱。解決方案並不明顯。
答案:error。
妳運行壹下試試:
let x = 1; function func() { console.log(x); // ReferenceError: Cannot access 'x' before initialization let x = 2; } func();
在這個例子中,我們可以觀察到“不存在”的變量和“未初始化”的變量之間的特殊差異。
妳可能已經在 變量作用域,閉包 中學過了,從程序執行進入代碼塊(或函數)的那壹刻起,變量就開始進入“未初始化”狀態。它壹直保持未初始化狀態,直至程序執行到相應的 let
語句。
換句話說,壹個變量從技術的角度來講是存在的,但是在 let
之前還不能使用。
下面的這段代碼證實了這壹點。
function func() { // 引擎從函數開始就知道局部變量 x, // 但是變量 x 壹直處于“未初始化”(無法使用)的狀態,直到遇到 let,此時“死區”結束 // 因此答案是 error console.log(x); // ReferenceError: Cannot access 'x' before initialization let x = 2; }
變量暫時無法使用的區域(從代碼塊的開始到 let
)有時被稱爲“死區”。
重要程度: 5
我們有壹個內建的數組方法 arr.filter(f)
。它通過函數 f
過濾元素。如果它返回 true
,那麽該元素會被返回到結果數組中。
制造壹系列“即用型”過濾器:
inBetween(a, b)
—— 在 a
和 b
之間或與它們相等(包括)。
inArray([...])
—— 包含在給定的數組中。
用法如下所示:
arr.filter(inBetween(3,6))
—— 只挑選範圍在 3 到 6 的值。
arr.filter(inArray([1,2,3]))
—— 只挑選與 [1,2,3]
中的元素匹配的元素。
例如:
/* .. inBetween 和 inArray 的代碼 */ let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
打開帶有測試的沙箱。
function inBetween(a, b) { return function(x) { return x >= a && x <= b; }; } let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6
function inArray(arr) { return function(x) { return arr.includes(x); }; } let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
使用沙箱的測試功能打開解決方案。
重要程度: 5
我們有壹組要排序的對象:
let users = [ { name: "John", age: 20, surname: "Johnson" }, { name: "Pete", age: 18, surname: "Peterson" }, { name: "Ann", age: 19, surname: "Hathaway" } ];
通常的做法應該是這樣的:
// 通過 name (Ann, John, Pete) users.sort((a, b) => a.name > b.name ? 1 : -1); // 通過 age (Pete, Ann, John) users.sort((a, b) => a.age > b.age ? 1 : -1);
我們可以讓它更加簡潔嗎,比如這樣?
users.sort(byField('name')); users.sort(byField('age'));
這樣我們就只需要寫 byField(fieldName)
,而不是寫壹個函數。
編寫函數 byField
來實現這個需求。
打開帶有測試的沙箱。
function byField(fieldName){ return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1; }
使用沙箱的測試功能打開解決方案。
重要程度: 5
下列的代碼創建了壹個 shooters
數組。
每個函數都應該輸出其編號。但好像出了點問題……
function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let shooter = function() { // 創建壹個 shooter 函數, alert( i ); // 應該顯示其編號 }; shooters.push(shooter); // 將此 shooter 函數添加到數組中 i++; } // ……返回 shooters 數組 return shooters; } let army = makeArmy(); // ……所有的 shooter 顯示的都是 10,而不是它們的編號 0, 1, 2, 3... army[0](); // 編號爲 0 的 shooter 顯示的是 10 army[1](); // 編號爲 1 的 shooter 顯示的是 10 army[2](); // 10,其他的也是這樣。
爲什麽所有的 shooter 顯示的都是同樣的值?
修改代碼以使得代碼能夠按照我們預期的那樣工作。
打開帶有測試的沙箱。
讓我們檢查壹下 makeArmy
內部到底發生了什麽,那樣答案就顯而易見了。
它創建了壹個空數組 shooters
:
let shooters = [];
在循環中,通過 shooters.push(function)
用函數填充它。
每個元素都是函數,所以數組看起來是這樣的:
shooters = [ function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); }, function () { alert(i); } ];
該數組返回自函數。
然後,對數組中的任意數組項的調用,例如調用 army[5]()
(它是壹個函數),將首先從數組中獲取元素 army[5]()
並調用它。
那麽,爲什麽所有此類函數都顯示的是相同的值,10
呢?
這是因爲 shooter
函數內沒有局部變量 i
。當壹個這樣的函數被調用時,i
是來自于外部詞法環境的。
那麽,i
的值是什麽呢?
如果我們看壹下源代碼:
function makeArmy() { ... let i = 0; while (i < 10) { let shooter = function() { // shooter 函數 alert( i ); // 應該顯示它自己的編號 }; shooters.push(shooter); // 將 shooter 函數添加到該數組中 i++; } ... }
……我們可以看到,所有的 shooter
函數都是在 makeArmy()
的詞法環境中被創建的。但當 army[5]()
被調用時,makeArmy
已經運行完了,最後 i
的值爲 10
(while
循環在 i=10
時停止)。
因此,所有的 shooter
函數獲得的都是外部詞法環境中的同壹個值,即最後的 i=10
。
正如妳在上邊所看到的那樣,在 while {...}
塊的每次叠代中,都會創建壹個新的詞法環境。因此,要解決此問題,我們可以將 i
的值複制到 while {...}
塊內的變量中,如下所示:
function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let j = i; let shooter = function() { // shooter 函數 alert( j ); // 應該顯示它自己的編號 }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); // 現在代碼正確運行了 army[0](); // 0 army[5](); // 5
在這裏,let j = i
聲明了壹個“局部叠代”變量 j
,並將 i
複制到其中。原始類型是“按值”複制的,因此實際上我們得到的是屬于當前循環叠代的獨立的 i
的副本。
shooter 函數正確運行了,因爲 i
值的位置更近了(譯注:指轉到了更內部的詞法環境)。不是在 makeArmy()
的詞法環境中,而是在與當前循環叠代相對應的詞法環境中:
如果我們壹開始使用 for
循環,也可以避免這樣的問題,像這樣:
function makeArmy() { let shooters = []; for(let i = 0; i < 10; i++) { let shooter = function() { // shooter 函數 alert( i ); // 應該顯示它自己的編號 }; shooters.push(shooter); } return shooters; } let army = makeArmy(); army[0](); // 0 army[5](); // 5
這本質上是壹樣的,因爲 for
循環在每次叠代中,都會生成壹個帶有自己的變量 i
的新詞法環境。因此,在每次叠代中生成的 shooter
函數引用的都是自己的 i
。
至此,妳已經花了很長時間來閱讀本文,發現最終的解決方案就這麽簡單 — 使用 for
循環,妳可能會疑問 —— 我花了這麽長時間讀這篇文章,值得嗎?
其實,如果妳可以輕松地明白並答對本題目,妳應該就不會閱讀它的答案。所以,希望這個題目可以幫助妳更好地理解閉包。
此外,確實存在有些人相較于 for
更喜歡 while
,以及其他情況。
使用沙箱的測試功能打開解決方案。