作用域 是一个与编程语言紧密相关的概念,但是当面试官提出作用域是什么时,我们应该如何回答呢?
先上干货,作用域是一套管理和查找标识符(变量)的规则。现在看来作用域的概念还有些抽象,不过没关系,接下来我会带大家更深入的理解作用域。
编译
JavaScript 虽然被归类为 解释执行 语言,但是在实际的运行过程中仍然具有 编译 这个步骤。只是相对于传统的编译语言来说,它没有大量的时间进行预先编译,而是一边编译一边执行,这个过程非常短暂(几微妙甚至更短),紧接着代码就会执行。
我们先来看看传统编译的流程:
- 分词/词法分析,将源代码处理为词法单元。
- 解析/语法分析,利用上一步生成的词法单元流组合生成抽象语法树(AST)。
- 将 AST 转换为目标平台可执行的代码。
这三步是传统编译器在执行编译时的必要步骤,实际当中 JavaScript 引擎所调用的编译器更加复杂,其中包含了许多优化执行效率的操作。
运行
了解完基础的编译步骤,接下来我们通过一句简单的声明语句 var a = 2
来了解 JavaScript 代码运行的两个阶段 —— 编译阶段 和 执行阶段,顺序如下:
- 编译阶段:编译器 发现需要声明一个名为
a
的标识符,此时它就会通知 作用域 进行查找。如果当前作用域集合中已经存在相同名称的标识符,编译器会忽略本次声明继续编译;否则它会要求在当前作用域集合中生成一个新的标识符,并命名为a
。 - 执行阶段:编译结束后,编译器 会将可执行代码提供给 引擎 。执行过程中 引擎 发现需要对标识符
a
进行赋值。引擎 就会询问 作用域 是否能查找到名为a
的标识符,查找成功引擎就将 2 赋值给a
,失败则抛出异常。
通过上述的分析我们得知,这句简单的语句在运行时会被分解为 var a
和 a = 2
。声明 在编译阶段就会执行,而 赋值 是在执行阶段才发生,作用域 在这两个阶段当中就起到查找并且管理标识符的作用。
变量提升
在 JavaScript 中标识符的声明会“移动”到各自作用域的顶端,我们将这种现象称为 变量提升 ,它的实际原理就在于 JavaScript 代码在 编译阶段 就将 声明 这个动作完成了。所以在运行时 作用域 当中已经包含了所有的标识符,感觉上就好像变量提升到了当前作用域的顶端,我们重点关注一下不同类型的标识符提升的特点:
- 普通变量只会提升声明部分,并将为其赋初值
undefined
,当在声明之前访问变量时将获得undefined
。 - 函数声明属于完全提升,所以 JavaScript 中允许在 函数声明 之前发生 函数调用 。并且函数提升 优先 于普通变量,当存在函数与普通变量同名时,普通变量的声明将会被忽略。
console.log(a) // undefined
var a = 2
foo() // success
function foo() {
console.log('success');
}
LHS、RHS
接下来我们重点来了解一下 引擎 查找标识符的两种方式 —— LHS 和 RHS:
- LHS,可以理解为对标识符的 内存地址 的查询。
- RHS,可以理解为对标识符所存储的 值 的查询。
function foo(a) {
var b = a
}
foo(2)
我们来分析下上面的代码,上面这段代码中包含了 2 次 LHS 和 2 次 RHS,实际执行过程如下:
- 当需要调用
foo
函数时,引擎 会执行 RHS 查询foo
的值。 - 当
foo
函数在执行时,为了给型参a
进行赋值会执行 LHS 查询标识符a
的内存地址,并将 2 赋值给a
。 - 当执行
var b = a
语句时,引擎 会执行对标识符a
的值的 RHS 查询,以获取值 2。然后对标识符b
执行 LHS 查询得到内存地址后,将 2 赋值给b
。
异常
首先我们来了解下 JavaScript 语言目前拥有的三类作用域:
- 全局作用域:其中包含一些必要的公共函数和变量,属于顶层作用域。
- 函数作用域:该作用域会包含函数的参数和在函数当中声明的所有标识符。
- 块级作用域:ES6 开始引入了块级作用域的概念,
{}
括号内包裹的区域可以看作是一个块级作用域。
在 JavaScript 中作用域是可以相互 嵌套 的,最外层的作用域被称为 全局作用域 ,当作用域嵌套时就形成了我们所谓的 作用域链 。当执行查询操作时,如果在当前的作用域中无法查找到相应的标识符就会顺着作用域链向外层寻找,直到查询成功就停止。假如一直到全局作用域当中都查询不到,此时引擎就会抛出 ReferenceError 异常。
这里重点来讲一下 LHS 查询与 RHS 查询在查找不到标识符时的区别:
- RHS 查询与一开始我们所讲的规则是一致的,当一路查询到全局作用域中都没有结果时就会抛出异常。
- LHS 查询稍有不同,在 严格模式 下抛出异常的规则和上述的一致。但是在 非严格模式 下查询不到结果时,引擎会在全局作用域中创建一个同名的标识符。
小结
今天为大家讲解了 作用域 的概念,现在对今天的内容做个总结:
- 作用域是一套严谨的管理和查找标识符的规则。
- 变量提升的本质原因是因为 JavaScript 代码是先编译后执行的,在编译阶段标识符的声明就已经完成了。
- 作用域分为全局作用域 、函数作用域和块级作用域。作用域是可以相互嵌套的,标识符查找会顺着嵌套关系向外层作用域移动,直至查询到全局作用域时结束,查询失败时引擎会抛出 ReferenceError 异常。
- 引擎查询标识符存在两种形式 —— LHS 和 RHS,LHS 查询标识符的内存地址,RHS 查询存放的值。