在閱讀本篇文章以前, 請先了解執行上下文及執行棧的基礎知識點, 移步《JavaScript進階-執行上下文(理解執行上下文一篇就夠了)》.javascript
本篇文章是接着介紹執行上下文的要點和講解變量提高.html
在使用javascript
編寫代碼的時候, 咱們知道, 聲明一個變量用var
, 定義一個函數用function
.那你知道程序在運行它的時候, 都經歷了什麼嗎?前端
首先是用var
定義一個變量的時候, 例如:java
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
一個默認值.函數
這就是最簡單的變量聲明提高.post
定義函數也有兩種方法:ui
function foo () {}
;var foo = function () {}
.第二種函數表達式的聲明方式更像是給一個變量foo
賦值一個匿名函數.this
那這兩種在函數聲明的時候有什麼區別嗎?
案例一🌰:
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(變量對象), 也就是variable object
, 建立執行上下文時與之關聯的會有一個變量對象,該上下文中的全部變量和函數全都保存在這個對象中。
AO(活動對象), 也就是``activation object`,進入到一個執行上下文時,此執行上下文中的變量和函數均可以被訪問到,能夠理解爲被激活了。
活動對象和變量對象的區別在於:
上面彷佛說的比較難理解😢, 不要緊, 咱們慢慢來看.
首先來看看一個執行上下文(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
建立階段分爲建立階段和代碼執行階段;
在進入執行上下文時會給變量對象添加形參、函數聲明、變量聲明等初始的屬性值;
在代碼執行階段,會再次修改變量對象的屬性值.
參考文章: