寫在前面
javascript在瀏覽器中被瀏覽器的js引擎執行解釋,從執行上下文的角度分析一下js的執行機制
執行上下文
執行上下文被定義成javascript引擎在處理理解js程式碼時,所建立的一個動態的環境。理解執行上下文對理解javascript的執行機制至關重要。this指向問題、原型鏈、作用域和垃圾回收都與這個環境密切相關。
javascript程式碼在執行時的相關概念
EC (Execution Context),執行上下文 ECS (Execution Context Stack),執行環境棧,棧在計算機記憶體中是一種後入先出的資料結構 VO(Variable Object),變數物件。在js執行之前先確定,這一階段根據函式形參、函式宣告、變數宣告進行建立,其中在建立函式宣告時,如果名字存在,則會被重寫,在建立變數時,如果變數名存在,則忽略不會進行任何操作。 AO(Active Obejct),活動物件,實際執行時,就是程式碼在執行時變數的賦值和計算。可以理解成,AO = function(VO)
在執行js程式碼之前,js引擎會建立好當前程式碼的執行環境(EC),執行環境在js中可以分為三種:
- 全域性程式碼,程式碼首次被執行的預設環境,可以理解成一個js 檔案或者js片段預設所在的環境
- 函式程式碼,每次進入一個函式體,函式體執行之前
- eval程式碼,eval是一個可以執行string型別的javascript方法
執行上下文與執行上下文堆疊
當瀏覽器首次載入指令碼,根據程式碼建立全域性執行環境,建立的要點可以抽象成確認三個要點:變數、作用域鏈、this指向,確認了這三點之後,開始進入啟用/執行程式碼階段,這時程式從上到下執行,遇到非同步程式碼會壓入任務佇列,如果在全域性程式碼中呼叫一個函式,程式引擎將會進入被呼叫的函式,並建立一個新的上下文,同時將建立的上下文壓入執行棧ECS頂部。執行棧是一個後入先出的結構,最後一個被壓入的函式將會最先執行。執行完成之後依次從棧中出棧,執行棧陸續被清空,最後回到全域性環境,一直到最後一行程式碼執行完畢,啟用/執行程式碼階段結束。為了解釋執行上下文棧的執行特點,我們看一下一面一組簡單的程式碼:
function foo(i) {
if(i === 3) {
return
}
foo(i+1);
console.log(i);
}
foo(0);
複製程式碼
上面是一個自執行函式,我們來模仿一下javascript引擎在看到這段程式碼時處理的思路: 首先看到這段程式碼,預設建立的全域性執行環境可以抽象成一個object:
globalExecutionContextObj = {
scopeChain: { }, /* 變數物件(variableObject)+ 所有父執行上下文的變數物件*/
variableObject: {
foo: [function] //
}, /*函式 arguments/引數,內部變數和函式宣告 */
this: window // 嚴格模式下為undefined,node中為global,瀏覽器為window,這裡理解可以理解為全域性物件
}
複製程式碼
建立完此環境後,開始進入全域性啟用\執行階段,也就是執行foo(0), 在執行foo(0)之前,根據當前的函式體建立一個新的函式執行上下文,函式執行上下文可以抽象為下面的object:
fooExecutionContextObj1 = {
scopeChain: {
i : 0 /* 變數物件(variableObject)+ 所有父執行上下文的變數物件*/
},
variableObject: {
foo: [function] //
}, /*函式 arguments/引數,內部變數和函式宣告 */
this: window // 在函式體內this指向呼叫父函式的物件,這裡this代表為window
}
複製程式碼
函式執行上下文fooExecutionContextObj1建立完成後,進行啟用/執行階段,在一次進入foo函式體內部,不過此時建立的執行上下文fooExecutionContextObj2中,scopeChain.i 變成了2,js引擎重複上面的過程,到第三次啟用執行階段,return 語句結束,壓在棧頂的第三次的foo函式被推出,接著第二次壓入的foo函式推出,最終執行棧回退到全域性執行上下文,一直到程式碼結束,因此上面的程式碼最終列印出來的分別是 2,1,0
解釋變數提升現象
在進入啟用執行階段之前,引擎會先建立執行環境,因此,並不是按照程式碼的書寫順序來理解執行,如下栗子中:
console.log(foo); // 函式指標
console.log(bar); // undefined
var foo = 'hello';
var bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
複製程式碼
使用function 關鍵字生命的foo,覆蓋了前面宣告的foo便令,會被先新增到執行環境中的variableObject,但bar變數只會進行宣告,它的值只有在執行後才會被解析,因此foo可以列印出foo的函式體,bar列印出為undefined
執行上下文中this的四種含義
this在執行上下文中才會被明確,因此js程式碼是動態的,不能以一般靜態語言的邏輯對它進行推測分析,在《javascript語言精粹》中列舉的this指標可能的四種傳值方式。
- 在普通函式中呼叫
- 作為物件的方法
- 在建構函式中呼叫
- 使用Function.prototype.bind、Function.prototype.call、Function.prototype.apply方法改變this指向
執行上的下文與程式碼應用
作用域
閉包
函式柯里化
柯里化通常也被稱為部分求值,它的含義是給函式分步傳遞引數,每次傳遞引數後,部分應用引數,並返回一個更具體的函式接受剩下的引數,中間可以巢狀多個這樣的過程,逐步縮小適用範圍,逐步求解,直至返回最終結果
js 中Function.prototype.bind方法實現了柯里化,看如下程式碼:
Function.prototype.bind = function(ctx) {
var fn = this;
return function() { // 返回的是一個function,先把引數存起來,實際並沒有呼叫
fn.apply(ctx, arguments);
};
};
複製程式碼
總結
- 執行上下文: 在函式執行前被建立,可以用一個object進行抽象描述,分別包含,作用域鏈,當前環境變數,this指標
- 執行上下文解釋了變數提升的現象
- this指標在建立時根據不同情形,它的具體值可能有四種情況:
- 作為物件的方法呼叫,指向這個物件。
- 在普通函式體中直接呼叫,此時指向全域性(原書中說道,這裡應該被設計成指向上一級執行環境,是設計的糟粕)。
- 在建構函式中呼叫,指向新構造出來的物件示例。
- 使用apply, call, bind時,指向被繫結的物件。