執行上下文(Execution Context)
JavaScript中的執行環境大概包括三種情況:
- 全域性環境:JavaScript程式碼執行起來會首先進入該環境
- 函式環境:當函式被呼叫執行時,會進入當前函式中執行程式碼
- eval:存在安全問題(因為它可以執行傳給它的任何字串,所以永遠不要傳入字串或者來歷不明和不受信任源的引數)不建議使用,可忽略
每次當控制器轉到可執行程式碼的時候,就會進入一個執行上下文。執行上下文可以理解為當前程式碼的執行環境,它會形成一個作用域。
函式呼叫棧(call stack)
因此在一個JavaScript程式中,必定會產生多個執行上下文,JavaScript引擎會以函式呼叫棧的方式來處理它們。棧底永遠都是全域性上下文,而棧頂就是當前正在執行的上下文。
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
複製程式碼
注意:函式中,遇到 return 能直接終止可執行程式碼的執行,因此會直接將當前上下文彈出棧。
全域性上下文的生命週期,與程式的生命週期一致,只要程式執行不結束,比如關掉瀏覽器視窗,全域性上下文就會一直存在。其他所有的上下文環境,都能直接訪問全域性上下文的屬性。
解了這個過程之後,我們就可以對執行上下文做一些總結:
- 單執行緒
- 同步執行,只有棧頂的上下文處於執行中,其他上下文需要等待
- 全域性上下文只有唯一的一個,它在瀏覽器關閉時出棧
- 函式的執行上下文的個數沒有限制
- 每次某個函式被呼叫,就會有個新的執行上下文為其建立,即使是呼叫的自身函式
執行上下文生命週期
一個執行上下文的生命週期可以分為兩個階段:
-
建立階段:在這個階段中,執行上下文會分別建立變數物件,建立作用域鏈,以及確定this的指向。
- 建立變數物件:
- 建立arguments物件:檢查當前上下文中的引數,建立該物件下的屬性與屬性值。
- 檢查當前上下文的函式宣告:在變數物件中以函式名建立一個屬性,屬性值為指向該函式所在記憶體地址的引用,如果函式名的屬性已經存在,那麼該屬性將會被新的引用所覆蓋。
- 檢查當前上下文中的變數宣告:每找到一個變數宣告,在變數物件中以變數名建立一個屬性,屬性值為undefined,如果該變數名的屬性已經存在,為了防止同名的函式被修改為undefined,則會直接跳過,原屬性值不會被修改。
- 建立作用域鏈:作用域鏈,是由當前環境與上層環境的一系列變數物件組成,它保證了當前執行環境對符合訪問許可權的變數和函式的有序訪問
- 確定this的指向:this的指向,是在函式被呼叫的時候確定的,在函式執行過程中,this一旦被確定,就不可更改了。如果呼叫者函式,被某一個物件所擁有,那麼該函式在呼叫時,內部的this指向該物件。如果函式獨立呼叫,那麼該函式內部的this,則指向undefined。
- 建立變數物件:
-
程式碼執行階段:建立完成之後,就會開始執行程式碼,這個時候,會完成變數賦值,函式引用,以及執行其他程式碼。
建立變數物件:
例子1:
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
複製程式碼
等價於
function test() {
function foo() {
return 2;
}
var a;
console.log(a);
console.log(foo());
a = 1;
}
test();
複製程式碼
例子2:
function test() {
console.log(foo);
console.log(bar);
var foo = 'Hello';
console.log(foo);
var bar = function () {
return 'world';
}
function foo() {
return 'hello';
}
}
test();
複製程式碼
等價於
function test() {
function foo() {
return 'hello';
}
var bar;
console.log(foo);
console.log(bar);
foo = 'Hello';
console.log(foo);
var bar = function () {
return 'world';
}
}
test();
複製程式碼
未進入執行階段之前,變數物件中的屬性都不能訪問。但是進入執行階段之後,變數物件轉變為了活動物件,裡面的屬性都能被訪問了,然後開始進行執行階段的操作。 變數物件和活動物件其實都是同一個物件,只是處於執行上下文的不同生命週期。不過只有處於函式呼叫棧棧頂的執行上下文中的變數物件,才會變成活動物件。 我們可以用建立變數物件來理解變數提升。
建立作用域鏈: 作用域鏈,是由當前環境與上層環境的一系列變數物件組成,它保證了當前執行環境對符合訪問許可權的變數和函式的有序訪問。
var a = 20;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest();
}
test();
複製程式碼
確定this的指向: this的指向,是在函式被呼叫的時候確定的,在函式執行過程中,this一旦被確定,就不可更改了。如果呼叫者函式,被某一個物件所擁有,那麼該函式在呼叫時,內部的this指向該物件。如果函式獨立呼叫,那麼該函式內部的this,則指向undefined。
// demo01
var a = 20;
function fn() {
console.log(this.a);
}
fn();
複製程式碼
// demo02
var a = 20;
function fn() {
function foo() {
console.log(this.a);
}
foo();
}
fn();
複製程式碼
// demo03
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
console.log(obj.c);
console.log(obj.fn());
複製程式碼
使用call,apply顯示指定this
function fn() {
console.log(this.a);
}
var obj = {
a: 20
}
fn.call(obj);
複製程式碼
call與applay後面的引數,都是向將要執行的函式傳遞引數。其中call以一個一個的形式傳遞,apply以陣列的形式傳遞。這是他們唯一的不同。
function fn(num1, num2) {
console.log(this.a + num1 + num2);
}
var obj = {
a: 20
}
fn.call(obj, 100, 10);
fn.apply(obj, [20, 10]);
複製程式碼
事件迴圈機制
JS 引擎建立在單執行緒事件迴圈的概念上。單執行緒( Single-threaded )意味著同一時刻只能執行一段程式碼,與 Swift、 Java 或 C++ 這種允許同時執行多段不同程式碼的多執行緒語言形成了反差。
JavaScript程式碼的執行過程中,除了依靠函式呼叫棧來搞定函式的執行順序外,還依靠任務佇列(task queue)來搞定另外一些程式碼的執行。
- 一個執行緒中,事件迴圈是唯一的,但是任務佇列可以擁有多個。
- 任務佇列又分為macro-task(巨集任務)與micro-task(微任務),在最新標準中,它們被分別稱為task與jobs。
- macro-task大概包括:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
- micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
- setTimeout/Promise等我們稱之為任務源。而進入任務佇列的是他們指定的具體執行任務。
- 來自不同任務源的任務會進入到不同的任務佇列。
- 事件迴圈的順序,決定了JavaScript程式碼的執行順序。它從script(整體程式碼)開始第一次迴圈。之後全域性上下文進入函式呼叫棧。直到呼叫棧清空(只剩全域性),然後執行所有的micro-task。當所有可執行的micro-task執行完畢之後。迴圈再次從macro-task開始,找到其中一個任務佇列執行完畢,然後再執行所有的micro-task,這樣一直迴圈下去。
- 其中每一個任務的執行,無論是macro-task還是micro-task,都是藉助函式呼叫棧來完成。
例子1:
setTimeout(function() {
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
})
console.log('global1');
複製程式碼
例子2:
// demo02
console.log('glob1');
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
複製程式碼