### 作用域
在深入學習JavaScript作用域之前,首先要了解一下,究竟什么是作用域。幾乎所有的編程語言都有作用域的概念,簡單的說,作用域就是變量與函數的可訪問范圍,即作用域控制著變量與函數的可見性和生命周期。
我們先了解一下JavaScript的工作原理,引擎,編譯器,作用域三者是如何協同工作來完成javascript代碼的執行的。
**引擎**:從頭到尾負責整個JavaScript程序的編譯及執行過程。
**編譯器**:負責詞法分析及代碼生成
**作用域**:負責收集并維護由所有聲明的變量組成的一系列查詢,并實施一套非常嚴格的規則,確定當前執行的代碼對這些變量的訪問權限。
我們看下最簡單的var index = 10;了解一下引擎、編譯器和作用域是如何協同工作的。? ??
JS會將其看成是兩個聲明,第一個是定義聲明:編譯器在編譯階段執行。第二個是賦值聲明:由引擎在運行時執行。
因此可以分解為:
~~~
var index;
index = 10;
~~~
首先遇到var index,"編譯器"會詢問"作用域":當前的作用域中是否有index,如果是,那么"編譯器"會忽略這個聲明,繼續進行編譯;如果否,那么它會要求“作用域”在當前的作用域聲明一個新的變量,并命名為index.
然后,"引擎"處理index = 10時,首先會詢問"作用域":當前的作用域中是否存在一個index的變量,如果是,那么引擎就會使用這個變量,如果否,那么"引擎"會繼續查找該變量。如果"引擎"最終找到了index變量,那么就將10賦值給它,否則"引擎"就會拋出 一個異常(作用域鏈)。
總結一下變量賦值操作過程,即:首先編譯器會在當前作用域中聲明一個變量(如果之前沒有聲明過),然后在運行時,引擎會在作用域中查找該變量,如果能夠找到就對它賦值,否則就拋出異常。
此處需要注意的是:編譯階段是在當前的作用域中聲明變量,而引擎查找時,是在整個作用域中查找該變量。
在上面的變量聲明的執行時,我們提到了引擎在作用域中查找變量的問題,我們將此分為兩種方式,一種為LHS查詢,一種為RHS查詢,在上面的例子中,引擎在執行index = 10時,進行的是LHS查詢。
那么究竟何為LHS查詢和RHS查詢呢,簡單的,當變量出現在左側時,執行的是LHS查詢(L可以認為是Left)。除了LHS查詢,剩下的就是RHS查詢。作進一步說明,LHS查詢是企圖找到變量的容器本身,即去尋找賦值操作的目標,而RHS查詢是找賦值操作的源頭,即獲取變量的值。?
舉例說明:
~~~
index = 10;
console.log(index);
~~~
在執行index = 10時,引擎會進行LHS查詢,去尋找index變量的容器,目的是找到賦值操作的目標。而在執行console.log(index)時,引擎會進行RHS查詢,目的是去找到index的值。我們之所以進行執行的區分,是因為如果index變量沒有聲明的情況下,這兩種查詢方式的結果是完全不同的。
作用域嵌套引擎從當前的執行作用域開始查找變量,如果找不到,就向上一級繼續查找,直至到最外層的全局作用域鏈,不管最終是否找到了變量,查找過程到到此結束。
如下:在執行index = 15時,引擎首先在當前作用域fun函數中查找index變量,沒有找到,那么繼續向上查找,在par函數中也沒有找到,那么繼續向上一級查找,最后在全局作用域中找到了該index.
~~~
<script>
var index = 10;
function par(){
function fun(){
index = 15;
}
fun();
}
par();
</script>
~~~
當引擎進行RHS查詢時,如果查詢到作用域鏈的頂層(全局作用域)依舊未找到index變量,那么引擎就會拋出一個ReferenceError異常。
當引擎進行LHS查詢,在全局作用域中也未能找到目標變量(本例中的index),在非嚴格模式下,會在全局作用域中創建一個該名稱的變量。而在嚴格模式下,會同RHS查詢一樣,拋出一個ReferenceError異常。
作用域查找會在找到第一個匹配的標識符(變量)時停止,在多層嵌套作用域中可以定義同名的標識符,這也稱之為"遮蔽效應",如上面的代碼中,如果fun函數中定義了index變量,那么在fun中對index的賦值操作不會影響到全局變量中的index.因為作用域查找始終是從運行時所處的最內部的作用域開始,逐級向上查找,直到找到匹配的標識符為止。
詞法作用域是由寫代碼時將變量和函數寫在哪里決定的,而不是由其調用的位置決定,JS提供了兩種機制修改詞法作用域,即:width和eval,鑒于這兩種機制都會導致性能的降低,在此不多作介紹,盡量避免使用即可。
初學者或多或少都會遇到一個問題:命名沖突。
命名沖突會導致變量的值被意外覆蓋。而這并非是我們想看到的。那么如何規避沖突呢?
1.全局命名空間(類似于jQuery的實現)
如:我們在全局作用域重視聲明了一個名字足夠獨特的變量,通常是一個變量,如下面的carousel_yve,這個對象被稱為庫的命名空間,所有需要暴露給外界的功能都會稱為這個對象的屬性(如:index、defaults、init),避免將自己的標識符暴露在頂級的詞法作用域中。
~~~
<script type = "text/javascript">
var carousel_yve = {
index: 0,
defaults: { width: "1200px",
height: "500px"},
init: function(){
console.log(this.defaults);
}
}
carousel_yve.init(); //Object {width: "1200px", height: "500px"}
console.log(carousel_yve.index); //10
</script>
~~~
2.模塊模式
模塊模式分為兩種,一種是每次調用都會創建一個新的模塊實例,另一種是單例模式,即只會創建一個實例。
如:
~~~
<script type = "text/javascript">
function carousel_yve(){
var index = 0;
var defaults = {width: "1200px",
height: "500px"};
function init(){
console.log(defaults);
}
function doSomething(){
console.log(index);
}
return {
init: init,
doSomething: doSomething
}
}
var example = carousel_yve();
example.init(); //Object {width: "1200px", height: "500px"}
example.doSomething(); //0
</script>
~~~
carousel_yve是一個函數,通過對它的調用來創建一個模塊實例。每次調用都會生成一個實例。
我們再來看一下單例模式:? ??
~~~
<script type = "text/javascript">
var example = (function carousel_yve(){
var index = 0;
var defaults = {width: "1200px",
height: "500px"};
function init(){
console.log(defaults);
}
function doSomething(){
console.log(index);
}
return {
init: init,
doSomething: doSomething
}
})();
example.init(); //Object {width: "1200px", height: "500px"}
example.doSomething(); //0
</script>
~~~
我們將先前的模塊函數換成了IIFE,即:立即調用。
模塊模式需要具備兩個條件:
1.必須有外部的封閉函數,該函數必須至少被調用一次。
2.封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態。
鑒于本文的目的是研究javaScript的作用域問題,因此對于模塊模式不再做更多的擴展說明。
關于IIFE,有時我們想對外隱藏時,也可以簡單的使用此方式。通過這樣的方式,避免污染所在的作用域。? ?
~~~
<script type = "text/javascript">
(function carousel_yve(){
var index = 10;
var defaults = {width: "1200px", height: "500px;"};
var obj = document.getElementById("btn");
obj.addEventListener("click", function(){/*code*/}, false);
//code……
})();
</script>
~~~
包裝函數的聲明以(function開始,而不是以function開始,看起來區別很小,但是實際上卻完全不一樣,因為(function開始會當做函數表達式,而function開頭是作為標準的函數聲明。而函數聲明和函數表達式最重要的區別在于它們的名稱標識符被綁定在何處。上面的代碼中carousel_yve被綁定在自身的函數中,而不是所在的作用域中,其只能在自身函數的內部被訪問。
此外,對于匿名函數和具名函數還要做一點說明。
JavaScript中,函數表達式允許匿名,但是函數聲明不允許省略函數名,即不允許匿名。盡管匿名函數使用起來簡單快捷,但是匿名函數的幾個缺點需要考慮:
1.匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試困難。
2.如果沒有函數名,當函數需要調用自身時,只能使用過期的arguments.callee引用,比如在遞歸中。
3.匿名函數省略了對于代碼可讀性/可理解性很重要的函數名,一個描述性的名稱可以防止代碼不言自明。(代碼即注釋)
匿名或具名并不會影響函數的功能,因此始終給函數表達式命名是值得推崇的。如我們經常使用的setTimeout、setInterval中。給回調函數命一個形象的名字將使代碼的可讀性更強。??
### 提升
我們前面說過JavaScript運行代碼,分為兩步,第一步:編譯,第二步:執行。
在編譯階段,我們首先會提升變量聲明。舉例說明:? ??
~~~
<script type = "text/javascript">
console.log(a);
var a = 2;
</script>
~~~
這兒是會拋出reference error呢還是Undefined呢?結果是Undefined,原因是因為,作用域會進行提升操作,上面這段代碼實際上的處理流程是下面這樣子的。
~~~
var a;
console.log(a);
a = 2;
~~~
在執行console.log(a)時,a已經被聲明,僅僅是未賦值,因此結果是undefined,而非是拋出reference error.
想想,如果是下面這個樣子的呢。結果又是什么?? ??
~~~
<script type = "text/javascript">
console.log(a);
a = 2;
</script>
~~~
這個地方輸出的又是什么呢?結果是reference error,想一想原因是什么。其實很好理解,這段代碼中,有對a的賦值,但是并沒有去聲明a,盡管在執行賦值時,引擎查在作用域中找不到a,會在全局的作用域中創建一個a,但是因為a沒有聲明,所以在編譯時,不會被提升,在執行console.log(a)時,進行的是RHS查詢,在頂級作用域中查找不到a,拋出reference error的異常。
正因為這些差別,無論是全局作用域中,還是局部作用域中,希望大家都是使用var 去定義變量,而不是省略var,還口口聲聲說javascript是弱語言,有沒有var都一樣。很明顯,有很多區別,在函數作用域里不使用var很有可能會無意中改變了全局變量的值。
關于提升,還有一點需要說明的是:**變量聲明和函數聲明都會被提升,但是函數優先
**舉例說明**
~~~
<script type = "text/javascript">
example();
var example = function(){
console.log(10);
};
function example(){
console.log(20);
}
</script>
~~~
此處的輸出結果是20,而不是10。原因就是因為函數優先,上面的代碼,實際的順序為:? ??
~~~
function example(){
console.log(20);
}
example();
example = function(){
console.log(10);
};
~~~
函數聲明首先被提升,即function example()會被提升到第一步,第二步是var example,但是因為作用域中已經有了example聲明,屬于重復聲明,被忽略。因此引擎正在理解的代碼如上所示,這就是為什么輸出的是20,而并非是10.
值得注意的是:
var的重復聲明會被忽略,但是函數的重復聲明會覆蓋,如下:? ??
~~~
<script type = "text/javascript">
example();
var example = function(){
console.log(10);
};
function example(){
console.log(20);
};
function example(){
console.log(30)
}
</script>
~~~
輸出的結果是30,而不是20,因此請記住這個結論:
函數聲明和變量聲明都會被提升,但是首先是函數被提升,然后才是變量,重復的var聲明會被忽略,但是重復的函數聲明會覆蓋。
另外一個需要注意的地方是:盡可能避免再塊內部聲明函數,至于為何這樣說,我們來看一個例子:
~~~
<script type = "text/javascript">
var a = true;
if(a == true){
function example(){
console.log(10);
};
}else{
function example(){
console.log(20);
}} example();
~~~
你的本意是想當a為true的時候,輸出10,而a為false時,輸出20;很遺憾的是并非是你想的那樣,對于這個例子,火狐的輸出結果是10,而谷歌是20。顯然,引擎對其的處理有所不同。對此,建議您不要這樣使用。
最后再簡單說下閉包的問題,關于閉包,之前已經寫過一篇博文介紹過,這里再說明一次。
閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數。
說一個最典型的問題,下面這段代碼是今天一個初學JS的朋友問我的,相信很多初學者都有遇到過這個問題。? ?
~~~
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
<style>
#ullist li { display: block;width: 40px; height: 40px;
border:1px solid #ccc; text-align: center;
line-height: 40px; cursor: pointer; float: left;
margin:10px;}
</style>
</head>
<body>
<ul id="ullist">
<li id="li1">1</li>
<li id="li2">2</li>
<li id="li3">3</li>
<li id="li4">4</li>
<li id="li5">5</li>
</ul>
</body>
<script>
window.onload=function(){
var ullist=document.getElementById("ullist");
var listE=document.getElementsByTagName("li");
for (var i=0; i<listE.length; i++){
listE[i].onclick = function(i){
alert(listE[i].innerHTML);
};
};
}
</script>
</html>
~~~
很顯然,他的目的是點擊每一個li時,彈出對應的內容,但是結果卻并非如此,并且控制臺中還會報錯。這是為什么呢?事實上,當你點擊時,i的值已經變成了listE.length;而listE[listE.length]是不存在的。JS中for并非是一個塊級作用域,因此i其實是定義在外部的一個變量,for中的函數共用同一個i.
我們將JS的代碼改一改,就可以得到我們想要的結果,如下:
~~~
<script type = "text/javascript">
window.onload=function(){
var ullist=document.getElementById("ullist");
var listE=document.getElementsByTagName("li");
for (var i=0; i<listE.length; i++){
listE[i].onclick = (function(i){
return function(){
alert(listE[i].innerHTML);
}
})(i);
};
};
</script>
~~~
除了這個方法以外,還可以使用ES6中的let聲明for中的i,但是這需要支持ES6的瀏覽器。
更多關于閉包的內容可以查看本人先前的博客《JS閉包與變量》。
此篇博文花費時間較長,如果能為您更一步理解JS作用域提供了一點點的幫助,也是值得的。
? ??
? ??
- 前言
- jQuery輪播圖插件
- JS模擬事件操作
- JS閉包與變量
- JS綁定事件
- HTML5之file控件
- JavaScript的this詞法
- JavaScript的this詞法(二)
- JS this詞法(三)
- JS檢測瀏覽器插件
- JS拖拽組件開發
- number輸入框
- Modernizr.js和yepnode.js
- DOM變化后事件綁定失效
- div和img之間的縫隙問題
- 詳解JavaScript作用域
- bootstrap入門
- 表單驗證(登錄/注冊)
- Bootstrap網格系統
- Bootstrap排版
- Bootstrap創建表單(一)
- Bootstrap表單(二)
- Bootstrap按鈕
- Bootstrap圖片
- Bootstrap字體圖標(glyphicons)
- Bootstrap的aria-label和aria-labelledby
- Bootstrap下拉菜單
- Bootstrap按鈕組
- Bootstrap按鈕菜單
- Bootstrap輸入框組
- Bootstrap導航元素
- Bootstrap導航欄
- sublimeText頻頻崩潰
- JQuery不同版本的差異(checkbox)
- Bootstrap面包屑導航、分頁、標簽、徽章
- Bootstrap警告
- Bootstrap進度條
- 前端的上傳下載
- JS字符串的相關方法
- CSS3選擇器(全)
- CSS3新增文本屬性詳述
- 利用CSS3實現圖片切換特效
- CSS3新增顏色屬性
- CSS3的border-radius屬性詳解
- JS創建對象幾種不同方法詳解
- JS實現繼承的幾種方式詳述(推薦)
- CSS3響應式布局
- JS模塊化開發(requireJS)
- 利用@font-face實現個性化字體
- 前端在html頁面之間傳遞參數的方法
- CSS自動換行、強制不換行、強制斷行、超出顯示省略號
- 如何在Html中引入外部頁面
- reactJS入門
- React組件生命周期
- 使用React實現類似快遞單號查詢效果
- ReactJS組件生命周期詳述
- React 屬性和狀態詳解