[TOC]
# 理解 JavaScript 作用域
ECMAScript 規(guī)范描述了所有JavaScript 代碼都運(yùn)行在一個(gè)執(zhí)行上下文(execution context)中。
執(zhí)行上下文在 JavaScript 中不是可訪問的實(shí)體,但是了解它們對(duì)于全面理解函數(shù)和閉包的工作原理是至關(guān)重要的。
規(guī)范中說:“當(dāng)控制權(quán)(control)轉(zhuǎn)移至 ECMAScript 的可執(zhí)行代碼時(shí),控制權(quán)進(jìn)入一個(gè)執(zhí)行上下文?;顒?dòng)的執(zhí)行上下文邏輯上形成一個(gè)棧。棧頂?shù)膱?zhí)行上下文是當(dāng)前正在運(yùn)行的執(zhí)行上下文?!?
在運(yùn)行任何代碼之前,JavaScript 引擎會(huì)創(chuàng)建一個(gè)全局對(duì)象,其初始化屬性除了用戶定義的屬性外,還包含 ECMAScript 定義的內(nèi)建對(duì)象(builts-ins),比如 Object、String、Array 和其他。瀏覽器的 JavaScript 實(shí)現(xiàn)提供了全局對(duì)象的一個(gè)屬性,其本身也是全局對(duì)象,即 `window === window.window`。
每當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),控制權(quán)會(huì)進(jìn)入一個(gè)新的執(zhí)行上下文。即使對(duì)一個(gè)函數(shù)的遞歸調(diào)用也是這樣。
# 介紹
在解釋過程中,JavaScript 引擎是嚴(yán)格按著作用域機(jī)制(scope)來執(zhí)行的。JavaScript語法采用的是詞法作用域(lexcical scope),也就是說 **JavaScript 的變量和函數(shù)作用域是在定義時(shí)決定的,而不是執(zhí)行時(shí)決定的**,**由于詞法作用域取決于源代碼結(jié)構(gòu),所以 JavaScript解釋器只需要通過靜態(tài)分析就能確定每個(gè)變量、函數(shù)的作用域,這種作用域也稱為靜態(tài)作用域**(static scope)。補(bǔ)充:但需要注意,`with` 和 `eval` 的語義無法僅通過靜態(tài)技術(shù)實(shí)現(xiàn),實(shí)際上,只能說JS的作用域機(jī)制非常接近詞法作用域。
# 靜態(tài)作用域與動(dòng)態(tài)作用域
作用域是指程序源代碼中定義變量的區(qū)域。
作用域規(guī)定了如何查找變量,也就是確定當(dāng)前執(zhí)行代碼對(duì)變量的訪問權(quán)限。
**JavaScript 采用詞法作用域(lexical scoping),也就是靜態(tài)作用域**。
## 詞法作用域
因?yàn)?JavaScript 采用的是詞法作用域,函數(shù)的作用域在函數(shù)定義的時(shí)候就決定了。
而與詞法作用域相對(duì)的是動(dòng)態(tài)作用域,函數(shù)的作用域是在函數(shù)調(diào)用的時(shí)候才決定的。
讓我們認(rèn)真看個(gè)例子就能明白之間的區(qū)別:
```
var value = 1;
function foo() {
console.log(value); // 在該作用域中使用的變量 value,沒有在該作用域中聲明(即在其他作用域中聲明的),對(duì)于該作用域來說,value 就是一個(gè)**自由變量**。
}
function bar() {
var value = 2;
foo();
}
bar();
// 結(jié)果是 ???
```
假設(shè)JavaScript采用靜態(tài)作用域,讓我們分析下執(zhí)行過程:
執(zhí)行 foo 函數(shù),先從 foo 函數(shù)內(nèi)部查找是否有局部變量 value,如果沒有,就根據(jù)書寫的位置,查找上面一層的代碼 ,(沿著作用域鏈到全局作用域中查找),也就是 value 等于 1,所以結(jié)果會(huì)打印 1。
假設(shè)JavaScript采用動(dòng)態(tài)作用域,讓我們分析下執(zhí)行過程:
執(zhí)行 foo 函數(shù),依然是從 foo 函數(shù)內(nèi)部查找是否有局部變量 value。如果沒有,就從調(diào)用函數(shù)的位置所處作用域,也就是 bar 函數(shù)內(nèi)部查找 value 變量,所以結(jié)果會(huì)打印 2。
前面我們已經(jīng)說了,**JavaScript采用的是靜態(tài)作用域,所以這個(gè)例子的結(jié)果是 1。**
## 動(dòng)態(tài)作用域
也許你會(huì)好奇什么語言是動(dòng)態(tài)作用域?
bash就是動(dòng)態(tài)作用域,不信的話,把下面的腳本存成例如 `scope.bash`,然后進(jìn)入相應(yīng)的目錄,用命令行執(zhí)行 `bash ./scope.bash`,看看打印的值是多少
```
value=1
function foo () {
echo $value;
}
function bar () {
local value=2;
foo;
}
bar
```
這個(gè)文件也可以在 `demos/scope/` 中找到。
最后,讓我們看一個(gè)《JavaScript權(quán)威指南》中的例子:
```js
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
============================
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
```
猜猜兩段代碼各自的執(zhí)行結(jié)果是多少?
答:兩段代碼都會(huì)打印 `local scope`。
# 閉包
**閉包包含了在創(chuàng)建函數(shù)時(shí)的作用域里面的所有變量**。任何時(shí)候你聲明一個(gè)新的函數(shù)并將它賦值給一個(gè)變量,你存儲(chǔ)了函數(shù)定義,同時(shí)存儲(chǔ)了閉包。閉包它就像一個(gè)背包。一個(gè)函數(shù)定義時(shí)附帶了一個(gè)小背包。在這個(gè)包里保存了 **所有在這個(gè)函數(shù)定義創(chuàng)建時(shí)的作用域中的擁有的變量**。
要記住的關(guān)鍵點(diǎn)就是當(dāng) **一個(gè)函數(shù)被聲明時(shí),它會(huì)同時(shí)包含一個(gè)函數(shù)定義和一個(gè)閉包**。
這個(gè)閉包是指在**這個(gè)函數(shù)創(chuàng)建出來時(shí)的作用域中的所有變量的集合**。
你可能會(huì)問,任何函數(shù)都有閉包嗎,甚至在全局作用域創(chuàng)建的函數(shù)?
答案是有。在全局作用域創(chuàng)建的函數(shù)也會(huì)創(chuàng)建一個(gè)閉包。但是因?yàn)檫@些函數(shù)是在全局作用域被創(chuàng)建的,它們擁有所有全局作用域的變量的訪問權(quán)限。這種情況下閉包的概念并沒有什么意義。
當(dāng)一個(gè)函數(shù)返回一個(gè)函數(shù)時(shí),這才是讓閉包概念變得有意義的時(shí)候。這個(gè)返回的函數(shù)擁有并不在全局作用域中的變量的訪問權(quán)限,但他們是完全存在于閉包內(nèi)的。
## 閉包示例
有時(shí)候閉包在你完全沒有注意到它的情況下出現(xiàn)。你可能在偏函數(shù)應(yīng)用中已經(jīng)看到過例子。就像下面的代碼。
```js
let c = 4
const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
```
如果箭頭函數(shù)讓你難以理解,下面是等價(jià)的代碼。
```js
let c = 4
function addX(x) {
return function(n) {
return n + x
}
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
```
我們聲明了一個(gè)常規(guī)的加法函數(shù)addX,它包含一個(gè)參數(shù)(x)并返回另一個(gè)函數(shù)。
返回的函數(shù)仍然有一個(gè)參數(shù),并且將它加到變量x上。
變量x是閉包的一部分。當(dāng)變量addTree在本地上下文中聲明時(shí),它被賦值了一個(gè)函數(shù)定義和一個(gè)閉包。這個(gè)閉包中含有變量x。
于是現(xiàn)在當(dāng)addThree被調(diào)用并執(zhí)行,它擁有對(duì)它的閉包中變量x的訪問權(quán)限,并且將變量n作為參數(shù)傳遞進(jìn)去給予它返回和值的能力。
在這個(gè)例子中,控制臺(tái)會(huì)打印出數(shù)字7。
## 結(jié)語
讓我一直記住閉包的方式是通過把它比喻成**背包**。當(dāng)一個(gè)函數(shù)創(chuàng)建、傳遞或從其他函數(shù)返回時(shí)。它會(huì)隨身攜帶一個(gè)背包。所有在這個(gè)函數(shù)聲明時(shí)的作用域中的變量都在這個(gè)背包里面。
# 作用域鏈(Scope Chain)
在JavaScript中,函數(shù)也是對(duì)象,實(shí)際上,JavaScript里一切都是對(duì)象。函數(shù)對(duì)象和其它對(duì)象一樣,擁有可以通過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內(nèi)部屬性。其中一個(gè)內(nèi)部屬性是[[Scope]],由ECMA-262標(biāo)準(zhǔn)第三版定義,該內(nèi)部屬性包含了函數(shù)被創(chuàng)建的作用域中對(duì)象的集合,這個(gè)集合被稱為函數(shù)的作用域鏈,它決定了哪些數(shù)據(jù)能被函數(shù)訪問。
當(dāng)一個(gè)函數(shù)創(chuàng)建后,它的作用域鏈會(huì)被創(chuàng)建此函數(shù)的作用域中可訪問的數(shù)據(jù)對(duì)象填充。例如定義下面這樣一個(gè)函數(shù):
~~~
function add(num1,num2) {
var sum = num1 + num2;
return sum;
}
~~~
在函數(shù)`add`創(chuàng)建時(shí),它的作用域鏈中會(huì)填入一個(gè)全局對(duì)象,該全局對(duì)象包含了所有全局變量,如下圖所示(注意:圖片只例舉了全部變量中的一部分):

這些值按照它們出現(xiàn)在函數(shù)中的順序被復(fù)制到運(yùn)行期上下文的作用域鏈中。它們共同組成了一個(gè)新的對(duì)象,叫“活動(dòng)對(duì)象(activation object)”,該對(duì)象包含了函數(shù)的所有局部變量、命名參數(shù)、參數(shù)集合以及this,**然后此對(duì)象會(huì)被推入作用域鏈的前端,當(dāng)運(yùn)行期上下文被銷毀,活動(dòng)對(duì)象也隨之銷毀**。新的作用域鏈如下圖所示:

**函數(shù)執(zhí)行過程中,每個(gè)標(biāo)識(shí)符都要經(jīng)歷這樣的搜索過程。過程從作用域鏈頭部,也就是從活動(dòng)對(duì)象開始搜索,查找同名的標(biāo)識(shí)符。**
## 作用域鏈和代碼優(yōu)化
如上圖所示,因?yàn)槿肿兞靠偸谴嬖谟谶\(yùn)行期上下文作用域鏈的最末端,因此在標(biāo)識(shí)符解析的時(shí)候,查找全局變量是最慢的。所以,在編寫代碼的時(shí)候應(yīng)盡量少使用全局變量,盡可能使用局部變量。一個(gè)好的經(jīng)驗(yàn)法則是:如果一個(gè)跨作用域的對(duì)象被引用了一次以上,則先把它存儲(chǔ)到局部變量里再使用。
例如下面的代碼,這個(gè)函數(shù)引用了兩次全局變量`document`,查找該變量必須遍歷整個(gè)作用域鏈,直到最后在全局對(duì)象中才能找到。這段代碼可以重寫如下:
~~~
function changeColor(){
var doc=document;
doc.getElementById("btnChange").onclick=function(){
doc.getElementById("targetCanvas").style.backgroundColor="red";
};
}
~~~
這段代碼比較簡單,重寫后不會(huì)顯示出巨大的性能提升,但是如果程序中有大量的全局變量被從反復(fù)訪問,那么重寫后的代碼性能會(huì)有顯著改善。
## 改變作用域鏈
函數(shù)每次執(zhí)行時(shí)對(duì)應(yīng)的運(yùn)行期上下文都是獨(dú)一無二的,所以多次調(diào)用同一個(gè)函數(shù)就會(huì)導(dǎo)致創(chuàng)建多個(gè)運(yùn)行期上下文,當(dāng)函數(shù)執(zhí)行完畢,執(zhí)行上下文會(huì)被銷毀。每一個(gè)運(yùn)行期上下文都和一個(gè)作用域鏈關(guān)聯(lián)。**一般情況下,在運(yùn)行期上下文運(yùn)行的過程中,其作用域鏈只會(huì)被 with 語句和 catch 語句影響。**
`with`語句是對(duì)象的快捷應(yīng)用方式,用來避免書寫重復(fù)代碼。例如:
~~~
function initUI(){
with(document){
var bd=body,
links=getElementsByTagName("a"),
i=0,
len=links.length;
while(i < len){
update(links[i++]);
}
getElementById("btnInit").onclick=function(){
doSomething();
};
}
}
~~~
這里使用 `width` 語句來避免多次書寫`document`,看上去更高效,實(shí)際上產(chǎn)生了性能問題。
當(dāng)代碼運(yùn)行到`with`語句時(shí),運(yùn)行期上下文的作用域鏈臨時(shí)被改變了。一個(gè)新的可變對(duì)象被創(chuàng)建,它包含了參數(shù)指定的對(duì)象的所有屬性。這個(gè)對(duì)象將被推入作用域鏈的頭部,這意味著函數(shù)的所有局部變量現(xiàn)在處于第二個(gè)作用域鏈對(duì)象中,因此訪問代價(jià)更高了。如下圖所示:

因此在程序中應(yīng)避免使用`with`語句,在這個(gè)例子中,只要簡單的把`document`存儲(chǔ)在一個(gè)局部變量中就可以提升性能。
另外一個(gè)會(huì)改變作用域鏈的是`try-catch`語句中的`catch`語句。當(dāng)`try`代碼塊中發(fā)生錯(cuò)誤時(shí),執(zhí)行過程會(huì)跳轉(zhuǎn)到`catch`語句,然后把異常對(duì)象推入一個(gè)可變對(duì)象并置于作用域的頭部。在`catch`代碼塊內(nèi)部,函數(shù)的所有局部變量將會(huì)被放在第二個(gè)作用域鏈對(duì)象中。示例代碼:
~~~
try{
doSomething();
}catch(ex){
alert(ex.message); //作用域鏈在此處改變
}
~~~
請(qǐng)注意,一旦`catch`語句執(zhí)行完畢,作用域鏈機(jī)會(huì)返回到之前的狀態(tài)。`try-catch`語句在代碼調(diào)試和異常處理中非常有用,因此不建議完全避免。你可以通過優(yōu)化代碼來減少`catch`語句對(duì)性能的影響。一個(gè)很好的模式是將錯(cuò)誤委托給一個(gè)函數(shù)處理,例如:
~~~
try{
doSomething();
}catch(ex){
handleError(ex); //委托給處理器方法
}
~~~
優(yōu)化后的代碼,`handleError`方法是`catch`子句中唯一執(zhí)行的代碼。該函數(shù)接收異常對(duì)象作為參數(shù),這樣你可以更加靈活和統(tǒng)一的處理錯(cuò)誤。由于只執(zhí)行一條語句,且沒有局部變量的訪問,作用域鏈的臨時(shí)改變就不會(huì)影響代碼性能了。
# 參考
https://github.com/mqyqingfeng/Blog/issues/17
[深入理解javascript作用域第二篇之詞法作用域和動(dòng)態(tài)作用域](https://www.jb51.net/article/89146.htm)
- 步入JavaScript的世界
- 二進(jìn)制運(yùn)算
- JavaScript 的版本是怎么回事?
- JavaScript和DOM的產(chǎn)生與發(fā)展
- DOM事件處理
- js的并行加載與順序執(zhí)行
- 正則表達(dá)式
- 當(dāng)遇上this時(shí)
- Javascript中apply、call、bind
- JavaScript的編譯過程與運(yùn)行機(jī)制
- 執(zhí)行上下文(Execution Context)
- javascript 作用域
- 分組中的函數(shù)表達(dá)式
- JS之constructor屬性
- Javascript 按位取反運(yùn)算符 (~)
- EvenLoop 事件循環(huán)
- 異步編程
- JavaScript的九個(gè)思維導(dǎo)圖
- JavaScript奇淫技巧
- JavaScript:shim和polyfill
- ===值得關(guān)注的庫===
- ==文章==
- JavaScript框架
- Angular 1.x
- 啟動(dòng)引導(dǎo)過程
- $scope作用域
- $q與promise
- ngRoute 和 ui-router
- 雙向數(shù)據(jù)綁定
- 規(guī)范和性能優(yōu)化
- 自定義指令
- Angular 事件
- lodash
- Test
