1.執行上下文和執行棧
執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念, JavaScript 中運行任何的代碼都是在執行上下文中運行。
執行上下文的生命周期包括三個階段:創建階段→執行階段→回收階段,我們重點介紹創建階段。
創建階段(當函數被調用,但未執行任何其內部代碼之前)會做以下三件事:
-
創建變量對象:首先初始化函數的參數arguments,提升函數聲明和變量聲明。
-
創建作用域鏈
-
確定this指向
function test(arg){ // 1. 形參 arg 是 "hi" // 2. 因為函數聲明比變量聲明優先級高,所以此時 arg 是 function console.log(arg); var arg = 'hello'; // 3.var arg 變量聲明被忽略, arg = 'hello'被執行 function arg(){ console.log('hello world') } console.log(arg); } test('hi'); /* 輸出: function arg() { console.log('hello world'); } hello */
這是因為當函數執行的時候,首先會形成一個新的私有的作用域,然后依次按照如下的步驟執行:
-
如果有形參,先給形參賦值
-
進行私有作用域中的預解釋,函數聲明優先級比變量聲明高,最后后者會被前者所覆蓋,但是可以重新賦值
-
私有作用域中的代碼從上到下執行
函數多了,就有多個函數執行上下文,每次調用函數創建一個新的執行上下文,那如何管理創建的那么多執行上下文呢?
JavaScript 引擎創建了執行棧來管理執行上下文。可以把執行棧認為是一個存儲函數調用的棧結構,遵循先進后出的原則。
//引用 慕課手記 的圖示來示例一下:
console.log(1); function pFn() { console.log(2); (function cFn() { console.log(3); }()); console.log(4); } pFn(); console.log(5); //輸出:1 2 3 4 5
從上面的流程圖,我們需要記住幾個關鍵點:
-
JavaScript執行在單線程上,所有的代碼都是排隊執行。
-
一開始瀏覽器執行全局的代碼時,首先創建全局的執行上下文,壓入執行棧的頂部。
-
每當進入一個函數的執行就會創建函數的執行上下文,並且把它壓入執行棧的頂部。當前函數執行完成后,當前函數的執行上下文出棧,並等待垃圾回收。
-
瀏覽器的JS執行引擎總是訪問棧頂的執行上下文。
-
全局上下文只有唯一的一個,它在瀏覽器關閉時出棧。
2.作用域與作用域鏈
ES6 到來JavaScript 有全局作用域、函數作用域和塊級作用域(ES6新增)。
我們可以這樣理解:作用域就是一個獨立的地盤,讓變量不會外泄、暴露出去。也就是說作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。
函數作用域:顧名思義就是在這個函數體里邊才能訪問的變量;當然可以利用閉包來實現跨區域訪問局部作用域的變量;查看
塊級作用域:ES6新增,用let命令新增了塊級作用域,外層作用域無法獲取到內層作用域,非常安全明了。即使外層和內層都使用相同變量名,也都互不干擾;
接下來我們再來了解下自由變量(也就是全局變量);
如下代碼中,console.log(a) 要得到a變量,但是在當前的作用域中沒有定義a(可對比一下b)。當前作用域沒有定義的變量,這成為 自由變量。
var a = 100 function fn() { var b = 200 console.log(a) // 這里的a在這里就是一個自由變量 100 console.log(b) // 200 } fn()
接下來再看一個示例:
function F1() { var a = 100 return function () { console.log(a) } } function F2(f1) { var a = 200 console.log(f1()) } var f1 = F1() F2(f1) // 100
上述代碼中,自由變量a的值,從函數F1中查找而不是F2,這是因為當自由變量從作用域鏈中去尋找,依據的是函數定義時的作用域鏈,而不是函數執行時。
那么自由變量的值如何得到 ? —— 向父級作用域 (創建該函數的那個父級作用域)尋找。
如果父級也沒呢?再一層一層向上尋找,直到找到全局作用域還是沒找到,就宣布放棄。這種一層一層的關系,就是作用域鏈 。