前言
在閱讀本篇文章之前, 請先了解執行上下文及執行棧的基礎知識點, 移步《JavaScript進階-執行上下文(理解執行上下文一篇就夠了)》.
本篇文章是接著介紹執行上下文的要點和講解變數提升.
變數提升
在使用javascript
編寫程式碼的時候, 我們知道, 宣告一個變數用var
, 定義一個函式用function
.那你知道程式在執行它的時候, 都經歷了什麼嗎?
變數宣告提升
首先是用var
定義一個變數的時候, 例如:
var a = 10;
複製程式碼
大部分的程式語言都是先宣告變數再使用, 但是javascript
有所不同, 上面的程式碼, 實際相當於這樣執行:
var a;
a = 10;
複製程式碼
因此有了下面這段程式碼的執行結果:
console.log(a); // 宣告,先給一個預設值undefined;
var a = 10; // 賦值,對變數a賦值了10
console.log(a); // 10
複製程式碼
上面的程式碼?在第一行中並不會報錯Uncaught ReferenceError: a is not defined
, 是因為宣告提升, 給了a
一個預設值.
這就是最簡單的變數宣告提升.
函式宣告提升
定義函式也有兩種方法:
- 函式宣告:
function foo () {}
; - 函式表示式:
var foo = function () {}
.
第二種函式表示式的宣告方式更像是給一個變數foo
賦值一個匿名函式.
那這兩種在函式宣告的時候有什麼區別嗎?
案例一?:
console.log(f1) // function f1(){}
function f1() {} // 函式宣告
console.log(f2) // undefined
var f2 = function() {} // 函式表示式
複製程式碼
可以看到, 使用函式宣告的函式會將整個函式都提升到作用域(後面會介紹到)的最頂部, 因此列印出來的是整個函式;
而使用函式表示式宣告則類似於變數宣告提升, 將var f2
提升到了頂部並賦值undefined
.
我們將案例一的程式碼新增一點東西:
案例二?:
console.log(f1) // function f1(){...}
f1(); // 1
function f1() { // 函式宣告
console.log('1')
}
console.log(f2) // undefined
f2(); // 報錯: Uncaught TypeError: f2 is not a function
var f2 = function() { // 函式表示式
console.log('2')
}
複製程式碼
雖然f1()
在function f1 () {...}
之前,但是卻可以正常執行;
而f2()
卻會報錯, 原因在案例一中也介紹了是因為在呼叫f2()
時, f2
還只是undifined
並沒有被賦值為一個函式, 因此會報錯.
宣告優先順序: 函式大於變數
通過上面的介紹我們已經知道了兩種宣告提升, 但是當遇到函式和變數同名且都會被提升的情況時, 函式宣告的優先順序是要大於變數宣告的.
- 變數宣告會被函式宣告覆蓋
- 可以重新賦值
案例一?:
console.log(f1); // f f1() {...}
var f1 = "10";
function f1() {
console.log('我是函式')
}
// 或者將 var f1 = "10"; 放到後面
複製程式碼
案例一說明了變數宣告會被函式宣告所覆蓋.
案例二?:
console.log(f1); // f f1() { console.log('我是新的函式') }
var f1 = "10";
function f1() {
console.log('我是函式')
}
function f1() {
console.log('我是新的函式')
}
複製程式碼
案例二說明了前面宣告的函式會被後面宣告的同名函式給覆蓋.
如果你搞懂了, 來做個小練習?
練習✍️
function test(arg) {
console.log(arg);
var arg = 10;
function arg() {
console.log('函式')
}
console.log(arg)
}
test('LinDaiDai');
複製程式碼
答案?
function test(arg) {
console.log(arg); // f arg() { console.log('函式') }
var arg = 10;
function arg() {
console.log('函式')
}
console.log(arg); // 10
}
test('LinDaiDai');
複製程式碼
- 函式裡的形參
arg
被後面函式宣告的arg
給覆蓋了, 所以第一個列印出的是函式; - 當執行到
var arg = 10
的時候,arg
又被賦值了10
, 所以第二個列印出10
.
執行上下文棧的變化
先來看看下面兩段程式碼, 在執行結果上是一樣的, 那麼它們在執行的過程中有什麼不同嗎?
var scope = "global";
function checkScope () {
var scope = "local";
function fn () {
return scope;
}
return fn();
}
checkScope();
複製程式碼
var scope = "global"
function checkScope () {
var scope = "local"
function fn () {
return scope
}
return fn;
}
checkScope()();
複製程式碼
答案是 執行上下文棧的變化不一樣。
在第一段程式碼中, 棧的變化是這樣的:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
複製程式碼
可以看到fn
後被推入棧中, 但是先執行了, 所以先被推出棧;
而在第二段中, 棧的變化為:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
複製程式碼
由於checkscope
是先推入棧中且先執行的, 所以在fn
被執行前就被推出了.
VO/AO
接下來要介紹兩個概念:
-
VO(變數物件), 也就是
variable object
, 建立執行上下文時與之關聯的會有一個變數物件,該上下文中的所有變數和函式全都儲存在這個物件中。 -
AO(活動物件), 也就是``activation object`,進入到一個執行上下文時,此執行上下文中的變數和函式都可以被訪問到,可以理解為被啟用了。
活動物件和變數物件的區別在於:
- 變數物件(VO)是規範上或者是JS引擎上實現的,並不能在JS環境中直接訪問。
- 當進入到一個執行上下文後,這個變數物件才會被啟用,所以叫活動物件(AO),這時候活動物件上的各種屬性才能被訪問。
上面似乎說的比較難理解?, 沒關係, 我們慢慢來看.
執行過程
首先來看看一個執行上下文(EC) 被建立和執行的過程:
- 建立階段:
-
建立變數、引數、函式
arguments
物件; -
建立作用域鏈;
-
確定
this
的值.
- 執行階段:
變數賦值, 函式引用, 執行程式碼.
進入執行上下文
在建立階段, 也就是還沒有執行程式碼之前
此時的變數物件包括(如下順序初始化):
- 函式的所有形參(僅在函式上下文): 沒有實參, 屬性值為
undefined
; - 函式宣告:如果變數物件已經存在相同名稱的屬性,則完全替換這個屬性;
- 變數宣告:如果變數名稱跟已經宣告的形參或函式相同,則變數宣告不會干擾已經存在的這類屬性
一起來看下面的例子?:
function fn (a) {
var b = 2;
function c () {};
var d = function {};
b = 20
}
fn(1)
複製程式碼
對於上面的例子, 此時的AO
是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c() {},
d: undefined
}
複製程式碼
可以看到, 形參arguments
此時已經有賦值了, 但是變數還是undefined
.
程式碼執行
到了程式碼執行時, 會修改變數物件的值, 執行完後AO
如下:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 20,
c: reference to function c() {},
d: reference to function d() {}
}
複製程式碼
在此階段, 前面的變數物件中的值就會被賦值了, 此時變數物件處於啟用狀態.
總結
-
全域性上下文的變數物件初始化是全域性物件, 而函式上下文的變數物件初始化只有
Arguments
物件; -
EC
建立階段分為建立階段和程式碼執行階段; -
在進入執行上下文時會給變數物件新增形參、函式宣告、變數宣告等初始的屬性值;
-
在程式碼執行階段,會再次修改變數物件的屬性值.
後語
參考文章: