JS中的閉包為何會產生"副作用,即閉包只能取得包含函數中任何變數的最後一個值"?
本人JS 小白一枚,問題RT:
比如這個Demo 為何輸出的始終是10?文中提到因為每個函數作用域中都保存著活動對象,所以i的值是10,那麼也就等同於非同步請求的效果,先執行完JS裡面的for循環,然後全部執行返回?為什麼會出現這種情況?而且JS 的閉包宏觀上來說,是個函數都屬於閉包,那麼什麼樣的閉包會有這種效果?望大腿們給予小弟一些提示~
首先任意一個 JS 的函數(值)都包含兩個部分:它的代碼,和一個 upvalue 指針。而在函數被定義的時候,會設置 upvalue 指針為當前棧幀(Stack frame),即存儲局部變數的一個結構(這裡做了簡化,嚴格的定義牽涉到很多東西比如 with 的處理之類)。因此圖例中的 createFunctions 被調用後產生了十個不同的函數,但是它們共享相同的代碼和相同的 upvalue,因此它們被再次調用之後便會循著這個 upvalue 找到相同的 i。
(你用 let 代替 var 的話,那麼每次循環都會刷出來一個不同的 i,因為多壓了一層棧幀。此時 10 個函數被調用後輸出的結果就不同了。)
謝邀。
因為 var 是函數作用域變數
所以本題中 i 的作用域即為 createFunctions 作用域
所以 result 中所有的成員函數中的 i 為同一變數
而在任一時刻,這個變數 i 只能有一個取值
所以 result 中所有的成員函數調用時 i 的取值都為 10
注意,以上結論與 pass by value 還是 pass by reference 無關,也與 變數i 的數據類型無關,僅與變數的作用域有關。
如果通過函數調用創建一個新的作用域變數
for(var i=0;i&<10;++i){
(function(j){
result[j] = function(){
return j;
};
})(i);
}
每次循環都會創建一個變數 j , result 的每個成員函數中的 j 為不同的取值。
又或者使用塊級作用域變數聲明
for(let i=0;i&<10;++i){
result[i] = function(){
return i;
};
}
變數i 的作用域為塊級,result 的每個成員函數中的 i 為不同的取值。
同一個域里一個變數只能有一個值,而閉包是一個域(你可能沒明白這一點),為result中的所有function所共享。
這個函數返回十個函數,每個函數返回i,函數執行前i只是create函數中變數,準確點是for的作用域塊中的i,不是你腦補的12345。內存中不會有十個i,只有一個。所以這十個函數執行後return的都是循環完畢後的i這個變數的值——10。
這其實和閉包沒關係..result[i]所儲存之匿名函數中所使用的i皆為createfunction函數中for循環所定義的i的引用才是這個問題產生的原因。閉包不過是剛好出現在它旁邊罷了,何苦非要糾纏在一塊虐待自己。
話說,不管是JavaScript還是C#,用captured variable(捕獲變數)來解釋概念上(而不是實現上)的閉包總是最容易的:
Lambdas - Know Your Closures
要點:在你的例子中,i是一個局部變數。它是createFunctions函數的局部變數。當在for循環中調用function關鍵字來創建匿名函數(閉包)時,i被捕獲。那麼,只要createFunctions函數還沒有退出,所有這些匿名函數中的i都是一起變化的。
一旦createFunctions函數退出了,那麼createFunctions函數中原先的那個i就不會被createFunctions函數再次引用了。也就是說,如果沒有那些創建出來的閉包,那麼i就是可以被垃圾回收了(在C#的實現中,以及為了方便理解在概念上,可以認為這樣的i是在堆上創建的,所以可以被垃圾回收;但也因為是在堆上創建的,createFunctions退出之後,那些匿名函數還可以用到i)。匿名函數仍然可以讀寫這個共享的i。以下例子可以看出在匿名函數里,i是共享的:
function createFunctions()
{
var result = new Array();
var i;
for (i = 0; i &< 2; i++) { result.push((function(j) { return function() { i += j + 1; return i; }; })(i)); } // Expected: 2 WScript.Echo("createFunctions: " + i.toString()); return resu< } var funcs = createFunctions(); // Expected: 3 WScript.Echo("funcs[0]: " + funcs[0]().toString()); // Expected: 5 WScript.Echo("funcs[1]: " + funcs[1]().toString()); // Expected: 7 WScript.Echo("funcs[1]: " + funcs[1]().toString()); // Expected: 8 WScript.Echo("funcs[0]: " + funcs[0]().toString());
C#版本(需要C# 3.0或更高):
using System;
using System.Collections.Generic;
namespace RobbieTests
{
class Closure02
{
private static List&
{
var result = new List&
int i;
for (i = 0; i &< 2; i++) {
int j = i;
result.Add(() =&> { i += j + 1; return i; });
}
// Expected: 2
Console.WriteLine("createFunctions: {0}", i);
return resu<
}
public static void Main()
{
var funcs = createFunctions();
// Expected: 3
Console.WriteLine("funcs[0]: {0}", funcs[0]());
// Expected: 5
Console.WriteLine("funcs[1]: {0}", funcs[1]());
// Expected: 7
Console.WriteLine("funcs[1]: {0}", funcs[1]());
// Expected: 8
Console.WriteLine("funcs[0]: {0}", funcs[0]());
}
}
}
並不懂JavaScript。看了下代碼,這應該capture-by-reference和capture-by-value的問題。前者的閉包返回的是既不是全局變數又不是局部變數的變數的引用(也就是題主所說的「最後的值」)。後者返回的是值。js應該和Python一樣,用的是capture-by-reference
推薦閱讀:
※如何識別用戶是通過 PC、iPad 還是手機來訪問網站?
※css設置元素的寬高為整數,為什麼有的瀏覽器解析出來的寬高是小數?
※開發vue(或類似的MVVM框架)的過程中,需要面對的主要問題有哪些?
※研究js框架源碼用處真的大么?
※畢業後在建築工地上呆了半年後,辭職後堅決打算轉行做前端,但是卻被現實動搖了——還能做什麼工作?
TAG:前端開發 | JavaScript語言精粹 | JavaScript編程 |