JavaScript從做用域到閉包

目錄javascript

做用域html

  全局做用域和局部做用域前端

  塊做用域與函數做用域java

  做用域中的聲明提早chrome

做用域鏈瀏覽器

函數聲明與賦值閉包

  聲明式函數、賦值式函數與匿名函數ide

  代碼塊  函數

  自執行函數工具

閉包 


 

做用域(scope)

全局做用域和局部做用域

一般來說這塊是全局變量與局部變量的區分。 參考引文:JavaScript 開發進階:理解 JavaScript 做用域和做用域鏈

全局做用域:最外層函數和在最外層函數外面定義的變量擁有全局做用域。

  1)最外層函數和在最外層函數外面定義的變量擁有全局做用域

  2)全部末定義直接賦值的變量自動聲明爲擁有全局做用域,即沒有用var聲明的變量都是全局變量,並且是頂層對象的屬性。

  3)全部window對象的屬性擁有全局做用域

局部做用域:和全局做用域相反,局部做用域通常只在固定的代碼片斷內可訪問到,最多見的例如函數內部,因此在一些地方也會看到有人把這種做用域稱爲函數做用域。

代碼部分請參照引文。

 

塊做用域與函數做用域

函數做用域是相對塊做用域來進行解釋的,其和局部做用域是一個意思。參考引文:JavaScript的做用域和塊級做用域概念理解

塊做用域:任何一對花括號{}中的語句集都屬於一個塊,在這之中定義的全部變量在代碼塊外都是無效的,咱們稱之爲塊級做用域。

函數做用域:在函數中的參數和變量在函數外部是沒法訪問的。JavaScript 的做用域是詞法性質的(lexically scoped)。這意味着,函數運行在定義它的做用域中,而不是在調用它的做用域中。下文會解釋。

 1 //C語言 
 2 #include <stdio.h> 
 3 void main() 
 4 { 
 5 int i=2; 
 6 i--; 
 7 if(i) 
 8 { 
 9 int j=3; 
10 } 
11 printf("%d/n",j); 
12 }
View Code

運行這段代碼,會出現「use an undefined variable:j」的錯誤。能夠看到,C語言擁有塊級做用域,由於j是在if的語句塊中定義的,所以,它在塊外是沒法訪問的。

1 function test(){ 
2         for(var i=0;i<3;i++){};    
3         alert(i); 
4         } 
5         test();
View Code

運行這段代碼,彈出"3",可見,在塊外,塊中定義的變量i仍然是能夠訪問的。也就是說,JS並不支持塊級做用域,它只支持函數做用域,並且在一個函數中的任何位置定義的變量在該函數中的任何地方都是可見的。

 

做用域中的聲明提早

var scope="global";  //全局變量
function t(){  
    console.log(scope);  
    var scope="local" ;//局部變量
    console.log(scope);  
            }  
t();

 

(console.log()是控制檯的調試工具,chrome叫檢查,有的瀏覽器叫審查元素,alert()彈窗會破壞頁面效果)

第一句輸出的是: "undefined",而不是 "global"

第二講輸出的是:"local"

第二個不用說,就是局部變量輸出"local"。第一個之因此也是"local",是由於Js中的聲明提早,儘管在第4行才進行局部變量的聲明與賦值,但實際上是將第4行的聲明提早了,放在了函數體頂部,而後在第4行進行局部變量的賦值。能夠理解爲下面這樣。

var scope="global";//全局變量
function t(){
    var scope;//局部變量聲明
    console.log(scope);
    scope="local";//局部變量賦值
    console.log(scope);
}
t();

 

具體細節能夠查閱犀牛書(《JavaScript權威指南》)中的詳細介紹。

 

做用域鏈(Scope Chain)

當代碼在一個環境中執行時,會建立變量對象的的一個做用域鏈(scope chain)。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是一個函數,則將其活動對象做爲變量對象。參考引文:Js做用域與做用域鏈詳解,淺析做用域鏈–JS基礎核心之一

num="one";
var a = 1;  
function t(){  //t函數的局部做用域,能夠訪問到a,b變量,可是訪問不到c變量
     var num="two"; 
     var b = 2;
    function A(){ //A函數局部做用域,能夠訪問到a,b,c變量 
        var num="three"; //局部變量與外部變量重名以局部變量爲主
        var c = 3;
        console.log(num); //three 
            }  
    function B(){  //B函數局部做用域,能夠訪問到a,b變量,訪問不到c變量
        console.log(num); //two 
            }  
    A();  
    B();  
}  
t();

當執行A時,將建立函數A的執行環境(調用對象),並將該對象置於鏈表開頭,而後將函數t的調用對象連接在以後,最後是全局對象。而後從鏈表開頭尋找變量num。

即:A()->t()->window,因此num是」three";

但執行B()時,做用域鏈是: B()->t()->window,因此num是」two";

另外,有一個特殊的例子我以爲應該發一下。利用「JavaScript 的做用域是詞法性質的(lexically scoped)。這意味着,函數運行在定義它的做用域中,而不是在調用它的做用域中。」 這句話,解釋了下面的例子。

var x = 10;

function a() {
console.log(x);
}

function b () {
var x = 5;
a();
}

b();//輸出爲10

雖然b函數調用了a,可是a定義在全局做用域下,一樣也是運行在全局做用域下的,因此其內部的變量x,向上尋找到了全局變量x=10;因此b函數的輸出爲10;

更深層次的講解請參照:JavaScript 開發進階:理解 JavaScript 做用域和做用域鏈

經典案例

下面是一個經典的事件綁定例子:

<div id = "test">
    <p>欄目1</p>
    <p>欄目2</p>
    <p>欄目3</p>
    <p>欄目4</p>
</div>
 </body>
<script type="text/javascript">    
function bindClick(){
    var allP = document.getElementById("test").getElementsByTagName("p"),
    i=0,
    len = allP.length;        
    for( ;i<len;i++){
    allP[i].onclick = function(){
        alert("you click the "+i+" P tag!");//you click the 4 P tag!    
    }
    }
}
bindClick();//運行函數,綁定點擊事件
</script>

上面的代碼給P標籤添加點擊事件,可是無論咱們點擊哪個p標籤,咱們獲取到的結果都是「you click the 4 P tag!」。

咱們能夠把上述的JS代碼給分解一下,讓咱們看起來更容易理解,以下所示。前面使用一個匿名函數做爲click事件的回調函數,這裏使用的一個非匿名函數,做爲回調,徹底相同的效果。

function bindClick(){
    var allP = document.getElementById("test").getElementsByTagName("p"),
    i=0,
    len = allP.length;
    for( ;i<len;i++){
    allP[i].onclick = AlertP;
    }
    function AlertP(){
    alert("you click the "+i+" P tag!");
    }
}
bindClick();//運行函數,綁定點擊事件

這裏應該沒有什麼問題吧,前面使用一個匿名函數做爲click事件的回調函數,這裏使用的一個非匿名函數,做爲回調,徹底相同的效果。也能夠作下測試哦。

理解上面的說法了,那麼就能夠很簡單的理解,爲何咱們以前的代碼,會獲得一個相同的結果了。首先看一下for循環中,這裏咱們只是對每個匹配的元素添加了一個click的回調函數,而且回調函數都是AlertP函數。這裏當爲每個元素添加成功click以後,i的值,就變成了匹配元素的個數,也就是i=len,而當咱們觸發這個事件時,也就是當咱們點擊相應的元素時,咱們期待的是,提示出咱們點擊的元素是排列在第幾行。click事件觸發時,執行回調函數AlertP可是當執行到這裏的時候,發現alert方法中,有一個變量是未知的,而且在AlertP的局部做用域中,也沒有查找到相應的變量,那麼按照做用域鏈的查找方式,就會向父級做用域去查找,這裏的父級做用域中,確實是有變量i的,而i的值,倒是通過for循環以後的值i=len。因此也就出現了咱們最初看到的效果。

解決辦法以下所示:

function bindClick(){
    var allP = document.getElementById("test").getElementsByTagName("p"),
  i
=0,
  len
= allP.length; for( ;i<len;i++){ AlertP(allP[i],i); } function AlertP(obj,i){ obj.onclick = function(){ alert("you click the "+i+" P tag!"); } } } bindClick();

這裏,objiAlertP函數內部,就是局部變量了。click事件的回調函數,雖然依舊沒有變量i的值,可是其父做用域AlertP的內部,倒是有的,因此能正常的顯示了,這裏AlertP我放在了bindClick的內部,只是由於這樣能夠減小必要的全局函數,放到全局也不影響的。

這裏是添加了一個函數進行綁定,若是我不想添加函數呢,固然也能夠實現了,這裏就要說到自執行函數了。能夠跳到本文的自執行函數,也能夠看參考引文的深度講解:淺析做用域鏈–JS基礎核心之一

 

函數聲明與賦值

聲明式函數、賦值式函數與匿名函數

匿名函數:function () {}; 使用function關鍵字聲明一個函數,但未給函數命名,因此叫匿名函數,匿名函數有不少做用,賦予一個變量則建立函數,賦予一個事件則成爲事件處理程序或建立閉包等等。下文會講到。

JS中的函數定義分爲兩種:聲明式函數與賦值式函數。

<script type="text/javascript">
Fn(); //執行結果:"執行了聲明式函數",在預編譯期聲明函數及被處理了,因此即便Fn()調用函數放在聲明函數前也能執行。
function Fn(){ //聲明式函數
alert("執行了聲明式函數");
}
</script>
<script type="text/javascript">
Fn(); //執行結果:"Fn is not a function"
var Fn = function(){ //賦值式函數
alert("執行了賦值式函數");
}
</script>

JS的解析過程分爲兩個階段:預編譯期(預處理)與執行期。
預編譯期JS會對本代碼塊中的全部聲明的變量和函數進行處理(相似與C語言的編譯),此時處理函數的只是聲明式函數,並且變量也只是進行了聲明(聲明提早)但未進行初始化以及賦值。因此纔會出現上面兩種狀況。

當正常狀況,函數調用在聲明以後,同名函數會覆蓋前者。

<script type="text/javascript">
function Fn(){ //聲明式函數
alert("執行了聲明式函數");
}
var Fn = function(){ //賦值式函數
alert("執行了賦值式函數");
}
Fn();//執行結果:"執行了賦值式函數",同名函數後者會覆蓋前者
</script>

 同理當提早調用聲明函數時,也存在同名函數覆蓋的狀況。

<script type="text/javascript">
Fn(); //執行結果:"執行了函數2",同名函數後者會覆蓋前者
function Fn(){ //函數1
alert("執行了函數1");
}
function Fn(){ //函數2
alert("執行了函數2");
}
</script> 

 

代碼塊

JavaScript中的代碼塊是指由<script>標籤分割的代碼段。JS是按照代碼塊來進行編譯和執行的,代碼塊間相互獨立,但變量和方法共享。以下:

<script type="text/javascript">//代碼塊一
var test1 = "我是代碼塊一test1";
alert(str);//由於沒有定義str,因此瀏覽器會出錯,下面的不能運行
alert("我是代碼塊一");//沒有運行到這裏
var test2 = "我是代碼塊一test2";//沒有運行到這裏可是預編譯環節聲明提早了,因此有變量可是沒賦值
</script>
<script type="text/javascript">//代碼塊二
alert("我是代碼塊二"); //這裏有運行到
alert(test1); //彈出"我是代碼塊一test1"
alert(test2); //彈出"undefined"
</script>

上面的代碼中代碼塊一中運行報錯,但不影響代碼塊二的執行,這就是代碼塊間的獨立性,而代碼塊二中能調用到代碼一中的變量,則是塊間共享性。

可是當第一個代碼塊報錯中止後,並不影響下一個代碼塊運行。固然在下面的例子中,雖然代碼塊二中的函數聲明預編譯了,可是在代碼塊1中的函數出現Fn函數爲定義錯誤(瀏覽器報錯,並非聲明未賦值的undefined),說明代碼塊1徹底執行後才執行代碼塊2。

<script type="text/javascript">//代碼塊1
Fn(); //瀏覽器報錯:"undefined",中止代碼塊1運行
alert("執行了代碼塊1");//未運行
</script>
<script type="text/javascript">//代碼塊2
alert("執行了代碼塊2");//執行彈框效果
function Fn(){ //函數1
alert("執行了函數1");
}
</script>
因此js函數解析順序以下:
  step 1. 讀入第一個代碼塊。
  step 2. 作語法分析,有錯則報語法錯誤(好比括號不匹配等),並跳轉到step5。
  step 3. 對var變量和function定義作「預編譯處理」(永遠不會報錯的,由於只解析正確的聲明)。
  step 4. 執行代碼段,有錯則報錯(好比變量未定義)。
  step 5. 若是還有下一個代碼段,則讀入下一個代碼段,重複step2。
  step6. 結束。
:須要在頁面元素渲染前執行的js代碼應該放在<body>前面的<script>代 碼塊中,而須要在頁面元素加載完後的js放在</body>元素後面,body標籤的onload事件是在最後執行的。
<script type="text/javascript">
alert("first");
function Fn(){
alert("third");
}
</script>
<body onload="Fn()">
</body>
<script type="text/javascript">
alert("second");
</script>

 

自執行函數

也就是在函數名後添加括號,函數就會自執行。在綁定事件時,像我這樣的初學者有時會犯以下的錯誤,window.onclick = ab();這樣函數ab一開始就會執行。正確的作法應該將ab後的括號去掉。而這種加括號的作法實際上是把ab函數運行的結果賦值給點擊事件。

下面兩個例子清楚地反映了函數賦值後的狀況。

1:

function ab () {
    var i=0;
    alert("ab");
    return i;
}
var c=ab();//執行ab函數
alert(typeof c+"      "+c);//number  0

2:

function ab () {
    var i=0;
    alert("ab");
    return i;
}
var c=ab;//只賦值
alert(typeof c+"      "+c);//function  function ab () {var i=0;alert("ab");return i;}

注:可是這個函數必須是函數表達式(諸如上文提到的賦值式函數),不能是函數聲明。詳細請看:js當即執行函數:(function(){...})()與(function(){...}())

文中主要講到匿名函數的自執行方法,即在function前面加!、+、 -甚至是逗號等到均可以起到函數定義後當即執行的效果,而()、!、+、-、=等運算符,都將函數聲明轉換成函數表達式,消除了javascript引擎識別函數表達式和函數聲明的歧義,告訴javascript引擎這是一個函數表達式,不是函數聲明,能夠在後面加括號,並當即執行函數的代碼(jq使用的就是這種方法)。舉例以下所示。

(function(a){
    console.log(a);   //firebug輸出123,使用()運算符
})(123);
  
(function(a){
    console.log(a);   //firebug輸出1234,使用()運算符
}(1234));
  
!function(a){
    console.log(a);   //firebug輸出12345,使用!運算符
}(12345);
  
+function(a){
    console.log(a);   //firebug輸出123456,使用+運算符
}(123456);
  
-function(a){
    console.log(a);   //firebug輸出1234567,使用-運算符
}(1234567);
  
var fn=function(a){
    console.log(a);   //firebug輸出12345678,使用=運算符
}(12345678)

其做用就是:實現塊做用域。

javascript中沒用私有做用域的概念,若是在多人開發的項目上,你在全局或局部做用域中聲明瞭一些變量,可能會被其餘人不當心用同名的變量給覆蓋掉,根據javascript函數做用域鏈的特性,使用這種技術能夠模仿一個私有做用域,用匿名函數做爲一個「容器」,「容器」內部能夠訪問外部的變量,而外部環境不能訪問「容器」內部的變量,因此( function(){…} )()內部定義的變量不會和外部的變量發生衝突,俗稱「匿名包裹器」或「命名空間」。代碼以下:

function test(){ 
(function (){ 
for(var i=0;i<4;i++){ 
} 
})(); 
alert(i); //瀏覽器錯誤:i is not defined
} 
test();

 能夠對比最開始介紹做用域時候的代碼。

 

閉包(Closure)

閉包對於初學者來講很難,須要學習不少不少才能領會,因此也是先把做用域鏈匿名函數的知識做爲鋪墊。我這裏的閉包內容屬於基礎篇,之後可能會貼一些更爲核心的內容。我這裏參照了大神們的講解來講。參考引文:學習Javascript閉包(Closure),JavaScript 匿名函數(anonymous function)與閉包(closure),淺析做用域鏈–JS基礎核心之一

閉包是可以讀取其餘函數內部變量的函數,因此在本質上,閉包將函數內部和函數外部鏈接起來的一座橋樑。

閉包是在函數執行結束,做用域鏈將函數彈出以後,函數內部的一些變量或者方法,還能夠經過其餘的方法引用。

兩個用處:一個是能夠讀取函數內部的變量,另外一個就是讓這些變量的值始終保持在內存中。

爲了幫助理解,我找了幾個例子:

1.(阮一峯老師的講解)

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

在這段代碼中,result實際上就是閉包f2函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證實了,函數f1中的局部變量n一直保存在內存中,並無在f1調用後被自動清除。

爲何會這樣呢?緣由就在於f1是f2的父函數,而f2被賦給了一個全局變量,這致使f2始終在內存中,而f2的存在依賴於f1,所以f1也始終在內存中,不會在調用結束後,被垃圾回收機制(garbage collection)回收。

這段代碼中另外一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,所以nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數(anonymous function),而這個匿名函數自己也是一個閉包,因此nAdd至關因而一個setter,能夠在函數外部對函數內部的局部變量進行操做。

2.(某大神)

function foo() { 
var a = 10; 
function bar() { 
a *= 2; 
return a; 
} 
return bar; 
} 
var baz = foo(); 
alert(baz()); //20
alert(baz()); //40    
alert(baz()); //80

var blat = foo(); 
alert(blat()); //20

如今能夠從外部訪問 a; 
a 是運行在定義它的 foo 中,而不是運行在調用 foo 的做用域中。 只要 bar 被定義在 foo 中,它就能訪問 foo 中定義的變量 a,即便 foo 的執行已經結束。也就是說,按理,"var baz = foo()" 執行後,foo 已經執行結束,a 應該不存在了,但以後再調用 baz 發現,a 依然存在。這就是 JavaScript 特點之一——運行在定義,而不是運行的調用。 
其中, "var baz = foo()" 是一個 bar 函數的引用;"var blat= foo()" 是另外一個 bar 函數引用。 
用閉包還可實現私有成員,可是我還沒理解,因此就先不貼出來,想看的請參照參考引文:JavaScript 匿名函數(anonymous function)與閉包(closure)

 

結束

第一次寫這麼長的文章,大部分是引用,可是全部內容都是親自實踐並思考後才貼出來,做爲初學者可能有解釋和引用不當的地方,還請你們指出。有問題的地方還請各位老師同窗多來指教探討。

再次感謝全部引文做者,知識的增加在於傳播,感謝辛苦的傳播者。

 

參考文獻:

JavaScript 開發進階:理解 JavaScript 做用域和做用域鏈,

JavaScript的做用域和塊級做用域概念理解,

Js做用域與做用域鏈詳解,

淺析做用域鏈–JS基礎核心之一,

javascript 匿名函數的理解(透徹版),

JS中函數執行順序的問題,

js當即執行函數:(function(){...})()與(function(){...}()), 

學習Javascript閉包(Closure),

JavaScript 匿名函數(anonymous function)與閉包(closure)

相關文章
相關標籤/搜索