JavaScript進階-執行上下文棧和變量對象(一週一更)

前言

在閱讀本篇文章以前, 請先了解執行上下文執行棧的基礎知識點, 移步《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');
複製代碼
  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深刻之執行上下文棧和變量對象》

相關文章
相關標籤/搜索