前言
在閱讀本篇文章之前, 請先了解執(zhí)行上下文及執(zhí)行棧的基礎(chǔ)知識點, 移步《JavaScript進階-執(zhí)行上下文(理解執(zhí)行上下文一篇就夠了)》
本篇文章是接著介紹執(zhí)行上下文的要點和講解變量提升.
變量提升
在使用javascript
編寫代碼的時候, 我們知道, 聲明一個變量用var
, 定義一個函數(shù)用function
.那你知道程序在運行它的時候, 都經(jīng)歷了什么嗎?
變量聲明提升
首先是用var
定義一個變量的時候, 例如:
var a = 10;
大部分的編程語言都是先聲明變量再使用, 但是javascript
有所不同, 上面的代碼, 實際相當于這樣執(zhí)行:
var a;
a = 10;
因此有了下面這段代碼的執(zhí)行結(jié)果:
console.log(a); // 聲明,先給一個默認值undefined;
var a = 10; // 賦值,對變量a賦值了10
console.log(a); // 10
上面的代碼??在第一行中并不會報錯Uncaught ReferenceError: a is not defined
, 是因為聲明提升, 給了a
一個默認值.
這就是最簡單的變量聲明提升.
函數(shù)聲明提升
定義函數(shù)也有兩種方法:
- 函數(shù)聲明:
function foo () {}
; - 函數(shù)表達式:
var foo = function () {}
.
第二種函數(shù)表達式的聲明方式更像是給一個變量foo
賦值一個匿名函數(shù).
那這兩種在函數(shù)聲明的時候有什么區(qū)別嗎?
案例一??:
console.log(f1) // function f1(){}
function f1() {} // 函數(shù)聲明
console.log(f2) // undefined
var f2 = function() {} // 函數(shù)表達式
可以看到, 使用函數(shù)聲明的函數(shù)會將整個函數(shù)都提升到作用域(后面會介紹到)的最頂部, 因此打印出來的是整個函數(shù);
而使用函數(shù)表達式聲明則類似于變量聲明提升, 將var f2
提升到了頂部并賦值undefined
.
我們將案例一的代碼添加一點東西:
案例二??:
console.log(f1) // function f1(){...}
f1(); // 1
function f1() { // 函數(shù)聲明
console.log('1')
}
console.log(f2) // undefined
f2(); // 報錯: Uncaught TypeError: f2 is not a function
var f2 = function() { // 函數(shù)表達式
console.log('2')
}
雖然f1()
在function f1 () {...}
之前,但是卻可以正常執(zhí)行;
而f2()
卻會報錯, 原因在案例一中也介紹了是因為在調(diào)用f2()
時, f2
還只是undifined
并沒有被賦值為一個函數(shù), 因此會報錯.
聲明優(yōu)先級: 函數(shù)大于變量
通過上面的介紹我們已經(jīng)知道了兩種聲明提升, 但是當遇到函數(shù)和變量同名且都會被提升的情況時, 函數(shù)聲明的優(yōu)先級是要大于變量聲明的.
- 變量聲明會被函數(shù)聲明覆蓋
- 可以重新賦值
案例一??:
console.log(f1); // f f1() {...}
var f1 = "10";
function f1() {
console.log('我是函數(shù)')
}
// 或者將 var f1 = "10"; 放到后面
案例一說明了變量聲明會被函數(shù)聲明所覆蓋.
案例二??:
console.log(f1); // f f1() { console.log('我是新的函數(shù)') }
var f1 = "10";
function f1() {
console.log('我是函數(shù)')
}
function f1() {
console.log('我是新的函數(shù)')
}
案例二說明了前面聲明的函數(shù)會被后面聲明的同名函數(shù)給覆蓋.
如果你搞懂了, 來做個小練習(xí)?
練習(xí)??
function test(arg) {
console.log(arg);
var arg = 10;
function arg() {
console.log('函數(shù)')
}
console.log(arg)
}
test('LinDaiDai');
答案??
function test(arg) {
console.log(arg); // f arg() { console.log('函數(shù)') }
var arg = 10;
function arg() {
console.log('函數(shù)')
}
console.log(arg); // 10
}
test('LinDaiDai');
- 函數(shù)里的形參
arg
被后面函數(shù)聲明的arg
給覆蓋了, 所以第一個打印出的是函數(shù); - 當執(zhí)行到
var arg = 10
的時候,arg
又被賦值了10
, 所以第二個打印出10
.
執(zhí)行上下文棧的變化
先來看看下面兩段代碼, 在執(zhí)行結(jié)果上是一樣的, 那么它們在執(zhí)行的過程中有什么不同嗎?
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()();
答案是 執(zhí)行上下文棧的變化不一樣。
在第一段代碼中, 棧的變化是這樣的:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
可以看到fn
后被推入棧中, 但是先執(zhí)行了, 所以先被推出棧;
而在第二段中, 棧的變化為:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
由于checkscope
是先推入棧中且先執(zhí)行的, 所以在fn
被執(zhí)行前就被推出了.
VO/AO
接下來要介紹兩個概念:
VO(變量對象), 也就是
variable object
, 創(chuàng)建執(zhí)行上下文時與之關(guān)聯(lián)的會有一個變量對象抱婉,該上下文中的所有變量和函數(shù)全都保存在這個對象中方仿。AO(活動對象), 也就是``activation object`,進入到一個執(zhí)行上下文時添寺,此執(zhí)行上下文中的變量和函數(shù)都可以被訪問到褪猛,可以理解為被激活了赌结。
活動對象和變量對象的區(qū)別在于:
- 變量對象(VO)是規(guī)范上或者是JS引擎上實現(xiàn)的吭露,并不能在JS環(huán)境中直接訪問吠撮。
- 當進入到一個執(zhí)行上下文后,這個變量對象才會被激活讲竿,所以叫活動對象(AO)泥兰,這時候活動對象上的各種屬性才能被訪問。
上面似乎說的比較難理解??, 沒關(guān)系, 我們慢慢來看.
執(zhí)行過程
首先來看看一個執(zhí)行上下文(EC) 被創(chuàng)建和執(zhí)行的過程:
- 創(chuàng)建階段:
創(chuàng)建變量题禀、參數(shù)鞋诗、函數(shù)
arguments
對象;建立作用域鏈;
確定
this
的值.
- 執(zhí)行階段:
變量賦值, 函數(shù)引用, 執(zhí)行代碼.
進入執(zhí)行上下文
在創(chuàng)建階段, 也就是還沒有執(zhí)行代碼之前
此時的變量對象包括(如下順序初始化):
- 函數(shù)的所有形參(僅在函數(shù)上下文): 沒有實參, 屬性值為
undefined
; - 函數(shù)聲明:如果變量對象已經(jīng)存在相同名稱的屬性,則完全替換這個屬性;
- 變量聲明:如果變量名稱跟已經(jīng)聲明的形參或函數(shù)相同迈嘹,則變量聲明不會干擾已經(jīng)存在的這類屬性
一起來看下面的例子??:
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
此時已經(jīng)有賦值了, 但是變量還是undefined
.
代碼執(zhí)行
到了代碼執(zhí)行時, 會修改變量對象的值, 執(zhí)行完后AO
如下:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 20,
c: reference to function c() {},
d: reference to function d() {}
}
在此階段, 前面的變量對象中的值就會被賦值了, 此時變量對象處于激活狀態(tài).
總結(jié)
全局上下文的變量對象初始化是全局對象, 而函數(shù)上下文的變量對象初始化只有
Arguments
對象;EC
創(chuàng)建階段分為創(chuàng)建階段和代碼執(zhí)行階段;在進入執(zhí)行上下文時會給變量對象添加形參削彬、函數(shù)聲明全庸、變量聲明等初始的屬性值;
在代碼執(zhí)行階段,會再次修改變量對象的屬性值.
后語
參考文章: