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&&> createFunctions()
{
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編程 |