JavaScript作用域深度剖析:从局部到全局一网打尽

简介: JavaScript作用域深度剖析:从局部到全局一网打尽

JavaScript作用域深度剖析:从局部到全局一网打尽

1.1 编译原理


  • JavaScript 事实上是一门编译语言
  • • 在传统编译语言中,一段源代码执行前会经历三个步骤:
  1. 1. 分词/词法分析(Tokenizing/Lexing)

var a =2;
// 分解后:
vara、=、2、;
// 空格是否会被当做词法单元,取决于空格在这门语言中是否具有意义。

  • • 期间经过两个过程:分词(tokenizing)和词法分析(Lexing) 、两者的主要差别在于词法单元的识别是通过有状态还是无状态的方式进行的。
  1. 1. 解析/语法分析(Parsing)
  • • 这个过程就是将词法单元流(数组)转换为一个由元素逐级嵌套组成的代表了程序语法结构的树,这个树被称为"抽象语法树"。(Abstract Syntax Tree, AST)。
  1. 2. 代码生成
  • • 将 AST 转换为可执行代码的过程被称为代码生成。也就是说有某种方法将 var a = 2; 的 AST 转换为一组机器指令,用来创建一个叫做 a 的变量(包含分配内存等),将一个值储存于 a 中。
  • • 比起其他编译过程只有这三个步骤的语言的编译器,JavaScript 引擎要复杂得多,在语法分析和代码生成阶段有着特定的步骤来对比运行性能进行优化,包括对冗余元素进行优化等。
  • 简单来说,任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)

1.2 理解作用域


1.2.1 演员表

  • 引擎:从头到尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器:引擎的好朋友之一,负责语法分析及代码生成等脏活累活。
  • 作用域:引擎的另一个好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实行一套严格的规则,确定当前执行的代码对这些标识符的访问权限。

1.2.2 对话

  • var a = 2; 这段代码是一句声明。但会经过编译器和引擎的处理来进行。
  • • S: 变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该便令,如果能够找到就会对它进行赋值。

1.2.3 编译器有话说


  • • 编译器在编译过程中的第二步中生成了代码,引擎执行它时,会通过查找变量 a 来判断他是否已声明过。查找的过程由作用域进行协助,但是引擎执行怎样的查找会影响最终的查找结果。
  • • 引擎常使用的查询类型为:LHS和RHS
  • LHS: 赋值操作的目标是谁
  • RHS: 谁是赋值操作的源头


1.2.5

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo(2);
// 对话:
1. 声明 var c
2. 对 c 进行 LHS
3. 对 foo(2) 进行 RHS
4. function foo(a) 期间会进行 a = 2, 对 a 进行 LHS
5. 声明 var b
6. 对 b 进行 LHS
7. 对 a 进行 RHS
8. return a + b; 分别对 a、b 进行 RHS
// 答案:
1. 所有的 LHS(一共有3处)
    1. c =..;
    2. a = 2(隐士变量分配)
    3. b = ..
2. 所有的 RHS (一共有4处)
    1. foo(2..
    2. = a;
    3. a..
    4. .. b


1.3 作用域嵌套


  • 作用域是根据名称查找变量的一套规则
  • 当一个块或函数嵌套在另一个块或函数中时,就会发生作用域的嵌套。因此在当前作用域中无法找到某个变量时,引擎就会在外层作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
// 非严格模式下
function foo(a) {
    console.log(a + b);
}
var a = 2;
foo(2); // 4
// 严格模式下:
function foo(a) {
    console.log(a + b);
}
var a = 2;
foo(2); // 4
  • • 遍历嵌套作用域链的规则:引擎会从当前的执行作用域中开始查找变量,如果找不到就会向上一级中继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找的过程都会停止。
  • • 例子:


  • • 整个建筑代表程序中的嵌套作用域链,第一层楼代表当前的执行作用域,也就是你所处的位置。建筑的顶层代表全局作用域。
  • • 引擎查找的方式:LHS 和 RHS 引用会先在当前楼层中进行查找,如果没找到,就会坐电梯前往上一层楼楼,如果还是没找到就会继续上下,以此类推。一旦达到了顶层(全局作用域), 可能找到你了你所需的变量,也可能没找到,但无论如何查找过程都会停止。


1.4 异常


  • • 为什么区分 LHS 与 RHS 是一种重要的事?
  • • 因为在变量还未声明(在任何作用域中都无法找到该变量)的情况下,引擎的这两种查询行为是不一样的。
// 非严格模式下:
function foo(a) {
    console.log(a + b);
    b = a;
}
foo(2); // 4
// 严格模式下:
'use strict';
function foo(a) {
    console.log(a + b);
    b = a;
}
foo(2); // ReferenceError: b is not defined
  • • 上述代码引擎行为:
  • • 非严格模式下:
  1. 1. 第一次对 b(.. + b) 进行 RHS 查询时未找到该变量,也就是说,这是一个"未声明" 的变量,因为在任何相关的作用域都无法找到它。
  2. 2. 第二次对 b(b = ..) 进行 LHS 查询时,如果在顶层(全局作用域)中也没找到该变量,就会在全局作用域中隐式地创建一个该名称的变量,并将其返回给引擎。
  3. 3. ......
  • • 严格模式下:
  1. 1. 第一次对 b(.. + b) 进行 RHS 查询时未找到该变量,也就是说,这是一个"未声明" 的变量,因为在任何相关的作用域都无法找到它,直接抛出 'ReferenceError'。
  2. 2. ......
  • 非严格模式下引擎查找规则
  1. 1. 当引擎执行 RHS 查询在所有嵌套的作用域中找不到所需的变量,引擎就会抛出 ReferenceError 异常。
  2. 2. 当引擎执行 LHS 查询时,如果在顶层作用域中也无法找到该变量,全局作用域就会创建一个该名称的变量,并将其返回给引擎(非严格模式下)。
  • 严格模式下引擎查找规则
  1. 1. ES5 引入了 "严格模式"(use strict),在行为上有很多不同,其中一个不同的行为就是严格模式下禁止自动或隐式地创建全局变量。因此在严格模式中引擎执行 LHS 查询失败时,并不会创建一个全局变量,而是直接抛出一个 ReferenceError
  2. 2. 如果 RHS 找到了一个变量,但尝试对这个变量进行一些不合理的操作时,比如对一个非函数类型的值进行函数调用,或者引用 nullundefined 类型的之中属性,那引擎则会抛出另外一种类型的异常 TypeError。
  • ReferenceError 同作用域判断失败相关,而 TypeError 代表作用域判别成功了,但对结果的操作是非法或不合理的。


1.5 小结


  1. 1. 作用域是根据名称查找变量的一套规则。
  2. 2. 引擎常使用的查询类型为:LHS 和 RHS
  • = 操作符在调用函数时的形参会导致关联作用的赋值操作。也就是说 foo (a, b, c...), 都会有 a = xxx, b = xxx, c = xxx ...... 的行为。
  • LHS: 赋值操作的目标是谁
  • RHS: 谁是赋值操作的源头
  1. 3. 非严格模式下引擎查找规则
  1. 1. 当引擎执行 RHS 查询在所有嵌套的作用域中找不到所需的变量,引擎就会抛出 ReferenceError 异常。
  2. 2. 当引擎执行 LHS 查询时,如果在顶层作用域中也无法找到该变量,全局作用域就会创建一个该名称的变量,并将其返回给引擎(非严格模式下)。
  1. 4. 严格模式下引擎查找规则
  1. 1. ES5 引入了 "严格模式"(use strict),在行为上有很多不同,其中一个不同的行为就是严格模式下禁止自动或隐式地创建全局变量。因此在严格模式中引擎执行 LHS 查询失败时,并不会创建一个全局变量,而是直接抛出一个 ReferenceError
  2. 2. 如果 RHS 找到了一个变量,但尝试对这个变量进行一些不合理的操作时,比如对一个非函数类型的值进行函数调用,或者引用 nullundefined 类型的之中属性,那引擎则会抛出另外一种类型的异常 TypeError。
  1. 5. ReferenceError 同作用域判断失败相关,而 TypeError 代表作用域判别成功了,但对结果的操作是非法或不合理的。

特殊字符描述

问题标注 Q:(question)答案标注 R:(result)注意事项标准:A:(attention matters)详情描述标注:D:(detail info)总结标注:S:(summary)分析标注:Ana:(analysis)提示标注:T:(tips)

相关文章
|
1月前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
1月前
|
自然语言处理 JavaScript 前端开发
[JS]作用域的“生产者”——词法作用域
本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。
35 2
[JS]作用域的“生产者”——词法作用域
|
1月前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。
|
1月前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域
|
2月前
|
JavaScript 前端开发
JavaScript 作用域
JavaScript 作用域是指程序中可访问的变量、对象和函数的集合。它分为函数作用域和局部作用域。函数作用域内的变量仅在函数内部可见,而全局作用域的变量在整个网页中均可访问。局部变量在函数执行完毕后会被销毁,而全局变量则在整个脚本生命周期中都存在。未使用 `var` 关键字声明的变量默认为全局变量。
|
2月前
|
JavaScript 前端开发
js作用域
js作用域
18 1
|
28天前
|
JavaScript 前端开发
JavaScript中的原型 保姆级文章一文搞懂
本文详细解析了JavaScript中的原型概念,从构造函数、原型对象、`__proto__`属性、`constructor`属性到原型链,层层递进地解释了JavaScript如何通过原型实现继承机制。适合初学者深入理解JS面向对象编程的核心原理。
25 1
JavaScript中的原型 保姆级文章一文搞懂
|
5月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
103 2
|
25天前
JS+CSS3文章内容背景黑白切换源码
JS+CSS3文章内容背景黑白切换源码是一款基于JS+CSS3制作的简单网页文章文字内容背景颜色黑白切换效果。
17 0