如果要了解閉包,我們需要先了解閉包的由來,閉包的產(chǎn)生,源于JS的詞法作用域
詞法作用域
作用域是指一個(gè) 變量能夠訪問到的區(qū)域 例如我們?cè)O(shè)置一個(gè)var n=0;實(shí)際分兩步,聲明和賦值,var n;n=1;在js中只有函數(shù)能夠限定作用域,除此之外聲明的都是全局作用域,在ES6的新標(biāo)準(zhǔn)中,采用了let和const來限定局部變量,注意局部變量的優(yōu)先級(jí)是高于全局變量的
執(zhí)行環(huán)境是JavaScript中最為重要的一個(gè)概念,在js代碼開始執(zhí)行的時(shí)候,會(huì)創(chuàng)建一個(gè)匿名函數(shù)的執(zhí)行環(huán)境,執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其它數(shù)據(jù),每個(gè)執(zhí)行環(huán)境都有一個(gè)與之相關(guān)的變量對(duì)象,環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對(duì)象中,雖然我們編寫的代碼無法訪問這個(gè)對(duì)象,但解析器咋及處理數(shù)據(jù)的時(shí)候會(huì)在后臺(tái)使用它
變量的作用域是在定義的時(shí)候決定的而不是在執(zhí)行的時(shí)候決定的,變量只能在本層作用域和上層作用域生效,不能在下層作用域生效
舉一個(gè)栗子來加深理解
var num;
function f1(){
num=3;
}
f1();
console.log(num);//3
在這個(gè)栗子中,num的值在全局最開始設(shè)置的是undefined,但是在函數(shù)f1中改變了該值,這個(gè)值變?yōu)?,在函數(shù)f1執(zhí)行了一次之后我們?cè)俅蛴um的值,得到的是num=3,通過解讀JavaScript是詞法作用域我們可以知道,num這個(gè)變量的作用域是在它定義的時(shí)候決定了它是一個(gè)全局變量,所以即使在函數(shù)中執(zhí)行num的操作,我們?cè)诤瘮?shù)外部依然可以獲取到這個(gè)值
注意一個(gè)細(xì)節(jié),獲取全局變量比獲取局變量要耗費(fèi)性能,舉個(gè)例子:
function (obj){
var i=0,
l=obj.length;
for(;i<l;i++){}
}
在這段代碼里,我們獲取設(shè)置局部變量obj.length用來進(jìn)行循環(huán)條件判斷,使用i<l是一個(gè)比i<obj.length更佳的選擇,因?yàn)槿绻鹢bj包含大量的代碼,我們?cè)谘h(huán)時(shí)每一次都要重新獲取obj的值,而設(shè)置l則可以降低這部分的性能損耗
閉包
在JS中只有function能形成塊級(jí)作用域,在函數(shù)內(nèi)部的變量外部是無法獲取的,而在函數(shù)中嵌套函數(shù),被嵌套的函數(shù)則可以拿到外層函數(shù)的變量的值,所以有的時(shí)候我們需要拿到一個(gè)函數(shù)的內(nèi)部數(shù)據(jù)時(shí),可以采用如下的方法:
function A(){
var name="tom"
function B(){
return name
}
return B();
}
console.log(A());//tom
以上的代碼就是我們常說的閉包,官方的閉包的解釋十分的概念化,我對(duì)閉包的理解就是:
- 初步的理解:能夠獲取其他函數(shù)的內(nèi)部數(shù)據(jù)的函數(shù)
- 深入的理解:函數(shù)記住并訪問其所在的詞法作用域,叫作閉包現(xiàn)象,而此時(shí)函數(shù)對(duì)作用域的引用叫作閉包(閉包就是引用,維基上有一段對(duì)閉包的引用解釋的我覺的是比較好:引用了自由變量的函數(shù),自由變量和函數(shù)將一同存在),如果想要理解這一塊,我們需要對(duì)JavaScript中的
垃圾回收機(jī)制和JavaScript的存儲(chǔ)機(jī)制做一些了解
JavaScript的垃圾回收機(jī)制
垃圾回收的主要工作是跟蹤內(nèi)存的分配和使用,在內(nèi)存不再工作時(shí),將其進(jìn)行釋放
在垃圾回收機(jī)制中,涉及到的算法比較多,所以也不打算深入,只做簡(jiǎn)單的了解即可,我們只需要知道:
在js中,在創(chuàng)建變量或函數(shù)時(shí),會(huì)開辟空間,有一個(gè)屬性來標(biāo)注它們是否被其它變量,被引用則計(jì)數(shù)器++,
- 如果函數(shù)被調(diào)用過了,并且以后不會(huì)再用到,此時(shí)判斷計(jì)數(shù)器為0,那么垃圾回收機(jī)制就會(huì)將其作用域進(jìn)行銷毀
- 全局變量是不會(huì)被銷毀的
JavaScript的存儲(chǔ)機(jī)制
我們都知道,在JS的存儲(chǔ)中是分為堆存儲(chǔ)和棧存儲(chǔ)的,堆存儲(chǔ)是用來存儲(chǔ)對(duì)象,也就是引用數(shù)據(jù)類型,而棧存儲(chǔ)是用來存儲(chǔ)簡(jiǎn)單數(shù)據(jù)類型,在這里我們需要了解的是:
- 對(duì)象的引用是通過地址來傳遞的,一個(gè)對(duì)象應(yīng)用另一個(gè)對(duì)象后只是它的地址指向發(fā)生了改變,而它原來的值是依然存在的
- 簡(jiǎn)單數(shù)據(jù)類型的引用是直接在棧中把原先數(shù)據(jù)進(jìn)行替換的
了解了以上兩個(gè)概念我們可以再回頭看一下關(guān)于深入的理解閉包,如果一個(gè)函數(shù)被引用,那么它的作用域就不會(huì)被垃圾回收機(jī)制進(jìn)行銷毀,這就可以理解為記住了這個(gè)作用域,如果對(duì)其內(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ù)對(duì)外部函數(shù)的變量進(jìn)行了引用,在垃圾回收機(jī)制看來,引用計(jì)數(shù)不為0,所以在函數(shù)的作用域被銷毀后,閉包內(nèi)的變量會(huì)一直存在,這也是閉包的缺點(diǎn),大量的閉包引用如果在引用后沒有賦值為null會(huì)占用大量的內(nèi)存空間,在IE低版本中會(huì)導(dǎo)致內(nèi)存泄漏
閉包還有一個(gè)特性是閉包只能氣的包含函數(shù)中任何變量的最后一個(gè)值,例如一些常見的面試題中經(jīng)常出現(xiàn)這個(gè)問題
function person(){
var arr=[];
for(var i=0;i<10;i++){
arr[i]=function (){
return i;
};
};
return arr;
}
console.log(person()[1]());//10
在上面的代碼中arr的每個(gè)位置存儲(chǔ)的都是10,如果我們想要使每個(gè)返回不同的數(shù)字那么我們需要對(duì)代碼進(jìn)行改進(jìn)
function person(){
var arr=[];
for(var i=0;i<10;i++){
arr[i]=function (num){
return function(){//再次添加一個(gè)閉包,在這個(gè)閉包內(nèi)將每次i的值進(jìn)行存儲(chǔ)
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(); //這里我們要探討一個(gè)問題,閉包緩存數(shù)據(jù)的形式是什么,之前一直對(duì)閉包的概念有誤解,一直以為A事必報(bào),但是實(shí)際上 // return function(){ // i++; // console.log(i); // } //這部分才是真正的閉包,所以能夠進(jìn)行緩存的是這一部分,這也就解決了之前的一個(gè)疑惑,為什么需要寫 // var a=A();這一步再調(diào)用a();才能看到閉包緩存的數(shù)據(jù) //我們可以根據(jù)這個(gè)特性座椅計(jì)時(shí)器,來記錄一個(gè)構(gòu)造函數(shù)創(chuàng)建了多少個(gè)對(duì)象 var C=function(){ var i=0; return function(){ return ++i } }();//讓函數(shù)自調(diào)用自身 function fn(name){ if(this==window){//判斷當(dāng)前調(diào)用的是不是window對(duì)象 return } this.name=name; fn.cn=C();//注意,在這里實(shí)際上已經(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 //還有一個(gè)寫法 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(){})();//防止代碼污染 //這里我們需要提到一個(gè)概念,也就是JS中的依賴注入,和AngularJS中的類似的概念,JS中的依賴注入也就是表示,在JS中如果需要調(diào)用某種我們已經(jīng)封裝好的,或者外部的JS庫,那么我們可以提前聲明 1.在JS中,如果讓JS自己解析,尋找我們定義的變量,這個(gè)過程的效率很低,無形中會(huì)損耗一部分性能,如果我們以實(shí)參的方式傳入,那么會(huì)減少這部分額性能損耗 (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)行能力檢測(cè)的時(shí)候,每次調(diào)用都會(huì)進(jìn)行if..else..的判斷,其實(shí)在我們進(jìn)入瀏覽器的時(shí)候,瀏覽器的能力已經(jīng)確定了,所以我們可以通過利用閉包的提前返回的特性,把能力檢測(cè)的結(jié)果保存下來,不需要每次都進(jìn)行判斷