🗒️函数的执行过程
整理的笔记,来源详见文末
1. 闭包
闭包,closure,在编程语言领域它表示一种函数。闭包,其实只是一个绑定了执行环境的函数。
1964 年的《The Computer Journal》里第一次提到闭包,它指出闭包包含两部分:环境部分(环境、标识符列表)和表达式部分。JavaScript 中的函数完全符合闭包的定义:
环境部分
环境 ≈ 函数词法环境
标识符列表 ≈ 函数中用到的未声明变量
表达式部分 ≈ 函数体
和普通函数相比,JavaScript 函数的主要复杂性来自于它所携带的环境部分。
2. 执行上下文
发展到今天, JavaScript 所定义的环境部分,已经比当初的经典定义复杂了很多,比如还需处理 this、变量声明、with 等一系列复杂语法。所以在 JavaScript 的设计中,词法环境只是执行上下文的一部分。
JavaScript 标准把一段代码(包括函数)执行所需要的所有信息定义为执行上下文。
ES3
scope, 作用域, 又称作用域链
variable object, 变量对象, 用于存储变量的对象
this value, this 值
ES5
lexical environment, 词法环境, 获取变量时用
variable environment, 变量环境, 声明变量时用
this value, this 值
ES2018
lexical environment, 词法环境, 获取变量或 this 值时用
variable environment, 变量环境, 声明变量时用
code evaluation state, 用于恢复代码执行位置
Function, 执行的任务是函数时用, 表示正在被执行的函数
ScriptOrModule, 执行的任务是脚本或者模块时使用, 表示正在被执行的代码
Realm, 使用的基础库和内置对象实例
Generator, 仅生成器有该属性, 表示当前生成器
通过代码,来分析函数的执行过程中需要哪些信息,以及它们对应着执行上下文中的哪些部分。
var 把 b 声明到哪里?b 表示哪个变量?b 的原型是哪个对象
let 把 c 声明到哪里?
this 指向哪个对象?
这些信息就需要执行上下文来给出了,这段代码出现在不同的位置,甚至在每次执行中,会关联到不同的执行上下文,所以,同样的代码会产生不一样的行为。
2.1 词法作用域
var 声明的作用域是:作用域函数执行的作用域。在还没有 let 时,有个技巧就是立即执行的函数表达式(IIFE),即通过创建一个函数并立即执行,来构造一个新的域,从而控制 var 的范围。
需要注意的是,当使用 with 的时候,代码如下:
with 内的 var b
作用到了 function 这个环境当中。一句对两个域产生了作用,从语言的角度是个非常糟糕的设计,这也是一些人坚定地反对在任何场景下使用 with 的原因之一。
2.2 变量作用域
为了实现 let,JavaScript 在运行时引入了块级作用域。以下语句会产生 let 使用的作用域:
for
if
switch
try/catch/finally
{}
也就是说,在 let 出现之前,JavaScript 的 if、for 等语句皆不产生作用域。
2.3 Realm
在 ES2016 之前的版本中,标准中甚少提及 {}
的原型问题。但在实际的前端开发中,通过 iframe 等方式创建多 window 环境并非罕见的操作,所以,这才促成了新概念 Realm 的引入。
Realm 中包含一组完整的内置对象,而且是复制关系。
对不同 Realm 中的对象操作,会有一些需要格外注意的问题,比如 instanceOf 几乎是失效的。比如在浏览器环境中获取来自两个 Realm 的对象,它们跟本土的 Object 做 instanceOf 时会产生差异:
可以看到,由于 b1 和 b2 由同样的代码 {}
在不同的 Realm 中执行,所以表现出了不同的行为。
3. 函数调用
一旦执行上下文被切换,整个语句的效果可能都会发生改变。那么,切换上下文的时机就显得非常重要了。在 JavaScript,切换上下文最主要的场景是函数调用。
函数家族:
普通函数:用 function 关键字定义
箭头函数:用
=>
运算符定义方法:在 class 中定义的函数
生成器函数:用
function *
定义的类:用 class 定义的类实际上也是函数
异步函数:async + 普通函数/箭头函数/生成器函数
对普通变量而言,这些函数没有本质区别,都遵循了“继承定义时环境”的规则。它们的一个行为差异在于 this 关键字。this 是执行上下文中很重要的一个组成部分。
通过 new 调用以上函数(只有普通函数和类可以),和直接调用 this 值还是有明显区别的。
3.1 this 的行为
3.1.1 普通函数
普通函数的 this 值由“调用它所使用的引用”决定。当我们获取函数的表达式时,它实际上返回的并不是函数本身,而是一个 Reference 类型,它由两部分组成:一个对象和一个属性值。比如 o.showThis 产生的 Reference 类型,就是由对象 o 和属性 showThis 构成的。
当做一些算术运算或其他运算时,Reference 类型会被解引用,即获取真正的值(被引用的内容)来参与运算,而类似函数调用、delete 等操作,都需要用到 Reference 类型中的对象。
在上面的例子中,Reference 类型中的对象被当作 this 值,传入了执行函数时的上下文当中。也就是说,调用函数时使用的引用,决定了函数执行时刻的 this 值。
实际上,从运行时的角度来看,this 和面向对象毫无关联,它是和函数调用时使用的表达式相关的。这个设计来自 JavaScript 早年,通过这样的方式巧妙地模仿了 Java 的语法,但是 JavaScript 仍然保持了纯粹的“无类”运行时设施。
3.1.2 箭头函数
如果,我们把上面的例子换成箭头函数,结果就不一样了:
改为箭头函数后,不论用什么引用来调用它,都不影响它的 this 值。
3.1.3 类的方法
我们把对象的方法赋值给一个变量,当用该变量去调用方法时,this 的值是 undefined。
3.1.4 其它
用上面的方法继续验证,可以知道:
生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的
异步箭头函数与箭头函数行为是一致的
3.2 this 的机制
函数能够引用定义时的变量,也能记住定义时的 this,因此函数内部必定有一个机制来保存这些信息。在 JavaScript 标准中,为函数规定了用来保存定义时上下文的私有属性 [[Environment]]
。
函数在定义时会创建一个定义时的环境记录(词法环境),在执行时会创建一个新的执行环境记录(执行时的词法环境)。在执行的同时,会把记录的外层词法环境(outer lexical environment)设置成函数的执行环境,这个动作就是切换上下文了。
JavaScript 用一个栈来管理执行上下文,栈中的每项又包含一个链表。如下图所示:
当函数调用时会入栈一个新的执行上下文,函数调用结束时执行上下文被出栈。
而 this 是一个更为复杂的机制,JavaScript 标准定义了 [[thisMode]]
私有属性,它有三个取值:
lexical:表示从上下文中找 this。比如箭头函数
global:表示当 this 为 undefined 时,就取全局对象。比如普通函数
strict:当严格模式时,this 会严格按照调用时传入的值,可能为 null 或者 undefined。比如类的方法,class 默认按 strict 模式执行
函数创建新的执行上下文中的词法环境记录时,会根据 [[thisMode]]
来标记新纪录的 [[ThisBindingStatus]]
私有属性。代码执行遇到 this 时,会逐层检查当前词法环境记录中的 [[ThisBindingStatus]]
,当找到有 this 的环境记录时获取 this 的值。这样的规则的实际效果是,嵌套的箭头函数中的代码都指向外层 this。比如:
3.3 操作 this 的内置函数
JavaScript 还提供了一系列函数的内置方法来操纵 this 值。
Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值,比如:
此外,还有 Function.prototype.bind 它可以生成一个绑定过的函数,这个函数的 this 值固定了参数:
call、bind 和 apply 用于不接受 this 的函数类型,比如箭头函数、class,此时它们无法实现改变 this 的能力,但是可以实现传参。
4. 参考
Last updated