目錄
作用域(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 }
運行這段代碼,會出現“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();
運行這段代碼,彈出"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();
這里,obj
和i
在AlertP
函數內部,就是局部變量了。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>
step 1. 讀入第一個代碼塊。
step 2. 做語法分析,有錯則報語法錯誤(比如括號不匹配等),並跳轉到step5。
step 3. 對var變量和function定義做“預編譯處理”(永遠不會報錯的,因為只解析正確的聲明)。
step 4. 執行代碼段,有錯則報錯(比如變量未定義)。
step 5. 如果還有下一個代碼段,則讀入下一個代碼段,重復step2。
step6. 結束。
<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 作用域和作用域鏈,
js立即執行函數:(function(){...})()與(function(){...}()),
JavaScript 匿名函數(anonymous function)與閉包(closure)