和其他大多數現代編程語言一樣,JS也采用詞法作用域,也就是說,函數的執行依賴於變量作用域,這個作用域是在函數定義時決定的,而不是函數調用時決定的。為了實現這種詞法作用域,JS函數對象的內部狀態不僅包含函數的代碼邏輯,還必須引用當前的作用域鏈。函數對象可以通過作用域鏈相互關聯起來,函數體內部的變量都可以保存在函數作用域內,這種特性在計算機科學中稱為“閉包”。
理解閉包首先要了解嵌套函數的詞法作用域規則。看一下下面的代碼:
var scope="global scope"; function checkscope(){ var scope="local scope"; function f(){return scope;} return f(); } checkscope()
checkscope()函數聲明了一個局部變量,並定義了一個函數f(),函數f()返回了這個變量的值,最后將其執行結果返回。明顯返回結果將是“local scope”。現在對代碼做一些改動:
var scope="global scope"; function checkscope(){ var scope="local scope"; function f(){return scope;} return f; } checkscope()()
在這段代碼中,將函數內的一對圓括號移動到了checkscope()之后,checkscope()僅僅返回函數內嵌套的一個函數對象,而不是直接返回結果。在定義函數的作用域外調用這個嵌套的函數會發生什么?
注意:函數的執行依賴於變量作用域,這個作用域是在函數定義時決定的,而不是函數調用時決定的。嵌套的函數f()定義在這個作用域鏈里,scope一定是局部變量,無論何時何地調用f(),返回都是“local scope”。
閉包的特性:它們可以捕捉到局部變量和參數,並一直保存下來,看起來像這些變量綁定到了其中定義它們的外部函數。
為什么在閉包中,外部函數定義的局部變量在函數返回后依然存在?
每次調用JS函數的時候,都會為之創建一個新的對象用來保存局部變量,把這個對象添加到作用域鏈中。當函數返回的時候,就從作用域鏈中將這個綁定變量的對象刪除。如果不存在嵌套的函數,也沒有其他引用指向這個綁定對象,他就會被當作垃圾回收掉。如果定義了嵌套的函數,每個嵌套的函數都各自對應一個作用域鏈,並且這個作用域鏈指向一個變量綁定對象。但如果這些嵌套的函數對象在外部函數中保存下來,那么它們也會和所指向的變量綁定對象一樣當作垃圾回收。但是如果這個函數定義了嵌套的函數,並將它作為返回值返回或者存儲在某處的屬性里,這時就會有一個外部引用指向這個嵌套的函數,它就不會被當作垃圾回收,並且它所指向的變量綁定對象也不會被當作垃圾回收。
使用閉包技術可以實現私有狀態的共享。
//這個函數給對象o增加了屬性存儲器方法 //方法的名稱為get<name>和set<name>。如果提供了一個判定函數, //setter方法就會用它來檢測參數的合法性,然后再存儲它, //如果判定函數返回false,setter方法拋出一個異常。 // //這個函數有一個非同尋常之處,就是getter和setter函數 //所操作的屬性值並沒有存儲在對象o中, //相反,這個值僅僅是保存在函數中的局部變量中 //getter和setter方法同樣是局部函數,因此可以訪問這個局部變量 //也就是說,對於兩個存儲方法來說,這個變量是私有的, //沒有辦法繞過存儲器方法來設置或修改這個值。 function addPrivateProperty(o,name,predicate){ var value;//這是一個屬性值 //getter方法簡單地將其返回 o["get"+name]=function(){return value;}; //setter方法首先檢查值是否合法,若不合法就拋出異常 //否則就將其存儲起來 o["set"+name]=function(v){ if(predicate&&!predicate(v)) throw Error("set"+name+":invalid value "+v); else value=v; }; } //下面的代碼展示了addPrivateProperty()方法 var o={};//設置一個空對象 //增加屬性存儲器方法getName()和setName() //確保只允許字符串值 addPrivateProperty(o,"Name",function(x){return typeof x=="string";}); o.setName("Frank");//設置屬性值 console.log(o.getName());//得到屬性值 o.setName(1);//試圖設置一個錯誤類型的值
在同一個作用域鏈中定義兩個閉包,這兩個閉包共享同樣的私有變量或變量,這是一種非常重要的技術。
試圖將循環代碼移入定義這個閉包的函數之內時要格外小心,看下面的代碼:
function constfuncs() { var funcs = []; for (var i = 0; i < 10; i++) { funcs[i] =function () {return i; }; } return funcs; } var funcs = constfuncs(); console.log(funcs[5]());//10
上面這段代碼創建了10個閉包,並將它們存儲到一個數組中。這些閉包都是在同一個函數調用中定義的,因此它們可以共享變量i。當constfuncs()返回時,變量i的值是10,所有的閉包都共享這一個值,因此數組中的函數的返回值都是同一個值。關聯到閉包的作用域鏈都是“活動的”,嵌套的函數不會將作用域內的私有成員復制一份,也不會對綁定的變量生成靜態快照。
解決方法可以再使用一個閉包,每次循環都將i的值傳入:
function constfuncs() { var funcs = []; for (var i = 0; i < 10; i++) { funcs[i] = (function (i) { return function () { return i; }; })(i); } return funcs; } var funcs = constfuncs(); console.log(funcs[5]());//5
從一個微信公共號(web前端開發)看到的關於閉包的更通俗的一些解釋:
什么是閉包?
基本原理:閉包是由函數引用其周邊狀態(詞法環境)綁在一起形成的(封裝)組合結構。在JS中,閉包在每個函數被創建時形成。由於閉包和它的詞法環境綁在一起,因此閉包讓我們能夠從一個函數內部訪問外部函數的作用域。要使用閉包,只需要簡單的將一個函數定義在另一個函數內部,並將它暴露出來,要暴露一個函數,可以將它返回或者傳給其他函數。內部函數將能夠訪問到外部函數作用域中的變量,即使外部函數已經執行完畢(上文已解釋)。
閉包的使用場景:
1、對象的私有數據,如上文。
2、偏函數應用:一個過程,它傳給某個函數其中一部分參數,然后返回一個新的函數,該函數等待接受后續參數。
3、柯里化:以后補充。