一篇文章帶你弄懂閉包

前言

對於我來說,初次接觸到閉包時我是懵逼的,因爲那是我第一次看到函數嵌套,也是那時,我不禁懷疑Java的萬物皆對象是個假命題。好吧,有點扯遠了。接着迴歸正題,請帶着這幾個問題看這篇文章:
1.什麼是閉包?
2.什麼情況會產生閉包?
3.使用閉包應該注意什麼?
4.閉包帶來了什麼好處?

作用域

要想理解閉包,必須要理解作用域。JavaScript的作用域可以看成三種:全局作用域、函數作用域、塊級作用域。在這裏只需要用到兩種:全局作用域和函數作用域,如果對塊級作用域感興趣,可以看看我以前寫的一篇文章帶你弄懂var、let與const
例1:

function func1(){
	var _name = 'func1';//因爲window下面有個name屬性  所以所有的變量用_name命名
	console.log('_name : ' + _name );//_name : func1
}
function func2(){
	var _name = 'func2';
	console.log('_name : ' + _name );//_name : func2
}
func1();
func2();

對於上面代碼中的兩個function中的變量name來說,它們是不會互相干涉的,因爲它們都各自生活在不同的函數作用域。在func1中是不能訪問到func2中的變量,而func2中也不能訪問到func1中的變量。

例2:

function outside(){
	var _name = 'outside';
	function inside(){
		var _name = 'inside';
		console.log('_name : ' + _name );//_name : inside
	}
	inside();
}
outside();

例3:

function outside(){
	var _name = 'outside';
	function inside(){
		console.log('_name : ' + _name );//_name  : outside
	}
	inside();
}
outside();

這裏的兩套代碼都是inside方法被嵌套在outside中,唯一的不同是inside中變量name的有無。在例2中inside方法中輸出的name是inside方法中聲明的變量name的值,而在例3中,由於inside方法中未對變量name進行聲明,所以輸出的是outside中聲明的變量name的值。

例4:

var _name = 'window';
function outside(){
	function inside(){
		console.log('_name : ' + _name );//_name  : window
	}
	inside();
}
outside();

在例4中,情況又與先前不一致,outside和inside中都未對變量name進行聲明,而是在window(假設現在運行的環境在瀏覽器)下對name進行了聲明。

例5:

function outside(){
	function inside(){
		console.log('_name : ' + _name );//_name is not defined
	}
	inside();
}
outside();

縱觀例1、2、3、4、5輸出的變量name的值,我們可以得出結論:
函數作用域中的值互不影響;當函數發生嵌套時,內部函數首先訪問自身作用域中的變量,如果沒有此變量,則繼續訪問立自己最近的外部函數的作用域,如果找到,則使用此變量,如果還是沒有此變量,則繼續訪問…一直到全局作用域,如果找到則使用,如果還是沒有,就報 is not defined

closure

這裏直接借用例3的代碼

function outside(){
	var _name = 'outside';
	function inside(){
		debugger
		console.log('_name : ' + _name );//_name  : outside
	}
	inside();
}
outside();

在這裏插入圖片描述
由於在inside作用域中沒有聲明變量_name,在inside被執行時會去執行環境(也就是outside中)尋找變量_name;在outside中是聲明瞭變量_name,inside就直接使用這個在outside中聲明的變量_name,從而在它(這裏指的是inside)的Scope(作用域)中生成了Closure(閉包)。

使用場景

學過Java的小夥伴肯定知道,在變量聲明前加個private,這個變量就變成私有變量了,但是JavaScript沒有private,那在JavaScript中是不是就不能創建私有變量了呢?答案肯定是否定的。其實在我以前寫的 函數防抖函數節流這兩篇文章中就有使用閉包創建私有變量的案例,感興趣的可以看一下。

//閉包創建私有變量
function timerFunc(){
	console.log("移動時間:"+new Date().getTime());
}

function moveFunc(func,delay){
	var timer = null;
	return () => {
		if(null != timer){
			clearTimeout(timer);
		}
		timer = setTimeout(()=>{
			func.apply(this, arguments)
		} ,delay);
	}
}

window.addEventListener("mousemove", moveFunc(timerFunc,1000));

//閉包創建私有變量寫法
function ableClick(func,delay){
	var time = null;
	return () => {
		var nowTime = new Date().getTime();
		if(time == null || (nowTime - time > delay)){
			time = nowTime;
			func.apply(this,arguments);
		}
	}
}
function clickFunc(){
	console.log("點擊時間:"+new Date().getTime());
}
window.addEventListener("click", ableClick(clickFunc,1000));

這裏其實只要理解了函數聲明的函數也是一個對象,應該也就沒什麼難點了。

注意事項

下面的代碼是將例3進行修改後得到的。

//第一步
function outside(){
	var _name = 'outside';
	function inside(){
		debugger
		console.log('_name : ' + _name );//_name  : outside
	}
	return inside;
}
var func = outside();
//第二步
func();

第一步,我們先得到outside函數返回的inside對象,並將func變量指向它;
第二步,執行func方法;
在這裏插入圖片描述
可能你已經發現,outside方法都已經return了,在inside中還能使用outside的變量。這裏按照我接觸的知識,v8肯定是做了大量的優化了,但是這裏具體是優化到持有outside不釋放呢,還是隻是持有_name不釋放,我也不是很清楚。如果只是持有_name不釋放,我個人覺得問題不大,如果是持有outside不釋放,而outside中又有佔據大內存的對象,肯定就容易引起內存泄漏了。不過我試過在outside中再聲明一個變量,如果在inside中不使用此變量,是在Closure中是不會有此變量的(只在v8下面試過)。

閉包的使用還有一點需要注意,一篇文章帶你弄懂var、let與const中的擴展中有提到過,比如像下面的代碼:

const func = (() => {
    var arr = [];
    for(var i = 0,length = 10;i<length;i++){
        arr.push((() => {
            return i;
        }));
    }
    return arr;
})
func()[1]();

你覺得會輸出什麼值呢?

結尾

這個時候再回憶一下開頭的幾個問題,如果都能答上了,這個知識點應該也算入門了,其餘的就只有靠自己舉一反三了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章