JavaScript進階-執行上下文棧和變數物件(一週一更)

LinDaiDai_霖呆呆發表於2019-11-10

前言

在閱讀本篇文章之前, 請先了解執行上下文執行棧的基礎知識點, 移步《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');
複製程式碼
  1. 函式裡的形參arg被後面函式宣告arg給覆蓋了, 所以第一個列印出的是函式;
  2. 當執行到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) 被建立和執行的過程:

  1. 建立階段:
  • 建立變數、引數、函式arguments物件;

  • 建立作用域鏈;

  • 確定this的值.

  1. 執行階段:

變數賦值, 函式引用, 執行程式碼.

進入執行上下文

在建立階段, 也就是還沒有執行程式碼之前

此時的變數物件包括(如下順序初始化):

  1. 函式的所有形參(僅在函式上下文): 沒有實參, 屬性值為undefined;
  2. 函式宣告:如果變數物件已經存在相同名稱的屬性,則完全替換這個屬性;
  3. 變數宣告:如果變數名稱跟已經宣告的形參或函式相同,則變數宣告不會干擾已經存在的這類屬性

一起來看下面的例子?:

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建立階段分為建立階段和程式碼執行階段;

  • 在進入執行上下文時會給變數物件新增形參、函式宣告、變數宣告等初始的屬性值;

  • 在程式碼執行階段,會再次修改變數物件的屬性值.

後語

參考文章:

《聊一聊javascript執行上下文》

《木易楊前端進階-JavaScript深入之執行上下文棧和變數物件》

相關文章