Stories

Detail Return Return

瀏覽器中的JavaScript執行機制 - Stories Detail

在瀏覽器中,JavaScript 代碼通常是通過若干個 script 標籤引入的。而瀏覽器在執行每個 script 標籤時,會有如下特點:

  • 每個 script 標籤引入的 JavaScript 代碼,都是一個宏任務(也就是説,微任務隊列必須在下一個script標籤執行前,全部執行完畢)。

那麼,每個 JavaScript代碼的執行機制是什麼的呢?接下來詳細介紹一下。

變量提升

每一段 JavaScript 代碼的執行機制:先編譯,再執行。

在該執行機制過程中,會將變量(使用 var 聲明)function函數提升到該段代碼的最前面。 具體細節如下:

什麼是一段 JavaScript 代碼:script 標籤引入的代碼、模塊內的代碼、函數內的代碼、eval函數。

編譯階段

編譯階段變量(使用 var 聲明)function函數會被存放到變量環境中。

注意點:1.變量的默認值會被設置為 undefined;2.如果存在兩個相同名稱的function函數,則變量環境只會存放最後定義的那個。

執行階段

執行階段,JavaScript 引擎會從變量環境中去查找自定義的變量和function函數。

實例

showName();
console.log(myName);
var myName = 'Tom';
function showName() {
  console.log('showName被調用');
}

變量提升後:

var myName = undefined;
function showName() {
  console.log('showName被調用');
}

showName();
console.log(myName);
myName = 'Tom';

調用棧

當在全局環境裏裏執行函數時,就會存在兩個執行上下文全局執行上下文函數的執行上下文

JavaScript 引擎通過調用棧(call stack)來管理這些執行上下文。

棧:一種後進先出的數據結構。

調用棧的大概管理流程如下:

  1. 每調用一個函數,JavaScript 引擎會為其創建執行上下文,並把該執行上下文壓入調用棧,然後 JavaScript 引擎開始執行函數代碼。
  2. 如果在一個函數 A 中調用了另外一個函數 B,那麼 JavaScript 引擎會為 B 函數創建執行上下文,並將 B 函數的執行上下文壓入棧頂。
  3. 當前函數執行完畢後,JavaScript 引擎會將該函數的執行上下文彈出棧。
  4. 當分配的調用棧空間被佔滿時,會引發“堆棧溢出”問題。

實例

var a = 2;

function add(b, c) {
  return b + c;
}

function addAll(b, c) {
  var d = 10;
  var result = add(b, c);
  return a + result + d;
}

addAll(3, 6);

第一步,創建全局上下文,並將其壓入棧底

執行a = 2的賦值操作後:

第二步是調用 addAll 函數

將 addAll 函數的執行上下文壓入棧中。

執行d = 10的賦值操作後,會將 addAll 函數執行上下文中的 d 由 undefined 變成了 10。

第三步,當執行到 add 函數

當 add 函數返回時

該函數的執行上下文就會從棧頂彈出,並將 result 的值設置為 add 函數的返回值,也就是 9。

最後

緊接着 addAll 執行最後一個相加操作後並返回,addAll 的執行上下文也會從棧頂部彈出,此時調用棧中就只剩下全局上下文了。

塊級作用域

首先介紹一下 JavaScript 中的代碼塊:

//if 塊
if (1) {}

//while 塊
while (1) {}

//for 循環塊
for (let i = 0; i < 100; i++) {}

// 單獨一個塊
{}

以 var 聲明的變量,都會無視這些代碼塊,不論在哪裏聲明,在編譯階段都會被提升到當前執行上下文的變量環境中。

最常見的例如:

var a = 1;

if (true) {
  var a = 2;
}

console.log(a); // 2

由於 JavaScript 的變量提升存在着:變量覆蓋、變量污染等設計缺陷,所以 ES6 引入了塊級作用域(以 let 和 const 聲明的變量都在塊級作用域裏)來解決這些問題。

let 和 const 塊級作用域實現方式

通過 let 或 const 聲明的變量,在編譯階段會將變量存放在執行上下文的詞法環境中。

不同 var ,在 let 和 const 聲明變量之前,無法訪問該變量,否則會報錯。

實例

function foo() {
  var a = 1;
  let b = 2;

  {
    let b = 3;
    var c = 4;
    let d = 5;
    console.log(a);
    console.log(b);
  }

  console.log(b);
  console.log(c);
  console.log(d);
}
foo();

第一步是編譯並創建執行上下文

第二步繼續執行代碼

變量查找過程

在詞法環境內部,維護了一個小型的棧結構,棧底是函數最外層的變量:

  • 進入一個作用域塊後,就會把該作用域塊內部的變量壓到棧頂。
  • 當作用域塊執行完成之後,該作用域塊內部的變量就會從棧頂彈出。

在執行上下文中,查找變量的過程:

  1. 沿着詞法環境的棧頂向下查詢;如果在詞法環境中的某個作用域塊中查找到了,就直接返回。
  2. 如果沒有查找到,那麼就繼續在變量環境中查找。

作用域執行完成示意圖

作用域鏈

上面介紹的內容都是隻涉及單個作用域。如果涉及到多個作用域,那麼就需要用到作用域鏈。(作用域鏈:在當前作用域中查找不到變量,就會向上級作用域查找,直到全局作用域,這種查找關係就是作用域鏈)

JavaScript 語言的作用域鏈是由詞法作用域決定的:詞法作用域由代碼中函數聲明的位置來決定的,詞法作用域是靜態的作用域,與函數是怎麼調用的沒有關係。

實例:塊級作用域中是如何查找變量的

var bar = {
  myName: 'time.geekbang.com',
  printName: function () {
    console.log(myName);
  },
};

function foo() {
  let myName = ' 極客時間 ';
  return bar.printName;
}

let myName = ' 極客邦 ';

let _printName = foo();

_printName();

bar.printName();

閉包

在 JavaScript 中,根據詞法作用域的規則:內部函數總是可以訪問其外部函數中聲明的變量。

當通過調用一個外部函數,其返回值為一個內部函數;即使該外部函數已經執行結束了,但是內部函數引用外部函數的變量依然保存在內存中,我們就把這些變量的集合稱為閉包

function foo() {
  var myName = ' 極客時間 ';
  let test1 = 1;
  const test2 = 2;
  var innerBar = {
    getName: function () {
      console.log(test1);
      return myName;
    },
    setName: function (newName) {
      myName = newName;
    },
  };
  return innerBar;
}

var bar = foo();
bar.setName(' 極客邦 ');
bar.getName();
console.log(bar.getName());

執行 bar 時調用棧狀態:

JavaScript 引擎會沿着“當前執行上下文 –> foo 函數閉包 –> 全局執行上下文”的順序來查找 myName 變量。

通過 Chrome devtools 中看閉包

閉包對象什麼時候銷燬

如果沒有變量引用閉包,那麼 JavaScript 引擎的垃圾回收器就會回收這塊內存。

this

面嚮對象語言中 this 表示當前對象的一個引用。

但在 JavaScript 中 this 不是固定不變的,它會隨着執行環境的改變而改變。

function函數

作為對象的方法調用時,函數內的 this 指向該對象。

const obj = {
  age: 2,
  printAge() {
    console.log(this.age); // 2
  },
};

作為獨立函數調用時,函數中的 this 指向 undefined(非嚴格模式下,指向 window)。

'use strict';

const printThis = function () {
  console.log(this);
};

printThis(); // undefined

箭頭函數

因為箭頭函數沒有自己的執行上下文,所以箭頭函數的 this 就是它外層的 this。

const obj = {
  age: 2,
  printAge() {
    setTimeout(() => {
      console.log(this.age); // 2;this 指向 printAge 函數的 this ,也就是 obj 對象。
    }, 1000);
  },
};

總結

介紹了 JavaScript 語言中的變量環境詞法環境執行上下文作用域鏈閉包this 的概念和運行方式。希望能對JavaScript的執行機制有一個更深入的理解。

user avatar grewer Avatar yinzhixiaxue Avatar front_yue Avatar dirackeeko Avatar zourongle Avatar leexiaohui1997 Avatar linx Avatar huajianketang Avatar zzd41 Avatar imba97 Avatar assassin Avatar libubai Avatar
Favorites 79 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.