定義
函數與對其狀態即詞法環境(lexical environment)的引用共同構成閉包(closure)。也就是說,閉包可以讓你從內部函數訪問外部函數作用域。在JavaScript,函數在每次創建時生成閉包。
通俗的說就是:定義在一個函數內部的函數,並且該被函數訪問外部函數作用域
只有調用外部函數時纔會生成新閉包,不然就是同一個閉包
作用域鏈
對變量進行訪問時,最先在自己的作用域中查找,如果找到則停止查找,否則向向一級作用域中查找,直到全局作用域中。如果還是沒有找到就會報錯。
例子:
// var myName = 'global'
function fn(){
// var myName = 'fn'
function fn1(){
// var myName = 'fn1'
console.log(myName)
}
fn1()
}
fn()
在上面的代碼裏依次從下面向上註釋,輸出結果依次是fn1
,fn
,global
,Uncaught ReferenceError: myName is not defined
,最後在全局作用域裏都沒找到就會報錯;
詞法作用域
詞法作用域:指函數在定義(或聲明)它們的作用域裏運行,而不是在執行(或調用)它們的作用裏運行;看個例子
var name = 'fn1_name'
function fn1(){
console.log(name)
}
function fn2(){
var name = 'fn2_name'
fn1()
}
fn2()// fn1_name
fn1是在全局中聲明的,因此fn1是在全局作用域中運行的,然後根據作用域鏈查找name,fn1函數中沒有向向一級作用域查找。最終在全局中找到它爲fn1_name
閉包
總所周知,一個函數運行完畢以後,該函數作用域裏的所有變量都會被垃圾機制回收;而閉包就是延長了作用域鏈;
閉包例子:
- 函數作爲返回值返回
function fn(){
var myName = 'fn'
return function bar(){
console.log(myName)
}
}
var f = fn() //bar函數的引用賦值給變量f
f() //調用b函數 輸出fn
上面的函數就是閉包的一種,var f = fn()
產生了閉包,延長myName
的作用域鏈,導致fn函數運行完畢後,myName得不到釋放;
我們可以打個斷點就清楚,閉包產生Closure
,記着只有引用了外部函數的變量纔會產生閉包。還有上面的Call Stack代表了調用棧,現在是bar,是函數名,如果該函數爲匿名函數則爲anonymous,如果點到下面的anonymous,就是匿名的全局,斷點就會跑到37行;
例子2:
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5); //返回匿名函數,x對外部函數有引用
var add10 = makeAdder(10);
console.log(add5(2)); // 7 相當於 function(2){return 5+2}
console.log(add10(2)); // 12
例子3:
for(var i=0;i<3;i++){
btn[i].onclick = function(){
console.log(i) //輸出都是3
}
}
上面的代碼運行了3次也就註冊了3個按鈕的點擊事件,但是隻有通過點擊按鈕纔會執行函數,所以那時已經變爲3了。
利用閉包:
for(var i=0;i<3;i++){
(function(j){
btn[i].onclick = function(){
console.log(j)
}
})(i)
}
這樣操作的話,產生了3個閉包也保存了它們的值。當然我們開發時一般都是使用屬性保存索引,ES6中的let
塊作用域也可以解決該問題。
用閉包模擬私有方法:
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
上面的代碼,對象的privateCounter只能通過暴露的方法修改,從而有了私有屬性或者方法;模塊化應該的思想應該就是這樣,像JQuery,向外暴露一些方法。
試題
var name = 'The window'
var obj = {
name:'my obj',
sayName:function(){
return function(){
return this.name
}
}
}
alert(obj.sayName()()) //The window
alert(obj.sayName().call(obj)) //my obj
上述代碼輸出The window,因爲obj.sayName()()的調用對象是window ,所以this指向The window;第二個是修改this的指向爲obj;
var name = 'The window'
var obj = {
name:'my obj',
sayName:function(){
var that = this
return function(){
return that.name
}
}
}
alert(obj.sayName()()) //my obj
這個是把this的指向的引用賦值給that,然後是閉包,內部函數引用了外部函數的變量;
function fun(n,o){
console.log(o)
return{
fun:function(m){
return fun(m,n)
}
}
}
var a = fun(0);//返回一個對象{fun:function(){}} 輸出undefined,產生一個閉包
a.fun(1);//向function(m)傳1,n引用爲0,輸出0
a.fun(2);//向function(m)傳2,n引用爲0,輸出0
a.fun(3)//向function(m)傳3,n引用爲0,輸出0
var b = fun(0).fun(1).fun(2).fun(3)
//返回一個對象{fun:function(){}} 輸出undefined,產生閉包
//第二次再次調用了外部函數又產生一個閉包,輸出0
//第三次再次調用了外部函數又產生一個閉包,輸出1
//第四次再次調用了外部函數又產生一個閉包,輸出2
var c = fun(0).fun(1);
//在第二個閉包的基礎上開始,不再生成新閉包,使用先輸出undefined,0
c.fun(2);//輸出1
c.fun(3)//輸出1
上述代碼,注意有沒有生成新的閉包;
總結
閉包,我們可能都用過,但是也沒有刻意的去使用它,都是潛意識的。但是閉包可能會造成內存泄漏,所以要謹慎使用。