一文带你深入理解JavaScript中的作用域机制

221 阅读5分钟

JavaScript(简称JS)是一种广泛使用的编程语言,尤其在Web开发中。它的作用域机制是理解变量如何声明、初始化和访问的关键。本文将深入探讨JS的作用域,包括其执行机制、编译与执行阶段的差异、变量查找规则以及引擎、编译器和作用域之间的交互。

1. 基本概念:变量声明与赋值

当我们写一句var a = 1时,我们实际上是在进行两个步骤的操作:声明一个名为a的变量,并对其进行赋值。这个过程可以被细分为两个阶段:一个由编译器在编译时处理,另一个则由引擎在运行时处理。

  • 编译阶段:在这个阶段,编译器会解析代码并处理所有的变量声明。遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。
  • 执行阶段:这是实际执行代码的阶段。在此期间,引擎会在作用域中查找该变量,如果能够找到,就会将 1 赋值给它。否则就会抛出一个异常。
// function foo() {
//     var a = 1;
//     // var a 会被忽略
//     var a = 2;
//     console.log(a); // 2
// }


function foo() {
    var a;
    var a;//会被忽略
    a = 1;
    a = 2;//覆盖之前的1
    console.log(a); // 2
}
foo()

2. 变量的作用域

变量不会单独存在,而是属于某个特定的作用域。作用域定义了变量的生命周期及其可见性。根据作用域的不同,我们可以将其分为全局作用域、函数作用域和块级作用域等。

  • 全局作用域:当变量在所有函数外部声明时,它就处于全局作用域下,意味着可以在整个程序中访问该变量。
  • 函数作用域:在函数内部声明的变量仅限于该函数内有效。一旦函数执行完毕,这些局部变量就会从内存中移除,除非它们通过闭包的方式被捕获。
  • 块级作用域(ES6引入):使用letconst声明的变量具有块级作用域特性,即它们只在包围它们的大括号{}内有效。

3. 作用域链与变量查找

当我们在代码中引用一个变量时,JS引擎会按照一定的顺序去查找该变量:

  1. 首先检查当前作用域是否有该变量;
  2. 如果没有找到,则沿着作用域链向上一级作用域继续查找;
  3. 这个过程会一直持续到全局作用域;
  4. 若最终仍未找到该变量,则返回undefined或抛出错误,具体取决于上下文环境。

这种查找路径构成了所谓的“作用域链”。它确保了即使在嵌套函数或多层嵌套结构中,也能够正确地定位和访问变量。

var a = 1;
var b = 4;
function foo() {
    var a = 2;
    function bar() {
        var a = 3;
        console.log(a); // 3
        return a + b;
    }
    console.log(a, bar()); // 2 7
    console.log(c); // ReferenceError: c is not defined
}

foo(); 

image.png

4. 引擎、编译器与作用域的关系

为了更好地理解变量是如何被管理和访问的,我们需要了解JS引擎内部的工作原理。以Chrome浏览器中的V8引擎为例:

  • 引擎:作为整个JS执行流程的核心,负责协调编译器和作用域管理器之间的协作。
  • 编译器:主要职责是对源代码进行词法分析,即将字符串形式的代码转换成机器可读的指令集。在这个过程中,编译器会识别出所有的变量声明,并通知作用域管理器为每个作用域创建相应的上下文。
  • 作用域管理器:它就像是一个运营经理,负责维护各个作用域之间的关系,构建作用域链,并确保变量可以在正确的范围内被访问。

5. LHS与RHS查找

在讨论变量查找时,我们经常提到LHS(Left-hand side)和RHS(Right-hand side)两种不同类型的查找方式:

  • LHS查找:用于确定是否允许对某个标识符进行赋值操作。例如,在表达式a = 1中,a位于等号左侧,因此需要进行LHS查找以确认是否存在一个名为a的容器来接收这个新值。
  • RHS查找:当我们要获取一个变量的值时,如在console.log(a)这样的语句中,就需要执行RHS查找。这意呸着我们要寻找的是存储在a中的实际数据。
  • “赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头 (RHS)”。

这两种查找类型决定了变量在不同上下文中是如何被解释和使用的。

异常
  • 不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式 地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)
function foo() {
   b = 2;  //LHS 默认声明变量
}
foo();
// LHS b 全局
console.log(b); 


function foo1() {
    var c = 2;
}
foo1();
console.log(c); // ReferenceError: c is not defined

/*
"use strict";
在严格模式下,都会抛出 ReferenceError 异常
*/

image.png

  • 如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或者引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError。
var a = 10; // LHS 赋值 Number
a();  //a number 没有执行操作  TypeError: a is not a function
  • ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对 结果的操作是非法或不合理的。

6. 思考

  1. 找到其中所有的 LHS 查询。(这里有 3 处!)
  2. 找到其中所有的 RHS 查询。(这里有 4 处!)
function foo(a) {
    var b = a;
    return a + b;
}
var c = foo(2);