<audio id="audio" title="JavaScript执行（三）：你知道现在有多少种函数吗？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/00/18/0093381651a5e580a13209cc6d07c918.mp3"></audio>

在前一篇文章中，我们大致了解了执行上下文是什么，也知道了任何语句的执行都会依赖特定的上下文。

一旦上下文被切换，整个语句的效果可能都会发生改变。那么，切换上下文的时机就显得非常重要了。

在JavaScript，切换上下文最主要的场景是函数调用。在这一课，我们就来讲讲函数调用切换上下文的事情。我们在讲函数调用之前，首先来认识一下函数家族。

## 函数

在ES2018中，函数已经是一个很复杂的体系了，我在这里整理了一下。

**第一种，普通函数：用function关键字定义的函数。**

示例：

```
function foo(){
    // code
}

```

**第二种，箭头函数：用 =&gt; 运算符定义的函数。**

示例:

```
const foo = () =&gt; {
    // code
}

```

**第三种，方法：在class中定义的函数。**

示例：

```
class C {
    foo(){
        //code
    }
}

```

**第四种，生成器函数：用function * 定义的函数。**

示例：

```
function* foo(){
    // code
}

```

**第五种，类：用class定义的类，实际上也是函数。**

示例：

```
class Foo {
    constructor(){
        //code
    }
}

```

**第六/七/八种，异步函数：普通函数、箭头函数和生成器函数加上async关键字。**

示例：

```
async function foo(){
    // code
}
const foo = async () =&gt; {
    // code
}
async function foo*(){
    // code
}

```

ES6以来，大量加入的新语法极大地方便了我们编程的同时，也增加了很多我们理解的心智负担。要想认识这些函数的执行上下文切换，我们必须要对它们行为上的区别有所了解。

对普通变量而言，这些函数并没有本质区别，都是遵循了“继承定义时环境”的规则，它们的一个行为差异在于this关键字。

那么，this关键字是什么呢，我们一起来看一看。

## this关键字的行为

this是JavaScript中的一个关键字，它的使用方法类似于一个变量（但是this跟变量的行为有很多不同，上一节课我们讲了一些普通变量的行为和机制，也就是var声明和赋值、let的内容）。

**this是执行上下文中很重要的一个组成部分。同一个函数调用方式不同，得到的this值也不同**，我们看一个例子：

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

var o = {
    showThis: showThis
}

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

```

在这个例子中，我们定义了函数showThis，我们把它赋值给一个对象o的属性，然后尝试分别使用两个引用来调用同一个函数，结果得到了不同的this值。

普通函数的this值由“调用它所使用的引用”决定，其中奥秘就在于：我们获取函数的表达式，它实际上返回的并非函数本身，而是一个Reference类型（记得我们在类型一章讲过七种标准类型吗，正是其中之一）。

Reference类型由两部分组成：一个对象和一个属性值。不难理解 o.showThis 产生的Reference类型，即由对象o和属性“showThis”构成。

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

在这个例子中，Reference类型中的对象被当作this值，传入了执行函数时的上下文当中。

至此，我们对this的解释已经非常清晰了：**调用函数时使用的引用，决定了函数执行时刻的this值。**

实际上从运行时的角度来看，this跟面向对象毫无关联，它是与函数调用时使用的表达式相关。

这个设计来自JavaScript早年，通过这样的方式，巧妙地模仿了Java的语法，但是仍然保持了纯粹的“无类”运行时设施。

如果，我们把这个例子稍作修改，换成箭头函数，结果就不一样了：

```
const showThis = () =&gt; {
    console.log(this);
}

var o = {
    showThis: showThis
}

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

```

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

接下来我们看看“方法”，它的行为又不一样了：

```
class C {
    showThis() {
        console.log(this);
    }
}
var o = new C();
var showThis = o.showThis;

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

```

这里我们创建了一个类C，并且实例化出对象o，再把o的方法赋值给了变量showThis。

这时候，我们使用showThis这个引用去调用方法时，得到了undefined。

所以，在方法中，我们看到this的行为也不太一样，它得到了undefined的结果。

按照我们上面的方法，不难验证出：生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的，异步箭头函数与箭头函数行为是一致的。

## this关键字的机制

说完了this行为，我们再来简单谈谈在JavaScript内部，实现this这些行为的机制，让你对这部分知识有一个大概的认知。

函数能够引用定义时的变量，如上文分析，函数也能记住定义时的this，因此，函数内部必定有一个机制来保存这些信息。

在JavaScript标准中，为函数规定了用来保存定义时上下文的私有属性[[Environment]]。

当一个函数执行时，会创建一条新的执行环境记录，记录的外层词法环境（outer lexical environment）会被设置成函数的[[Environment]]。

这个动作就是**切换上下文**了，我们假设有这样的代码：

```
var a = 1;
foo();

在别处定义了foo：

var b = 2;
function foo(){
    console.log(b); // 2
    console.log(a); // error
}

```

这里的foo能够访问b（定义时词法环境），却不能访问a（执行时的词法环境），这就是执行上下文的切换机制了。

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

<img src="https://static001.geekbang.org/resource/image/e8/31/e8d8e96c983a832eb646d6c17ff3df31.jpg" alt="">

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

而this则是一个更为复杂的机制，JavaScript标准定义了 [[thisMode]] 私有属性。

[[thisMode]] 私有属性有三个取值。

- lexical：表示从上下文中找this，这对应了箭头函数。
- global：表示当this为undefined时，取全局对象，对应了普通函数。
- strict：当严格模式时使用，this严格按照调用时传入的值，可能为null或者undefined。

非常有意思的是，方法的行为跟普通函数有差异，恰恰是因为class设计成了默认按strict模式执行。

我们可以用strict达成与上一节中方法的例子一样的效果:

```
&quot;use strict&quot;
function showThis(){
    console.log(this);
}

var o = {
    showThis: showThis
}

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

```

函数创建新的执行上下文中的词法环境记录时，会根据[[thisMode]]来标记新纪录的[[ThisBindingStatus]]私有属性。

代码执行遇到this时，会逐层检查当前词法环境记录中的[[ThisBindingStatus]]，当找到有this的环境记录时获取this的值。

这样的规则的实际效果是，嵌套的箭头函数中的代码都指向外层this，例如：

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

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

```

这个例子中，我们定义了三层嵌套的函数，最外层为普通函数，两层都是箭头函数。

这里调用三个函数，获得的this值是一致的，都是对象o。

JavaScript还提供了一系列函数的内置方法来操纵this值，下面我们来了解一下。

## 操作this的内置函数

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

```
function foo(a, b, c){
    console.log(this);
    console.log(a, b, c);
}
foo.call({}, 1, 2, 3);
foo.apply({}, [1, 2, 3]);

```

这里call和apply作用是一样的，只是传参方式有区别。

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

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

```

有趣的是，call、bind和apply用于不接受this的函数类型如箭头、class都不会报错。

这时候，它们无法实现改变this的能力，但是可以实现传参。

## 结语

在这一节课程中，我们认识了ES2018中规定的各种函数，我一共简单介绍了8种函数。

我们围绕this这个中心，介绍了函数的执行上下文切换机制。同时我们还讲解了this中的一些相关知识。包括了操作this的内置函数。

最后，留给你一个问题，你在日常开发中用过哪些函数类型呢？欢迎给我留言，我们一起讨论。

## 补充阅读：new与this

我们在之前的对象部分已经讲过new的执行过程，我们再来看一下：

- 以构造器的 prototype 属性（注意与私有字段[[prototype]]的区分）为原型，创建新对象；
- 将 this 和调用参数传给构造器，执行；
- 如果构造器返回的是对象，则返回，否则返回第一步创建的对象。

显然，通过new调用函数，跟直接调用的this取值有明显区别。那么我们今天讲的这些函数跟new搭配又会产生什么效果呢？

这里我整理了一张表：

<img src="https://static001.geekbang.org/resource/image/6a/da/6a9f0525b713a903c6c94f52afaea3da.png" alt="">

我们可以看到，仅普通函数和类能够跟new搭配使用，这倒是给我们省去了不少麻烦。


