第3章 第3節 作用域和作用域鏈的工作流程
目錄
第3章 作用域和作用域鏈的工作流程
在前面資料型別的區分後,下一個比較大的問題是變數和函式的作用範圍和作用時間。
也就C++語言中所對應的作用域和儲存型別,作用域和儲存型別是程式設計中與時間、空間相關的兩個重要概念。
來完成這個作用時間和空間範圍的管理辦法,Javascript使用作用域鏈的建立-修改-刪除來實現。
由於Javascript在es5之前只有全域性和函式作用域,所以研究變數的作用域時,必須以函式的作用範圍為基礎。要以研究函式作用域和作用域鏈為前提。
作用域,和作用域鏈
<!DOCTYPE HTML>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<title>Javascript</title>
</head>
<body>
<script type="text/javascript">
function test(){
}
//test.[scope]//**隱含屬性**
</script>
</body>
</html>
[scope]隱含屬性,儲存執行期上下文的集合
當函式執行時,會建立一個稱之為執行期上下文的內部物件。(AO,GO)
一個執行期上下文定義了一個函式執行時的環境,函式每次執行時對應的上下文都是獨一無二的。所以多次呼叫一個函式會導致建立多個執行上下文,當函式執行完畢,上下文銷燬。
作用域與執行上下文
許多開發人員經常混淆作用域和執行上下文的概念,誤認為它們是相同的概念,但事實並非如此。
我們知道 JavaScript 屬於解釋型語言,JavaScript 的執行分為:解釋和執行兩個階段,這兩個階段所做的事並不一樣:
解釋階段:
- 詞法分析
- 語法分析
- 作用域規則確定
執行階段:
- 建立執行上下文
- 執行函式程式碼
- 垃圾回收
JavaScript 解釋階段便會確定作用域規則,因此作用域在函式定義時就已經確定了,而不是在函式呼叫時確定,但是執行上下文是函式執行之前建立的。執行上下文最明顯的就是 this 的指向是執行時確定的。而作用域訪問的變數是編寫程式碼的結構確定的。
作用域和執行上下文之間最大的區別是:
執行上下文在執行時確定,隨時可能改變;作用域在定義時就確定,並且不會改變。
一個作用域下可能包含若干個上下文環境。有可能從來沒有過上下文環境(函式從來就沒有被呼叫過);有可能有過,現在函式被呼叫完畢後,上下文環境被銷燬了;有可能同時存在一個或多個(閉包)。同一個作用域下,不同的呼叫會產生不同的執行上下文環境,繼而產生不同的變數的值。
作用域鏈:[[scope]]中所儲存的執行期上下文物件的集合,這個集合呈鏈式銜接——>作用域鏈
例:
<!DOCTYPE HTML>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<title>Javascript</title>
</head>
<body>
<script type="text/javascript">
function a() {
function b() {
var b = 2;
}
var a = 1;
b();
}
var c = 3;
a();
</script>
</body>
</html>
執行上下文
“JavaScript中的一切都發生在執行上下文中。”
我希望每個人都記住這句話,因為這很重要。你可以假設執行上下文是一個大容器,當瀏覽器想要執行JavaScript程式碼時就呼叫這個容器。
在這個容器中,有兩個元件:1.記憶體元件;2.程式碼元件。
記憶體元件也就是變數環境。在記憶體元件中,變數和函式以鍵值對的形式儲存。
程式碼元件是容器中一次執行一行程式碼的地方。程式碼元件還有一個很形象的名字——執行執行緒。
JavaScript是一種同步執行的單執行緒語言。這是因為它一次只能以特定的順序執行一個命令。
程式碼的執行
舉個簡單的例子:
var a = 2;
var b = 4;
var sum = a + b;
console.log(sum);
在這個簡單的例子中,我們初始化了兩個變數a
和b
,並分別儲存2
和4
。
然後我們將a
和b
的值相加並存儲在sum
變數中。
那麼JavaScript將在瀏覽器中如何執行這段程式碼呢?請繼續看:
瀏覽器建立了一個有兩個元件的全域性執行上下文,這兩個元件分別是記憶體元件和程式碼元件。
瀏覽器將分兩階段執行這段JavaScript程式碼:
- 1> 記憶體建立階段
- 2> 程式碼執行階段
在記憶體建立階段,JavaScript會掃描所有程式碼,為程式碼中的所有變數和函式分配記憶體。對於變數,JavaScript將在記憶體建立階段儲存undefined
,對於函式,則保留整個函式程式碼。
現在,在第二階段,即程式碼執行階段,開始逐行遍歷整個程式碼。
當遇到var a = 2
時,2
會分配給記憶體中的a
。所以之前,a
的值是未定義的。
同理b
變數也如此,4
分配給b
。然後計算sum
的值並存儲在記憶體中,即6
。最後一步,在控制檯中列印sum
值,然後在完成程式碼時銷燬全域性執行上下文。
如何在執行上下文中呼叫函式?
JavaScript中的函式與其他程式語言相比,工作方式有所不同。
舉個簡單的例子:
var n = 2;
function square(num) {
var ans = num * num;
return ans;
}
var square2 = square(n);
var square4 = square(4);
上面的程式碼中有一個函式,它接受number
型別的引數並返回數字的平方。
當我們執行程式碼時,JavaScript會在第一階段中建立全域性執行上下文併為所有變數和函式分配記憶體,如下所示。
對於函式,則整個函式儲存在記憶體中。
激動人心的部分來了,當JavaScript執行函式時,會在全域性執行上下文中建立一個執行上下文。
首先是程式碼var a = 2
,2
分配給記憶體中的n
。第2行程式碼是一個函式,由於函式在記憶體執行階段已經分配了記憶體,所以會直接跳轉到第6行。
square2
變數將呼叫square
函式,javascript將建立一個新的執行上下文。
square
函式的這個新的執行上下文將在記憶體建立階段為函式中存在的所有變數分配記憶體。
為函式內部的所有變數分配完記憶體後,再繼續逐行執行程式碼。首先是取得num
的值,對於第一個變數就是2
,然後計算ans
。計算完ans
後,返回分配給square2
的值。
一旦函式返回值,就會在完成工作時銷燬函式的執行上下文。
現在對第7行程式碼也就是square4
變數執行類似的過程,如下所示。
一旦所有程式碼執行完畢,全域性執行上下文也將隨之銷燬,這就是JavaScript在後臺執行程式碼的方式。
呼叫堆疊
在JavaScript中呼叫函式時,JavaScript會建立執行上下文。當我們將函式巢狀在函式中時,執行上下文會變複雜。
JavaScript在呼叫棧的幫助下管理程式碼執行上下文的建立和刪除。
棧(有時稱為“下推棧”)是專案的有序集合,其中新專案的新增和現有專案的刪除總是發生在同一端,例如,一摞書。
呼叫棧是一種在呼叫多個函式的指令碼中跟蹤位置的機制。
舉個例子
function a() {
function insideA() {
return true;
}
insideA();
}
a();
我們建立了一個函式a
,它呼叫另一個返回true
的函式insideA
。好吧,有小夥伴說這程式碼很笨,什麼都沒做,的確如此,但這有助於我們理解JavaScript如何處理回撥函式。
JavaScript建立了一個全域性執行上下文。全域性執行上下文將在程式碼執行階段為函式a
分配記憶體並呼叫函式a
。
為函式a
建立了一個執行上下文,此執行上下文位於呼叫棧中的全域性執行上下文之上。
函式a
將分配記憶體並呼叫函式insideA
。為函式insideA
建立一個執行上下文,而此執行上下文將放置在函式a
的呼叫棧之上。
現在,這個insideA
函式將返回true
並從呼叫棧刪除。
由於函式a
執行上下文中沒有程式碼,因此從呼叫棧刪除。
最後,全域性執行上下文也從呼叫棧中刪除。
例題,預編譯的實現原理
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<script>
function test(a) {
console.log(a);
var a = 1;
console.log(a);
function a() {}
console.log(a);
var b = function() {}
console.log(b);
function d() {}
}
test(2);
</script>
</body>
</html>
AO(activation object 活躍物件)的建立過程:
- 1> 記憶體建立階段(預編譯):
四部曲:
1.建立AO物件
2.找形參和變數宣告,將變數和形參作為AO屬性名,值為undefined
3.將實參和形參統一
4.在函式體裡面找函式宣告,值賦予函式體
- 2> 程式碼執行階段
練習1
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<script>
function test(a, b) {
console.log(a);
c = 0;
var c;
a = 5;
b = 6;
function b() {};
function d() {};
console.log(b);
}
test(1);
//AO={
// a: undefined
// 1
// 5
// b: undefined
// function b() {}
// c: undefined
// 0
// d: function d() {}
// }
</script>
</body>
</html>
練習2
//GO global object 全域性上下文
//1.找變數
//2.找函式宣告
//3.執行
//GO = {
// a: undefined—>function a(){} —>1;
// }
//GO===window
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<script>
var a = 1;
function a() {
console.log(2);
}
console.log(a);
//GO global object 全域性上下文
//1.找變數
//2.找函式宣告
//3.執行
//GO = {
// a: undefined—>function a(){} —>1;
// }
//GO===window
</script>
</body>
</html>
練習3
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<script>
function test() {
console.log(b);
if (a) {
var b = 2;
}
c = 3;
console.log(c);
}
var a;
test();
a = 1;
console.log(a);
//GO={
// a: undefined
// test: function test() {}
// c: 3
// }
//
//AO={
// b: undefined
// }
</script>
</body>
</html>