如果要了解閉包想诅,我們需要先了解閉包的由來召庞,閉包的產(chǎn)生岛心,源于JS的詞法作用域
詞法作用域
作用域是指一個 變量能夠訪問到的區(qū)域 例如我們設(shè)置一個var n=0;實際分兩步篮灼,聲明和賦值忘古,var n;n=1诅诱;在js中只有函數(shù)能夠限定作用域髓堪,除此之外聲明的都是全局作用域,在ES6的新標(biāo)準(zhǔn)中娘荡,采用了let和const來限定局部變量干旁,注意局部變量的優(yōu)先級是高于全局變量的
執(zhí)行環(huán)境是JavaScript中最為重要的一個概念,在js代碼開始執(zhí)行的時候炮沐,會創(chuàng)建一個匿名函數(shù)的執(zhí)行環(huán)境争群,執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其它數(shù)據(jù),每個執(zhí)行環(huán)境都有一個與之相關(guān)的變量對象央拖,環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中,雖然我們編寫的代碼無法訪問這個對象鹉戚,但解析器咋及處理數(shù)據(jù)的時候會在后臺使用它
變量的作用域是在定義的時候決定的而不是在執(zhí)行的時候決定的鲜戒,變量只能在本層作用域和上層作用域生效,不能在下層作用域生效
舉一個栗子來加深理解
var num;
function f1(){
num=3;
}
f1();
console.log(num);//3
在這個栗子中抹凳,num的值在全局最開始設(shè)置的是undefined遏餐,但是在函數(shù)f1中改變了該值,這個值變?yōu)?赢底,在函數(shù)f1執(zhí)行了一次之后我們再打印num的值失都,得到的是num=3,通過解讀JavaScript是詞法作用域我們可以知道幸冻,num這個變量的作用域是在它定義的時候決定了它是一個全局變量粹庞,所以即使在函數(shù)中執(zhí)行num的操作,我們在函數(shù)外部依然可以獲取到這個值
注意一個細(xì)節(jié)洽损,獲取全局變量比獲取局變量要耗費性能庞溜,舉個例子:
function (obj){
var i=0,
l=obj.length;
for(;i<l;i++){}
}
在這段代碼里,我們獲取設(shè)置局部變量obj.length用來進(jìn)行循環(huán)條件判斷碑定,使用i<l是一個比i<obj.length更佳的選擇流码,因為如果obj包含大量的代碼,我們在循環(huán)時每一次都要重新獲取obj的值延刘,而設(shè)置l則可以降低這部分的性能損耗
閉包
在JS中只有function能形成塊級作用域漫试,在函數(shù)內(nèi)部的變量外部是無法獲取的,而在函數(shù)中嵌套函數(shù)碘赖,被嵌套的函數(shù)則可以拿到外層函數(shù)的變量的值驾荣,所以有的時候我們需要拿到一個函數(shù)的內(nèi)部數(shù)據(jù)時外构,可以采用如下的方法:
function A(){
var name="tom"
function B(){
return name
}
return B();
}
console.log(A());//tom
以上的代碼就是我們常說的閉包,官方的閉包的解釋十分的概念化秘车,我對閉包的理解就是:
- 初步的理解:能夠獲取其他函數(shù)的內(nèi)部數(shù)據(jù)的函數(shù)
- 深入的理解:函數(shù)記住并訪問其所在的詞法作用域典勇,叫作閉包現(xiàn)象,而此時函數(shù)對作用域的引用叫作閉包(閉包就是引用叮趴,維基上有一段對閉包的引用解釋的我覺的是比較好:引用了自由變量的函數(shù)割笙,自由變量和函數(shù)將一同存在),如果想要理解這一塊眯亦,我們需要對JavaScript中的
垃圾回收機(jī)制
和JavaScript的存儲機(jī)制
做一些了解
JavaScript的垃圾回收機(jī)制
垃圾回收的主要工作是跟蹤內(nèi)存的分配和使用伤溉,在內(nèi)存不再工作時,將其進(jìn)行釋放
在垃圾回收機(jī)制中妻率,涉及到的算法比較多乱顾,所以也不打算深入,只做簡單的了解即可宫静,我們只需要知道:
在js中走净,在創(chuàng)建變量或函數(shù)時,會開辟空間孤里,有一個屬性來標(biāo)注它們是否被其它變量伏伯,被引用則計數(shù)器++,
- 如果函數(shù)被調(diào)用過了捌袜,并且以后不會再用到说搅,此時判斷計數(shù)器為0,那么垃圾回收機(jī)制就會將其作用域進(jìn)行銷毀
- 全局變量是不會被銷毀的
JavaScript的存儲機(jī)制
我們都知道虏等,在JS的存儲中是分為堆存儲和棧存儲的弄唧,堆存儲是用來存儲對象,也就是引用數(shù)據(jù)類型霍衫,而棧存儲是用來存儲簡單數(shù)據(jù)類型候引,在這里我們需要了解的是:
- 對象的引用是通過地址來傳遞的,一個對象應(yīng)用另一個對象后只是它的地址指向發(fā)生了改變敦跌,而它原來的值是依然存在的
- 簡單數(shù)據(jù)類型的引用是直接在棧中把原先數(shù)據(jù)進(jìn)行替換的
了解了以上兩個概念我們可以再回頭看一下關(guān)于深入的理解閉包背伴,如果一個函數(shù)被引用,那么它的作用域就不會被垃圾回收機(jī)制進(jìn)行銷毀峰髓,這就可以理解為記住了這個作用域傻寂,如果對其內(nèi)部的變量進(jìn)行訪問,那么稱為訪問
<script type="text/javascript" language="javascript">
function a(){
var i=0;
return function b() {
alert(++i);//記住并訪問
}
}
c=a();//引用
c();
</script>
由于比包內(nèi)的函數(shù)對外部函數(shù)的變量進(jìn)行了引用携兵,在垃圾回收機(jī)制看來疾掰,引用計數(shù)不為0,所以在函數(shù)的作用域被銷毀后徐紧,閉包內(nèi)的變量會一直存在静檬,這也是閉包的缺點炭懊,大量的閉包引用如果在引用后沒有賦值為null會占用大量的內(nèi)存空間,在IE低版本中會導(dǎo)致內(nèi)存泄漏
閉包還有一個特性是閉包只能氣的包含函數(shù)中任何變量的最后一個值拂檩,例如一些常見的面試題中經(jīng)常出現(xiàn)這個問題
function person(){
var arr=[];
for(var i=0;i<10;i++){
arr[i]=function (){
return i;
};
};
return arr;
}
console.log(person()[1]());//10
在上面的代碼中arr的每個位置存儲的都是10侮腹,如果我們想要使每個返回不同的數(shù)字那么我們需要對代碼進(jìn)行改進(jìn)
function person(){
var arr=[];
for(var i=0;i<10;i++){
arr[i]=function (num){
return function(){//再次添加一個閉包,在這個閉包內(nèi)將每次i的值進(jìn)行存儲
return num;
};
}(i)
};
return arr;
}
console.log(person()[1]());//10
閉包的用途
獲取其它函數(shù)內(nèi)部的變量的值
-
緩存變量的值
function A(){ var i=1; return function(){ i++; console.log(i); } } var a=A(); a(); a(); a(); //這里我們要探討一個問題稻励,閉包緩存數(shù)據(jù)的形式是什么父阻,之前一直對閉包的概念有誤解,一直以為A事必報望抽,但是實際上 // return function(){ // i++; // console.log(i); // } //這部分才是真正的閉包加矛,所以能夠進(jìn)行緩存的是這一部分,這也就解決了之前的一個疑惑煤篙,為什么需要寫 // var a=A();這一步再調(diào)用a()斟览;才能看到閉包緩存的數(shù)據(jù) //我們可以根據(jù)這個特性座椅計時器,來記錄一個構(gòu)造函數(shù)創(chuàng)建了多少個對象 var C=function(){ var i=0; return function(){ return ++i } }();//讓函數(shù)自調(diào)用自身 function fn(name){ if(this==window){//判斷當(dāng)前調(diào)用的是不是window對象 return } this.name=name; fn.cn=C();//注意辑奈,在這里實際上已經(jīng)相當(dāng)于直接使用++i為fn.cn賦值了 } var a=new fn("tom"); var b=new fn("son"); var c=new fn("xm"); console.log(fn.cn)//3
-
設(shè)置變量的可讀可寫(封裝)
function A(_name){ var name=_name; return { getName:function(){//可讀 return name; }, setName:function(newName){//可寫 name=newName; } } } var a=new A("tom"); console.log(a.getName())//tom a.setName("xm"); console.log(a.getName())//xm //還有一個寫法 function A(_name,_age){ var name=_name, age=_age; return { name:function(value){ if(value===undefined) return name; else name=value; }, age:function(value){ if(value===undefined) return age; else age=value; } } } var a=new A("tom",18); console.log(a.name());//tom a.name("xm"); console.log(a.name());//xm
-
沙箱模式
(function(){})();//防止代碼污染 //這里我們需要提到一個概念苛茂,也就是JS中的依賴注入,和AngularJS中的類似的概念鸠窗,JS中的依賴注入也就是表示妓羊,在JS中如果需要調(diào)用某種我們已經(jīng)封裝好的,或者外部的JS庫塌鸯,那么我們可以提前聲明 1.在JS中侍瑟,如果讓JS自己解析唐片,尋找我們定義的變量丙猬,這個過程的效率很低,無形中會損耗一部分性能费韭,如果我們以實參的方式傳入茧球,那么會減少這部分額性能損耗 (function(window){ })(window) 2.提前聲明我們使用了什么庫,便于代碼的維護(hù)和可讀性星持,保障代碼的健壯性 (function($){ })($)
-
提前返回
var addEventlistner=function (){ if(window.addEventlistner){ return function(elem,type,calback,bool){ elem.addEventlistner("type",calback抢埋,bool); }; }else{ return function(elem,type,calback){ elem.attachEvent("on"+type, calback) }; } }(); //在之前我們進(jìn)行能力檢測的時候,每次調(diào)用都會進(jìn)行if..else..的判斷督暂,其實在我們進(jìn)入瀏覽器的時候揪垄,瀏覽器的能力已經(jīng)確定了,所以我們可以通過利用閉包的提前返回的特性逻翁,把能力檢測的結(jié)果保存下來饥努,不需要每次都進(jìn)行判斷