javascript执行环境三,你知道现在有多少种函数吗

在前一章学习过程中了解了执行上下文是什么,也知道了任何语句的执行都会依赖特定的上下文。
一旦上下文被切换,整个语句的想过可能都会发生改变;那么,切换上下文的时机就显得非常重要。
在JavaScript中,切换上下文最主要的场景是函数调用;这里就对函数调用切换上下文的事情进行学习,首先,了解一下函数家族。

函数

在 ES2018 中,函数已经非常复杂了,如下:

第一种,普通函数:用function关键字定义的函数,如下:

1
2
3
function foo() {
// code
}

第二种,箭头函数:用 => 运算符定义的函数,如下:

1
2
3
const foo = () => {
// code
}

第三种,在 class 中定义函数

1
2
3
4
5
class C {
foo() {
// code
}
}

第四种,生成器函数:用 function * 定义的函数

1
2
3
function foo* () {
// code
}

第五种,类:用 class 定义的类,实际上也是函数

1
2
3
4
5
class Foo {
constructor() {
// code
}
}

第六、七、八种,异步哈数:普通函数、箭头函数和生成器函数加上 async 关键字

1
2
3
4
5
6
7
8
9
10
11
async function foo() {
// code
}

const foo = async () => {
// code
}

async function foo* () {
// code
}

ES6以来,大量加入的新语法极大地方便了我们编程的同时,也增加了很多我们理解的心智负担;要想认识这些函数的执行上下文切换,必须要对它们行为上的区别有所了解。
对普通变量而言,这些函数并没有本质区别,都是遵循了“继承定义时环境”的规则,它们的一个行为差异在于 this 关键字。

this 关键字的行为

this 是 JavaScript 中的一个关键字,它的使用方法类似于一个变量(但是 this 跟变量的行为有很多不同,前面一节学习了一些普通变量的行为和机制,也就是 var 声明和赋值、let 的内容)。

this 是执行上下文中很重要的一个组成部分,同一个函数调用方式不同,得到的 this 值也不同,如下:

1
2
3
4
5
6
7
8
9
10
function showThis() {
console.log(this);
}

var o = {
showThis: shoThis
}

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

这里通过定义的 showThis 函数并把它赋值给一个对象 o 的属性,然后通过两个引用来调用同一个函数,结果得到的是两个不同的 this 值。

普通函数的 this 值由“调用它所使用的引用”决定,其中奥秘就在于:我们获取函数的表达式,它实际上返回的并非函数本身,而是一个 Reference 类型;Reference 类型由两部分组成:一个对象和一个属性值;当做一些算术运算,Reference 类型会被引用,即获取真正的值(被引用的内容)来参与运算,而类似函数调用、delete 等操作,都需要用到 Reference 类型中的对象。
在这个例子中,Reference 类型中的对象被当做 this 值,传入了执行函数时的上下文当中。
至此,这里说清楚了对 this 的认识:调用函数时使用的引用,决定了函数执行时刻的 this 值
实际上从运行时的角度看,this 跟面向对象毫无关联,它是与函数调用时使用的表达式相关;这个设计来自 JavaScript 早年,通过这样的方式,即模仿了 Java 的语法,又保持了纯粹的“无类”运行时设施。

如果我们把例子换成箭头函数,结果就不一样了:

1
2
3
4
5
6
7
8
9
10
const showThis = () => {
console.log(this);
}

var o = {
showThis: showThis
}

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

这里打印信息看到,改成箭头函数之后,不论用什么应用调用它,都不影响它的 this 值

接下来看看“方法”,它的行为又不一样:

1
2
3
4
5
6
7
8
9
10
11
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 的行为不太一样。

this 关键字的机制

前面说了 this 的相关知识,那么在 JavaScript 内部,实现 this 这些行为的机制又是怎样的呢?

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

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

这个动作就是切换上下文了。

JavaScript 用一个栈来管理执行上下文,这个栈中的每一项又包含一个链表,如下图:

当函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈;而this则是一个更复杂的机制,JavaScript 标准定义了 [[thisMode]] 私有属性,其取值有三个:

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

而方法的行为跟普通函数有差异恰恰是因为 class 设计成了默认按 strict 模式执行。如下:

1
2
3
4
5
6
7
8
9
10
11
"use strict"
function showThis() {
console.log(this);
}

var o = {
showThis: showThis
}

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

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

1
2
3
4
5
6
7
8
9
10
var o = {};
o.foo = function foo() {
console.log(this);
return () => {
console.log(this);
return () => console.log(this);
}
}

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

这里定义了一个三层嵌套的函数,最外层为普通函数,另外两层是箭头函数,这里得到的this值都是一致的。

操作 this 的内置函数

Function.prototype.call 和 Function.prototype.apply 可以执行函数调动时传入的 this 值,如下:

1
2
3
4
5
6
7
function foo(a,b,c) {
console.log(this);
console.log(a,b,c);
}

foo.call({}, 1,2,3);
foo.apply({}, [1,2,3]);

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

1
2
3
4
5
6
function foo(a,b,c) {
console.log(this);
console.log(a,b,c);
}

foo.bind({}, 1, 2, 3)();

有趣的是,call,bind,apply 用于不接受 this 的函数类型如箭头函数,class 时都不会报错;这时候,它们无法实现改变 this 的能力,但是可以实现传参。

总结

这一节内容学了了 8 种函数,围绕 this ,对函数的执行上下文切换机制进行了学习。