前言
對於我來說,初次接觸到閉包時我是懵逼的,因爲那是我第一次看到函數嵌套,也是那時,我不禁懷疑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]();
你覺得會輸出什麼值呢?
結尾
這個時候再回憶一下開頭的幾個問題,如果都能答上了,這個知識點應該也算入門了,其餘的就只有靠自己舉一反三了。