什么是作用域?
以下來自于百度百科的定義:
通常來說,一段程序代碼中所用到的名字并不總是有效/可用的,而限定這個名字的可用性的代碼范圍就是這個名字的作用域。
作用域的使用目的是為了提高了程序邏輯的局部性,增強程序的可靠性,減少名字沖突。
在JavaScript中,作用域指的是你代碼的當前上下文環境
補充
函數的每次調用都有與之緊密相關的作用域和上下文。從根本上來說,作用域是基于函數的,而上下文是基于對象的。 換句話說,作用域涉及到所被調用函數中的變量訪問,并且不同的調用場景是不一樣的。上下文始終是this關鍵字的值, 它是擁有(控制)當前所執行代碼的對象的引用。
作用域分類
全局作用域
當我們書寫JavaScript代碼的時候,所處的作用域就是我們所說的 全局作用域 。
<pre>
//Global Scope
var name = "caicai";
</pre>
在這里,我們使用它去創建能夠在別的作用域訪問的模塊以及接口
塊級作用域
任何一對花括號中的語句集都屬于一個塊,在這之中定義的所有變量在代碼塊外都是不可見的,我們稱之為塊級作用域。
大多數類C語言都是有塊級作用域的,然而在JS當中是沒有塊級作用域
<pre>
functin test(){
for(var i=0;i<3;i++){
}
alert(i);
}
test();
執行結果:彈出"3"
</pre>
可見,在塊外,塊中定義的變量i仍然是可以訪問的。
****如何在模擬塊級作用域呢?****
首先要明白一點:在一個函數中定義的變量,當這個函數調用完后,變量會被銷毀。利用閉包模擬:
<pre>
function test(){
(function (){
for(var i=0;i<4;i++){
}
})();
alert(i);
}
test();
執行結果:會彈出"i"未定義的錯誤
</pre>
另外說明一點:從ES6開始,你可以通過let關鍵字來定義變量,它修正了var關鍵字的缺點,能夠讓你像Java語言那樣定義變量,并且支持塊級作用域。
函數作用域
JavaScript中所有的作用域在創建的時候都只伴隨著 函數作用域 ,循環語句像 for 或者 while ,條件語句像 if 或者 switch,屬于塊作用域范疇,由于js不存在塊級作用域,所以都不能夠產生新的作用域. 規則:新的函數 = 新的作用域
<pre>
// Scope A
var myFunction = function () {
// Scope B
var myOtherFunction = function () {
// Scope C
};
};
</pre>
動態作用域
采用動態作用域的變量叫做動態變量。只要程序正在執行定義了動態變量的代碼段,那么在這段時間內,該變量一直存在;代碼段執行結束,該變量便消失。舉一個例子:如果有個函數f,里面調用了函數g,那么在執行g的時候,f里的所有局部變量都會被g訪問到。而在在靜態作用域的情況下,g不能訪問f的變量。有興趣的可以看這里
<pre>
var foo=1;
function static(){
alert(foo);
}
function(){
var foo=2;
static();
}();
</pre>
執行結果:會彈出1而非2,因為static的scope在創建時,記錄的foo是1。
如果js是動態作用域,那么他應該彈出2。
總之,JS不支持動態作用域~
詞法作用域
靜態作用域又叫做詞法作用域,采用詞法作用域的變量叫詞法變量。詞法變量有一個在編譯時靜態確定的作用域。詞法變量的作用域可以是一個函數或一段代碼,該變量在這段代碼區域內可見(visibility);在這段區域以外該變量不可見(或無法訪問)。詞法作用域里,取變量的值時,會檢查函數定義時的文本環境,捕捉函數定義時對該變量的綁定。
注意一點就是:詞法作用域是不可逆的
<pre>
// name = undefined
var scope1 = function () {
// name = undefined
var scope2 = function () {
// name = undefined
var scope3 = function () {
var name = 'Todd'; // locally scoped
};
};
}
</pre>
作用域鏈
再此之前,請先了解下js預編譯和執行過程
當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain);它為一個給定的函數建立了作用域,保證對執行環境有權訪問的所有變量和函數的有序訪問。 作用域鏈包含了在環境棧中的每個執行環境對應的變量對象。通過作用域鏈,可以決定變量的訪問和標識符的解析。 注意,全局執行環境的變量對象始終都是作用域鏈的最后一個對象。
雖然JS的語法風格和C/C++類似, 但作用域的實現卻和C/C++不同,并非用“堆棧”方式,而是使用列表,具體過程如下(ECMA262中所述):
任何執行上下文時刻的作用域, 都是由作用域鏈(scope chain, 后面介紹)來實現.
在一個函數被定義的時候, 會將它定義時刻的scope chain鏈接到這個函數對象的[[scope]]屬性.
在一個函數對象被調用的時候,會創建一個活動對象(也就是一個對象), 然后對于每一個函數的形參,都命名為該活動對象的命名屬性, 然后將這個活動對象做為此時的作用域鏈(scope chain)最前端, 并將這個函數對象的[[scope]]加入到scope chain中.
下面我們通過代碼倆講解下一下:
<pre>
var rain = 1;
function rainman(){
var man = 2;
function inner(){
var innerVar = 4;
alert(rain);
}
inner(); //調用inner函數
}
rainman(); //調用rainman函數
</pre>
通過js預編譯和執行過程來分析:
<pre>
Global LE = {
rainman:對函數引用
rain:1
}
rainman LE {
innerVar :對函數引用
man:2;
}
inner LE {
innerVar:4
}
</pre>
觀察alert(rain);這句代碼。JavaScript首先在inner函數中查找是否定義了變量rain,如果定義了則使用inner函數中的rain變量;如果inner函數中沒有定義rain變量,JavaScript則會繼續在rainman函數中查找是否定義了rain變量,在這段代碼中rainman函數體內沒有定義rain變量,則JavaScript引擎會繼續向上(全局對象)查找是否定義了rain;在全局對象中我們定義了rain = 1,因此最終結果會彈出'1'。
作用域鏈:JavaScript需要查詢一個變量x時,首先會查找作用域鏈的第一個對象,如果以第一個對象沒有定義x變量,JavaScript會繼續查找有沒有定義x變量,如果第二個對象沒有定義則會繼續查找,以此類推。
上面的代碼涉及到了三個作用域鏈對象,依次是:inner、rainman、window。
總之,結合js預編譯和執行過程來看,函數對象的[[scope]]屬性是在定義一個函數的時候決定的, 而非調用的時候而且內部環境可以通過作用域鏈訪問所有的外部環境,但是外部環境不能訪問內部環境中的任何變量和函數。 這些環境之間的聯系是線性的、有次序的。
對于標識符解析(變量名或函數名搜索)是沿著作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始, 然后逐級地向后(全局執行環境)回溯,直到找到標識符為止。
閉包
閉包是指有權訪問另一函數作用域中的變量的函數。換句話說,在函數內定義一個嵌套的函數時,就構成了一個閉包, 它允許嵌套函數訪問外層函數的變量。通過返回嵌套函數,允許你維護對外部函數中局部變量、參數、和內函數聲明的訪問。 這種封裝允許你在外部作用域中隱藏和保護執行環境,并且暴露公共接口,進而通過公共接口執行進一步的操作。
<pre>
var sayHello = function (name) {
var text = 'Hello, ' + name;
return function () {
console.log(text);
};
};
調用方式:
var helloTodd = sayHello('Todd');
helloTodd(); // will call the closure and log 'Hello, Todd'
or
sayHello('Bob')();
</pre>
閉包用處
- 模塊模式:允許你模擬公共的、私有的、和特權成員
<pre>
var Module = (function(){
var privateProperty = 'foo';
function privateMethod(args){
// do something
}
return {
publicProperty: '',
publicMethod: function(args){
// do something
},
privilegedMethod: function(args){
return privateMethod(args);
}
};
})();
</pre>
模塊類似于一個單例對象。由于在上面的代碼中我們利用了(function() { ... })();的匿名函數形式,因此當編譯器解析它的時候會立即執行。 在閉包的執行上下文的外部唯一可以訪問的對象是位于返回對象中的公共方法和屬性。然而,因為執行上下文被保存的緣故, 所有的私有屬性和方法將一直存在于應用的整個生命周期,這意味著我們只有通過公共方法才可以與它們交互。
- 立即執行的函數表達式
<pre>
(function(window){
var foo, bar;
function private(){
// do something
}
window.Module = {
public: function(){
// do something
}
};
})(this);
</pre>
對于保護全局命名空間免受變量污染而言,這種表達式非常有用,它通過構建函數作用域的形式將變量與全局命名空間隔離, 并通過閉包的形式讓它們存在于整個運行時(runtime)。在很多的應用和框架中,這種封裝源代碼的方式用處非常的流行, 通常都是通過暴露一個單一的全局接口的方式與外部進行交互。
其他
對于其他改變作用域的方式,后續文章介紹,比如 : call,apply,bind ,ES6的箭頭函數等等
總結:
1.著重理解 JS 預編譯和執行過程,有助于理解jS作用域鏈
2.閉包的引入帶來了如下好處:
- 減少全局變量
- 減少了傳遞給函數的參數變量
- 封裝
參考文章
1.http://wwsun.github.io/posts/scope-and-context-in-javascript.html
2.http://ryanmorr.com/understanding-scope-and-context-in-javascript/