引言
作用域是 针对存储在变量中的值 的概念。
即本质上是存储变量的规则。
简单理解就是,变量在什么地方起到作用,这个“地方”,就叫做 作用域。
由此引出一个问题,就是,我们怎么规定这个“地方”呢?
这就涉及到定义作用域的规则了。
编译原理
在深入理解作用域之前,需要掌握一些编译原理的基础知识。
在一般情况下的编程语言里,编译的步骤可以总结为三个part:
- 分词/词法分析
将由字符组成的字符串分解成有意义的代码块。
这些代码块的学名叫 词法单元。
而这里“有意义的代码块”指的是,参与代码逻辑运行的词法单元。
例如:
var a = 1;
----> var
、a
、=
、1
、;
- 解析/语法分析
将词法单元流(数组)转换成一个由元素逐级嵌套所组成的、代表了程序语法结构的 tree。
这个 tree 叫 “抽象语法树”,即我们经常见到的 AST(Abstract Syntax Tree)。
嵌套层级大概是,var
> a
> 1
- 代码生成
将 AST 转换为可执行代码的过程。
其实就是,把 AST 转换为一组机器指令。
对于var a = 1;
来说,这个机器指令的作用就是创建一个叫做 a 的变量,并将 1 这个值存储在变量 a 中。
JavaScript 的编译时机和其他编程语言不同。
JavaScript | 其他编程语言 |
---|---|
代码执行前几微秒(甚至更短) | 构建之前 |
简单来说,任何 JavaScript 代码片段在执行前都要进行编译。
因此,JavaScript 编译器首先会对
var a = 1;
这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。
对于var a = 1;
编译器和引擎的处理流程如下:
变量 的赋值操作会执行两个动作:
-
编译器会在当前作用域中声明一个变量(如果没有声明过)。
-
运行时引擎会在作用域中查找该变量,如果能找到就对其进行赋值操作。
LHS和RHS的简单了解
引擎查找的过程由 作用域 进行协助,但是引擎执行怎样的查找,会影响最终的查找结果。
LHS:赋值操作的目标是谁
例如,a = 2
。
只想为 = 2 找到一个赋值的目标。
RHS:谁是赋值操作的源头
例如,console.log(a)
。
查找a的值,才能传递给console.log
。
作用域嵌套
拿代码举例子:
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(2);
对b进行的 RHS 引用无法在函数foo内部完成,但可以在上一级作用域(全局)中完成。
遍历嵌套作用域的规则如下:
引擎从当前的执行作用域开始查找变量,如果找不到,就会向 上一级 继续查找。
当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
假如我们没有在全局声明var b = 2;
,那么在对b进行 RHS 查询时,在所有嵌套作用域中都找不到该变量,那么引擎就会抛出 ReferenceError 异常。
相比之下,当引擎执行的是 LHS 查询时,如果在全局作用域中也无法找到目标变量,全局作用域就会创建一个具有该名称的变量,并将其返还给引擎(前提是程序运行在非“严格模式”下)。
这是在变量声明缺失情况下,进行 RHS 查询和 LHS 查询的区别。
但是,即便 RHS 查询找到了一个变量,但你尝试对这个变量的值进行不合理的操作,比如对一个非函数类型(String、Number等)的值进行函数调用,或null.属性
或undefined.属性
,那么引擎会抛出另外一种类型的异常,叫TypeError。
ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
上述规律有助于帮助我们在做项目过程中快速定位bug来源,以提高解决问题的效率。
Case 练习
No.1
var a = 12;
function fn() {
console.log(a);
var a = 45;
console.log(a);
}
fn();
// 输出如下:
// undefined
// 45
引擎查找过程可以分解为:
var a = 12; // 查找触及不到该作用域(全局)里的a
function fn() {
// 想打印a的值,就会先在当前作用域中查找变量a
var a; // 变量提升
console.log(a); // 此时打印出来的是undefined
a = 45;
console.log(a); // 45
}
fn();
// 输出如下:
// undefined
// 45
No.2
var a = 12;
function fn() {
console.log(a);
a = 45;
console.log(a);
}
fn();
// 输出如下:
// 12
// 45
No.3
function fn() {
console.log(1);
function fn1() {
console.log(2);
}
fn1();
}
fn();
fn1();
// 输出如下:
// 1
// 2
// ReferenceError: fn1 is not defined
总结
作用域是一套用于确定在何处以及如何查找变量的规则。
我们主要介绍了RHS和LHS查询,还有JavaScript引擎的编译过程,在编译过程中,作用域和这两种查询方式是强关联的,因为查找变量的过程本身需要依赖这两种查询方式,而作用域则规定了查询的方式。
最后,本文声明原创,属于个人在读书过程中的记录和理解,如有误解的地方,欢迎大家在评论区指出,我也会和大家讨论并及时更正的~
我也会尽量日更的,也算将来入职前对自己的push叭~