目錄:
JS 中的執行上下文
JS 是一種描述性腳本語言,不同於 C#,JAVA,它不需要編譯成中間語言,而是由 JS 引擎動態解析和執行。執行上下文( Execution Context ),也便是常說的 執行環境。
執行上下文 三屬性: 變量對象、作用域鏈、this 指向
注意
這里我們需要注意的一點是, JS 引擎解析執行代碼的過程是一個邊執行邊解析的過程,解析發生在執行一段可執行代碼之前。舉個例子,當執行到一個函數的時候,就會先對這個函數進行解析,然后再執行這個函數。
變量提升
某函數或者某變量 在作用域中前面被調用,但聲明卻在后面,按理來說應該調用失敗,但實際上 JS 在解析(預處理)時,會根據 可執行代碼 創建相對應的 執行上下文,在這個環境中,所有變量會被事先提出來(變量提升),解析完代碼之后才開始執行。
可執行代碼
在代碼解析階段會根據不同的可執行代碼創建相應的執行上下文,可執行代碼分類:
- 全局執行代碼,在執行所有代碼前,解析創建全局執行上下文。
- 函數執行代碼,執行函數前,解析創建函數執行上下文。
- eval 執行代碼,運行於當前執行上下文中。
執行上下文的組成
執行上下文定義了變量或函數有權訪問的其他數據,決定了它們各自的行為。每一個執行上下文都由以下三個屬性組成。
- 變量對象(Variable object,VO)
- 作用域鏈(Scope chain)
- this
執行上下文棧
解析 可執行代碼 會創建對應的 執行上下文,JS 引擎通過 執行上下文棧(Execution context stack,ECS) 來管理這些執行上下文。
var a = "global var";
function foo() {
console.log(a);
}
function outerFunc() {
var b = "var in outerFunc";
console.log(b);
function innerFunc() {
var c = "var in innerFunc";
console.log(c);
foo();
}
innerFunc();
}
outerFunc();
代碼首先進入 全局執行上下文 ,然后依次進入 outerFunc,innerFunc 和 foo 的執行上下文,執行上下文棧就可以表示為:
JavaScript 開始要解析執行代碼的時候,最先遇到的就是全局代碼,所以 JavaScript 引擎會先解析創建全局執行上下文,然后將全局執行上下文壓棧。然后當執行流進入一個函數時,會先解析創建函數執行上下文,然后將它的執行上下文壓棧。而在函數執行之后,會將其執行上下文彈棧,彈棧后執行上下文中所有的數據都會被銷毀,然后把控制權返回給之前的執行上下文。注意,全局執行上下文會一直留在棧底,直到整個應用結束。
變量對象_皆是 var 聲明
創建變量對象(VO)時就是將各種變量和函數聲明進行提升的環節
定義:
變量對象(Variable object,VO)存儲了在執行上下文中定義的所有變量和函數聲明,保證代碼執行時對變量和函數的正確訪問。
//用下面代碼為例子
console.log(a);
console.log(b);
console.log(c);
console.log(d);
var a = 100;
b = 10;
function c() {}
var d = function () {};
上述代碼的變量對象:
//這里用VO表示變量對象
VO = {
a = undefined; //有a,a使用var聲明,值會被賦值為undefined
//沒有b,因為b沒用var聲明
c = function c (){} //有c,c是函數聲明,並且c指向該函數
d = undefined; //有d,d用var聲明,值會被賦值為undefined
}
解說:執行上述代碼的時候,會創建一個全局執行上下文,上下文中包含上面變量對象,創建完執行上下文后,這個執行上下文才會被壓進執行棧中。開始執行后,因為 js 代碼一步一步被執行,后面賦值的代碼還沒被執行到,所以使用 console.log 函數打印各個變量的值是變量對象中的值。
在運行到第二行時會報錯(報錯后就不再執行了),因為沒有 b(b is no defined)。把第二行注釋掉后,再執行各個結果就是 VO 里面的對應的值。
活動對象(active Object)
活動對象是在函數執行上下文里面的,其實也是變量對象,只是它需要在函數被調用時才被激活,而且初始化 arguments,激活后就是看做變量對象執行上面一樣的步驟。
//例子
function fn(name){
var age = 3;
console.log(name);
}
fn('ry');
//當上面的函數fn被調用,就會創建一個執行上下文,同時活動對象被激活
//活動對象
AO = {
arguments : {0:'ry'}, //arguments的值初始化為傳入的參數
name : ry, //形參初始化為傳進來的值
age : undefined //var 聲明的age,賦值為undefined
}
活動對象其實也是變量對象,做着同樣的工作。其實不管變量還是活動對象,這里都表明了,全局執行和函數執行時都有一個變量對象來儲存着該上下文(環境內)定義的變量和函數。
作用域鏈
在創建執行上下文時還要創建一個重要的東西,就是作用域鏈。每個執行環境的作用域鏈由當前環境的變量對象及父級環境的作用域鏈構成。(父級環境作用域鏈會繼續向上套娃)
創建作用域鏈過程:
//以本段代碼為例
function fn(a,b){
var x = 'string',
}
fn(1,2);
1. 函數被調用前,先創建了 全局執行上下文,初始化 function fn,fn 有個私有屬性[[scope]],它會被初始化為當前全局的作用域,fn.[[scope]="globalScope"。
2. 調用函數 fn(1,2),開始創建 fn 執行上下文,同時創建作用域鏈 fn.scopeChain = [fn.[[scope]]],此時作用域鏈中有全局作用域。
3. fn 活動對象 AO 被初始化后,把活動對象作為變量對象推到作用域鏈前端,此時 fn.scopeChain = [fn.AO,fn.[[scope]]],構建完成,此時作用域鏈中有兩個值,一個當前活動對象,一個全局作用域。
fn 的作用域鏈構建完成,作用域鏈中有兩個值,第一個是 fn 函數自身的活動對象,能訪問自身的變量,還有一個是全局作用域,所以 fn 能訪問外部的變量。這里就說明了為什么函數中能夠訪問函數外部的變量,因為有作用域鏈,在自身找不到就順着作用域鏈往上找。
this
執行上下文 ,一個全局執行上下文,一個函數執行上下,下面分別說說這兩種上下文的 this。
-
全局執行上下文的 this
指向 window 全局對象
-
函數執行上下文的 this(主要講函數的 this)
在《JavaScript 權威指南》中有這么幾句話:
1.this 是關鍵字,不是變量,不是屬性名,js 語法不允許給 this 賦值。2.關鍵字 this 沒有作用域限制,嵌套的函數不會從調用它的函數中繼承 this。
3.如果嵌套函數作為方法調用,其 this 指向調用它的對象。
4.如果嵌套函數作為函數調用,其 this 值是 window(非嚴格模式),或 undefined(嚴格模式下)。
解讀: 上面說的概括了 this 兩種值的情況:
- 函數直接作為某對象的方法被調用則函數的 this 指向該對象。
- 函數作為函數直接獨立調用(不是某對象的方法),或是函數中的函數,其 this 指向 window。
例子 1:函數被獨立運行時,其 this 的值指向 window 對象。
function a() {
console.log(this);
}
//獨立運行
a(); //window
例子 2:(函數中函數,這里嵌套了個外圍函數)這里也是指向 window 對象,也相當於函數作為函數調用,就是獨立運行。其實這個例子也說明閉包的 this 指向 Window。
//外圍函數
function a() {
//b函數在里面
function b() {
console.log(this);
}
//雖然在函數中,但b函數獨立運行,不是那個對象的方法
b();
}
a(); //window
例子 3:(再寫復雜點的話)x 函數即使在對象里面,但它是函數中的函數,也是作為函數運行,不是 Object 的方法。getName 才是 objcet 的方法,所以 getName 的 this 指向 object(在下個栗子有)。
//一個對象
var object = {
//getName是Object的方法
getName: function () {
//x是getName里面的函數,它是作為函數調用的,this就是window啦
function x() {
console.log(this); //this是window啦
}
x();
},
};
object.getName(); //window
例子 4:函數作為某個對象的方法被調用。
//一個對象
var object = {
name: "object",
//getName 是 Object 的方法
getName: function () {
console.log(this === object);
},
};
object.getName(); //true , 說明 this 指向了 object
這里的 getName 中的 this 是指向 objct 對象的,因為 getName 是 object 的一個方法,它作為對象方法被調用。
例子 5 :通過 call 改變 this
var name = "window";
var obj = {
name: "obj",
};
function fn() {
console.log(this.name);
}
//將fn通過call或bind或apply直接綁定給obj,從而成為obj的方法。
fn.call(obj); //obj
再總結一下 this 的值
-
全局執行上下文:this 的值是 window
-
函數執行上下文:this 的值兩種:
-
函數中 this 指向某對象,因為函數作為對象的方法:怎么看函數是對象的方法,一種是直接寫在對象里面,另一種是通過 call 等方法直接綁定在對象中。
-
函數中 this 指向 window:函數獨立運行,不是對象的方法,函數中的函數(閉包),其 this 指向 window。
-
總結整個 js 代碼執行過程
js 代碼執行分成了兩部分:解析和執行
解析:創建執行上下文,有兩種,一種是開始執行 js 代碼就創建全局執行上下文,一種是當某個函數被調用時創建它自己的函數執行上下文。執行上下文包含 三個部分(變量對象,作用域鏈,this)
執行:在執行棧中執行,棧頂的執行上下文獲得執行權,並按順序執行當前上下文中的代碼,執行完后彈棧銷毀上下文,執行權交給下一個棧頂執行上下文。