在瀏覽器中,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)來管理這些執行上下文。
棧:一種後進先出的數據結構。
調用棧的大概管理流程如下:
- 每調用一個函數,JavaScript 引擎會為其創建執行上下文,並把該執行上下文壓入調用棧,然後 JavaScript 引擎開始執行函數代碼。
- 如果在一個函數 A 中調用了另外一個函數 B,那麼 JavaScript 引擎會為 B 函數創建執行上下文,並將 B 函數的執行上下文壓入棧頂。
- 當前函數執行完畢後,JavaScript 引擎會將該函數的執行上下文彈出棧。
- 當分配的調用棧空間被佔滿時,會引發“
堆棧溢出”問題。
實例
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();
第一步是編譯並創建執行上下文
第二步繼續執行代碼
變量查找過程
在詞法環境內部,維護了一個小型的棧結構,棧底是函數最外層的變量:
- 進入一個作用域塊後,就會把該作用域塊內部的變量壓到棧頂。
- 當作用域塊執行完成之後,該作用域塊內部的變量就會從棧頂彈出。
在執行上下文中,查找變量的過程:
- 沿着詞法環境的棧頂向下查詢;如果在
詞法環境中的某個作用域塊中查找到了,就直接返回。 - 如果沒有查找到,那麼就繼續在
變量環境中查找。
作用域執行完成示意圖
作用域鏈
上面介紹的內容都是隻涉及單個作用域。如果涉及到多個作用域,那麼就需要用到作用域鏈。(作用域鏈:在當前作用域中查找不到變量,就會向上級作用域查找,直到全局作用域,這種查找關係就是作用域鏈)
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的執行機制有一個更深入的理解。