JavaScript 之代码执行

試着理解 JS 代碼的執行流程。

(最近沒有什麼動力寫文了,但還是要逼自己写出来,加深一下记忆。

這次啃一個 GitHub repo 上的幾篇文章,雖然 Blogger 講得算很易懂了,但還是要花時間好好吸收一下。

作用域

JavaScript 採用詞法作用域(靜態作用域),函數的作用域在函數定義時就已經確定。與之相反的動態作用域,函數的作用域是在函數調用時才決定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// example-1
var value = 1;

function foo() {
console.log(value);
// 定義: 函數內無局部變量 value,獲取全局變量 value = 1
}

function bar() {
var value = 2;
foo();
// 調用 foo(): 不再獲取 bar() 內的局部變量 value
}

bar();
// 1

如果是動態作用域,則是:执行 foo(),依然先从 foo() 内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar() 内部查找 value,结果爲 2。

接下來是兩段代碼,雖然執行結果相同,但在執行上卻有何差異?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// example-2 Code
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f();
}
checkscope(); // "local scope"

//example-3 Code
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f;
}
checkscope()(); //"local scope"

找到了 SegmentFault 上的一個提問,解釋的較爲詳細了。

从代码的行文来看,会发现:

example-2checkscope() 返回的是内部函数 f() 的执行结果;
example-3checkscope() 返回的是内部函数 f,然后再执行返回的函数。

順序執行?

JavaScript 引擎並非以行來分析和執行程式,而是以段來分析執行。當執行一段代碼的時候,會進行一個「準備工作」,比如下例的變量提升和函數提升:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// example-4 Code
var foo = function () {
console.log('foo1');
}
foo(); // foo1

var foo = function () {
console.log('foo2');
}
foo(); // foo2

// example-5 Code
function foo() {
console.log('foo1');
}
foo(); // foo2

function foo() {
console.log('foo2');
}
foo(); // foo2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// example-4 Real 變量提升
var foo;

foo = function() {
console.log('foo1');
}
foo(); //foo1

foo = function() {
console.log('foo2');
}
foo(); //foo2

//example-5 Real 函數提升
var foo;
foo = function () {
console.log('foo1');
}
foo = function () {
console.log('foo2');
}
foo(); // foo2
foo(); // foo2
1
2
3
4
5
6
7
8
9
var a = "global var";

function foo(a) {
a = "function var";
console.log(a);
}

foo("function arg"); // function var
// function var > function arg > global var

執行上下文棧

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文,可以定義它爲一個數組:ECStack = [ ];

當 JS 需要解釋執行代碼的時候,最先遇到全局代碼,所以在初始化時先向棧內壓入一個全局執行上下文,表示爲 globalContext。只有當整個應用程序結束时,ECStack 才會被清空。所以程序結束之前,ECStack 底部永遠有 globalContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// example-6 Code
function fun3() {
console.log('fun3')
}

function fun2() {
fun3();
}

function fun1() {
fun2();
}

fun1();

當上例的代碼執行時,執行上下文棧的處理爲:

1
2
3
4
5
6
7
8
// example-6  Execute
ECStack.push(<fun1> functionContext); // fun1()
ECStack.push(<fun2> functionContext); // fun1() 调用fun2(),創建 fun2() 的执行上下文
ECStack.push(<fun3> functionContext); // fun2() 调用fun3(),創建 fun3() 的执行上下文
ECStack.pop(); // fun3() 执行完毕
ECStack.pop(); // fun2() 执行完毕
ECStack.pop(); // fun1() 执行完毕
// JavaScript 接着执行下面的代码,但是 ECStack 底层永远有个 globalContext

最後我們回到 example-2example-3 中,模擬執行上下文棧。此時能看出兩段代碼的區別:

1
2
3
4
5
6
7
8
9
10
11
// example-2 Execute
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop(<f>);
ECStack.pop(<chechscope>);

// example-3 Execute
ECStack.push(<checkscope> functionContext);
ECStack.pop(<checkscope>);
ECStack.push(<f> functionContext);
ECStack.pop(<f>);

對於每個執行上下文,都有三個重要屬性:

  • 變量對象 (Variable object,VO)
  • 作用域鏈 (Scope chain)
  • this

下面具體討論上面提出的三個屬性。

變量對象

變量對象存儲上下文中定義的變量和函數聲明。下面分情況討論全局上下文的變量對象,和函數上下文的變量對象。

全局上下文

全局上下文中的變量對象,就是全局對象。是由 Object 構造函數實例化出的對象。

1
console.log(this instanceof Object);

全局對象預定義了一些函數和屬性,還可以作爲全局變量的宿主。

1
2
3
4
5
6
console.log(Math.random());
console.log(this.Math.random());

var a = 1;
console.log(this.a);
console.log(window.a);

函數上下文

在函數上下文中,通常使用活動對象(activation object, AO)表示變量對象。

活動對象無法直接通過 JS 進行引用,只有當 AO 進入執行上下文中才會被創建,通過函數的 arguments 屬性初始化。

執行過程

接下來以下面的代碼爲例,解釋執行上下文的過程。

1
2
3
4
5
6
7
8
9
// example-7 Code
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}

foo(1);
  1. 進入執行上下文,會依次導入:
    • 導入函數的所有形參
    • 函數聲明
    • 變量聲明
1
2
3
4
5
6
7
8
9
10
11
// example-7 Step-1
VO = {
arguments: {
0: 1,
length: 1
}, // 形參
a: 1, // 形參,值爲實參或 undefined
b: undefined, // 變量聲明
c: reference to function c(){}, // 函數聲明 地址引用
d: undefined // 變量聲明
}
  1. 代碼執行,給變量聲明賦值
1
2
3
4
5
6
7
8
9
10
11
// example-7 Step-2
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3, // 變量聲明 賦值
c: reference to function c(){},
d: reference to FunctionExpression "d" // 變量聲明 引用
}

思考:下面的代碼將輸出何內容?

1
2
3
4
5
6
7
8
9
10
11
12
// example-8 Code
console.log(foo);

function foo(){
console.log("foo");
}

var foo = 1;

// foo() {
// console.log("foo");
// }

思路:函數提升 > 變量提升

1
2
3
4
5
6
7
//example-8 Hint
function foo(){ // 函數提升
console.log("foo");
}
var foo; // 變量提升
console.log(foo); // foo
foo = 1;

作用域鏈

大概過程如下:

  1. 創建 ECStack = []
  2. 首先壓入 globalContext
  3. 初始化 globalContext 的執行上下文(VO),獲取變量對象
  4. globalContext 代碼執行(AO)
  5. 函数checkscope() 被创建,初始化内部属性 [[scope]],值爲 globalContext.AO
  6. 函數提升,執行 checkscope() 代碼,checkscopeContext 壓入ECStack
  7. 初始化 checkscope() 的執行上下文,複製 checkscope.[[scope]]scopeChain
  8. 進入執行上下文 VO
  9. 代碼執行 AO

更詳細的就不寫了:關於 example-2example-3 的具體分析

this

該部分待編輯

0%