每日读《你不知道的JavaScript(上)》 | 作用域

107 阅读5分钟

引言

作用域是 针对存储在变量中的值 的概念。

即本质上是存储变量的规则。

简单理解就是,变量在什么地方起到作用,这个“地方”,就叫做 作用域

由此引出一个问题,就是,我们怎么规定这个“地方”呢?

这就涉及到定义作用域的规则了。

编译原理

在深入理解作用域之前,需要掌握一些编译原理的基础知识。

在一般情况下的编程语言里,编译的步骤可以总结为三个part:

  • 分词/词法分析

将由字符组成的字符串分解成有意义的代码块。

这些代码块的学名叫 词法单元

而这里“有意义的代码块”指的是,参与代码逻辑运行的词法单元。

例如:

var a = 1; ----> vara=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;编译器和引擎的处理流程如下:

未命名文件 (1).png

变量 的赋值操作会执行两个动作:

  1. 编译器会在当前作用域中声明一个变量(如果没有声明过)。

  2. 运行时引擎会在作用域中查找该变量,如果能找到就对其进行赋值操作。

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叭~