> For the complete documentation index, see [llms.txt](https://anjia1.gitbook.io/web/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://anjia1.gitbook.io/web/js/runtime/function.md).

# 函数的执行过程

整理的笔记，来源详见文末

## 1. 闭包

闭包，closure，在编程语言领域它表示一种函数。<mark style="color:purple;">闭包</mark>，其实只是一个绑定了执行环境的<mark style="color:purple;">函数</mark>。

1964 年的《The Computer Journal》里第一次提到闭包，它指出闭包包含两部分：环境部分（环境、标识符列表）和表达式部分。JavaScript 中的函数完全符合闭包的定义：

1. <mark style="color:red;">环境部分</mark>
   * 环境 ≈ 函数词法环境
   * 标识符列表 ≈ 函数中用到的未声明变量
2. 表达式部分 ≈ 函数体

和普通函数相比，JavaScript 函数的主要复杂性来自于它所携带的环境部分。

## 2. 执行上下文

发展到今天， JavaScript 所定义的环境部分，已经比当初的经典定义复杂了很多，比如还需处理 this、变量声明、with 等一系列复杂语法。所以在 JavaScript 的设计中，<mark style="color:purple;">词法环境</mark>只是执行上下文的一部分。

JavaScript 标准把一段代码（包括函数）执行所需要的所有信息定义为<mark style="color:red;">执行上下文</mark>。

<table><thead><tr><th width="150">发展</th><th>执行上下文</th></tr></thead><tbody><tr><td>ES3</td><td><ol><li>scope, <mark style="color:purple;">作用域</mark>, 又称作用域链</li><li>variable object, 变量对象, 用于存储变量的对象</li><li>this value, <mark style="color:purple;">this</mark> 值</li></ol></td></tr><tr><td>ES5</td><td><ol><li>lexical environment, 词法环境, 获取变量时用</li><li>variable environment, 变量环境, 声明变量时用</li><li>this value, this 值</li></ol></td></tr><tr><td>ES2018</td><td><ol><li>lexical environment, <mark style="color:purple;">词法环境</mark>, 获取变量或 <mark style="color:purple;">this</mark> 值时用</li><li>variable environment, <mark style="color:purple;">变量环境</mark>, 声明变量时用</li><li>code evaluation state, 用于恢复代码执行位置</li><li>Function, 执行的任务是函数时用, 表示正在被执行的函数</li><li>ScriptOrModule, 执行的任务是脚本或者模块时使用, 表示正在被执行的代码</li><li>Realm, 使用的基础库和内置对象实例</li><li>Generator, 仅生成器有该属性, 表示当前生成器</li></ol></td></tr></tbody></table>

通过代码，来分析函数的执行过程中需要哪些信息，以及它们对应着执行上下文中的哪些部分。

```javascript
var b = {};
let c = 1;
this.a = 2;
```

1. var 把 b 声明到哪里？b 表示哪个变量？b 的原型是哪个对象
2. let 把 c 声明到哪里？
3. this 指向哪个对象？

这些信息就需要执行上下文来给出了，这段代码出现在不同的位置，甚至在每次执行中，会关联到不同的执行上下文，所以，同样的代码会产生不一样的行为。

### 2.1 词法作用域

var 声明的作用域是：作用域函数执行的作用域。在还没有 let 时，有个技巧就是立即执行的函数表达式（IIFE），即通过创建一个函数并立即执行，来构造一个新的域，从而控制 var 的范围。

```javascript
// 加分号是为了防止如果上一行代码不写分号，那()就会被解释成上一行代码最末的函数调用
;(function(){
    var a;
}());

;(function(){
    var a;
})();

// 更推荐用 void 关键字，既能避免语法问题，又具有合适的语义（忽略后面表达式的值）
void function(){ // void 任何表达式都会返回 undefined
    var a;
}();
```

需要注意的是，当使用 with 的时候，代码如下：

{% code lineNumbers="true" %}

```javascript
var b;
void function(){
    var env = {b:1};
    b = 2;
    console.log("In function b:", b);
    with(env) {
        var b = 3;
        console.log("In with b:", b);
    }
}();
console.log("Global b:", b);
```

{% endcode %}

with 内的 `var b` 作用到了 function 这个环境当中。一句对两个域产生了作用，从语言的角度是个非常糟糕的设计，这也是一些人坚定地反对在任何场景下使用 with 的原因之一。

### 2.2 变量作用域

为了实现 let，JavaScript 在运行时引入了块级作用域。以下语句会产生 let 使用的作用域：

1. for
2. if
3. switch
4. try/catch/finally
5. {}

也就是说，在 let 出现之前，JavaScript 的 if、for 等语句皆不产生作用域。

### 2.3 Realm

在 ES2016 之前的版本中，标准中甚少提及 `{}` 的原型问题。但在实际的前端开发中，通过 iframe 等方式创建多 window 环境并非罕见的操作，所以，这才促成了新概念 Realm 的引入。

Realm 中包含一组完整的内置对象，而且是复制关系。

对不同 Realm 中的对象操作，会有一些需要格外注意的问题，比如 instanceOf 几乎是失效的。比如在浏览器环境中获取来自两个 Realm 的对象，它们跟本土的 Object 做 instanceOf 时会产生差异：

```javascript
var iframe = document.createElement('iframe')
document.documentElement.appendChild(iframe)
iframe.src="javascript:var b = {};"

var b1 = iframe.contentWindow.b;
var b2 = {};

console.log(typeof b1, typeof b2); //object object

console.log(b1 instanceof Object, b2 instanceof Object); //false true
```

可以看到，由于 b1 和 b2 由同样的代码 `{}` 在不同的 Realm 中执行，所以表现出了不同的行为。

## 3. 函数调用

一旦执行上下文被切换，整个语句的效果可能都会发生改变。那么，切换上下文的时机就显得非常重要了。在 JavaScript，切换上下文最主要的场景是函数调用。

函数家族：

1. 普通函数：用 function 关键字定义
2. 箭头函数：用 `=>` 运算符定义
3. 方法：在 class 中定义的函数
4. 生成器函数：用 `function *` 定义的
5. 类：用 class 定义的类实际上也是函数
6. 异步函数：async + 普通函数/箭头函数/生成器函数

对普通变量而言，这些函数没有本质区别，都遵循了“继承定义时环境”的规则。它们的一个行为差异在于 this 关键字。this 是执行上下文中很重要的一个组成部分。

> 通过 new 调用以上函数（只有普通函数和类可以），和直接调用 this 值还是有明显区别的。

![](https://2598460105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FGNkDWo1TzHEOBRUxCRfy%2Fuploads%2Fp2IBjdoo0PMnzyUdHkKJ%2Fimage.png?alt=media\&token=58f04774-3b63-4e47-aa37-87d2ac2e96dc)

### 3.1 this 的行为

#### 3.1.1 普通函数

普通函数的 this 值由“调用它所使用的引用”决定。当我们获取函数的表达式时，它实际上返回的并不是函数本身，而是一个 [Reference 类型](/web/js/data-type/specification-type.md)，它由两部分组成：一个对象和一个属性值。比如 o.showThis 产生的 Reference 类型，就是由对象 o 和属性 showThis 构成的。

当做一些算术运算或其他运算时，Reference 类型会被解引用，即获取真正的值（被引用的内容）来参与运算，而类似函数调用、delete 等操作，都需要用到 Reference 类型中的对象。

```javascript
function showThis(){
    console.log(this);
}

var o = {
    showThis: showThis
}
// 分别使用两个引用来调用同一个函数，得到的 this 值会不同
showThis(); // global
o.showThis(); // o
```

在上面的例子中，Reference 类型中的对象被当作 this 值，传入了执行函数时的上下文当中。也就是说，调用函数时使用的引用，决定了函数执行时刻的 this 值。

实际上，从运行时的角度来看，this 和面向对象毫无关联，它是和函数调用时使用的表达式相关的。这个设计来自 JavaScript 早年，通过这样的方式巧妙地模仿了 Java 的语法，但是 JavaScript 仍然保持了纯粹的“无类”运行时设施。

#### 3.1.2 箭头函数

如果，我们把上面的例子换成箭头函数，结果就不一样了：

```javascript
const showThis = () => {
    console.log(this);
}

var o = {
    showThis: showThis
}

showThis(); // global
o.showThis(); // global
```

改为箭头函数后，不论用什么引用来调用它，都不影响它的 this 值。

#### 3.1.3 类的方法

我们把对象的方法赋值给一个变量，当用该变量去调用方法时，this 的值是 undefined。

```javascript
class C {
    showThis() {
        console.log(this);
    }
}
var o = new C();
var showThis = o.showThis; // 把对象 o 的方法赋值给了变量 showThis

showThis(); // undefined
o.showThis(); // o
```

#### 3.1.4 其它

用上面的方法继续验证，可以知道：

* 生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的
* 异步箭头函数与箭头函数行为是一致的

### 3.2 this 的机制

函数能够引用定义时的变量，也能记住定义时的 this，因此函数内部必定有一个机制来保存这些信息。在 JavaScript 标准中，为函数规定了用来保存定义时上下文的私有属性 `[[Environment]]`。

函数在定义时会创建一个定义时的环境记录（词法环境），在执行时会创建一个新的执行环境记录（执行时的词法环境）。在执行的同时，会把记录的外层词法环境（outer lexical environment）设置成函数的执行环境，这个动作就是切换上下文了。

JavaScript 用一个栈来管理执行上下文，栈中的每项又包含一个链表。如下图所示：

![](https://2598460105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FGNkDWo1TzHEOBRUxCRfy%2Fuploads%2F0PFmsHrftNQmNaUkm7r9%2Fimage.png?alt=media\&token=41249a9f-f4f7-4f44-af73-251b7172126a)

当函数调用时会入栈一个新的执行上下文，函数调用结束时执行上下文被出栈。

而 this 是一个更为复杂的机制，JavaScript 标准定义了 `[[thisMode]]` 私有属性，它有三个取值：

1. lexical：表示从上下文中找 this。比如箭头函数
2. global：表示当 this 为 undefined 时，就取全局对象。比如普通函数
3. strict：当严格模式时，this 会严格按照调用时传入的值，可能为 null 或者 undefined。比如类的方法，class 默认按 strict 模式执行

```javascript
"use strict"
function showThis(){
    console.log(this);
}

var o = {
    showThis: showThis
}

showThis(); // undefined
o.showThis(); // o
```

函数创建新的执行上下文中的词法环境记录时，会根据 `[[thisMode]]` 来标记新纪录的 `[[ThisBindingStatus]]` 私有属性。代码执行遇到 this 时，会逐层检查当前词法环境记录中的    `[[ThisBindingStatus]]`，当找到有 this 的环境记录时获取 this 的值。这样的规则的实际效果是，嵌套的箭头函数中的代码都指向外层 this。比如：

```javascript
var o = {}
o.foo = function foo(){
    console.log(this);
    return () => {
        console.log(this);
        return () => console.log(this);
    }
}

o.foo()()(); // o, o, o
```

### 3.3 操作 this 的内置函数

JavaScript 还提供了一系列函数的内置方法来操纵 this 值。

Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值，比如：

```javascript
function foo(a, b, c){
    console.log(this);
    console.log(a, b, c);
}
// 这里 call 和 apply 作用是一样的，只是传参方式有区别
foo.call({}, 1, 2, 3);
foo.apply({}, [1, 2, 3]);
```

此外，还有 Function.prototype.bind 它可以生成一个绑定过的函数，这个函数的 this 值固定了参数：

```javascript
function foo(a, b, c){
    console.log(this);
    console.log(a, b, c);
}
foo.bind({}, 1, 2, 3)();
```

call、bind 和 apply 用于不接受 this 的函数类型，比如箭头函数、class，此时它们无法实现改变 this 的能力，但是可以实现传参。

## 4. 参考

* <https://time.geekbang.org/column/article/83302>
* <https://time.geekbang.org/column/article/83719>
