JavaScript:作用域

128 阅读4分钟

1.前言

JavaScript 代码是如何运行的?

想象一下,JavaScript 引擎就像一个翻译官,它需要把我们写的代码"翻译"成计算机能懂的语言。以 Chrome 使用的 V8 引擎为例,这个过程分为两大步骤:

代码解析阶段

  • 1.拆解代码(词法分析):

    • 将代码拆分为最小语法单元

          var a = 2;
          //拆分成
          词法单元 标识符 等号 数值 分号;
           var       a    =    2    ;
      
  • 2.检查语法(语法分析):

    • 验证这些"单词"组合是否符合语法规则

    • 验证语法正确性,并生成象语法树(AST),可以理解为代码的结构图

代码执行阶段

  • 根据解析结果生成计算机能执行的指令

  • 按照作用域规则查找和操作变量

2.作用域:变量的"活动范围"

  • 定义:是在程序中定义变量的可访问性和生命周期范围。

    • 作用域就像变量的"居住证",决定了:

      • 这个变量在哪里能被访问(可访问性)
      • 这个变量能"活"多久(生命周期)
  • 作用域查找规则

    • 查找方向:**由内向外
      • a. 首先在 当前作用域中查找;

      • b. 若未找到,则向上查找父级作用域;

      • c. 继续向上查找,直至全局作用域;

      • d. 若全局作用域仍未找到,则抛出 ReferenceError;


全局作用域(Global Scope):全球通行的"护照"

  • 在所有函数外部声明的变量/函数拥有全局作用域

  • 特点 :在代码任何地方都能访问,浏览器环境中挂载在 window 对象上

      var globalVar = '我是全局变量';  // 在任何地方都能用
      function showGlobal() {
         console.log(globalVar);  // 这里能访问
      }
      showGlobal();
      console.log(globalVar);    // 这里也能访问
    
  • 风险:容易造成命名冲突(就像太多人叫"张三")

函数作用域(Function Scope): 函数内部的"门禁卡"

  • 在函数内部声明的变量,仅在函数内部可访问

  • 特点 :函数参数也属于函数作用域

      function foo() {
      const localVar = '只有在这里能知道这个秘密';
      console.log(localVar); // 正常访问
      }
      foo();
      console.log(localVar); //错误:变量未定义
    
  • 好处:保护私有变量,避免外部干扰

块级作用域(Block Scope):块级{}的"临时通行证"

  • 由 let 或 const 声明的变量,在 {} 块内有效

  • 特点 : var 声明的变量没有块级作用域

      if (true) {
      let blockVar = '我是块级变量';
      const fixedVar = '常量块级变量';
      console.log(blockVar); // 正常访问
      }
      console.log(blockVar); // 错误:变量未定义
    
  • 对比:var 会无视块级作用域(老式设计)

3. var、let和const 三兄弟对比

  • var 老大哥(有历史包袱)

    • var存在变量提升,声明会被提升到作用域顶部

       consle.log(a);  // 输出undefined
       var a =1
      
      • 在编译阶段:

          var a // 声明提升,设为undefined
          consle.log(a); //输出undefined
          a = 1 // 然后赋值为1
        
    • var允许重复声明同一变量,后声明的会覆盖前面的

       // var示例
       function varTest() {
       var x = 1
       if (true) {
          var x = 10;
          console.log(x); // 10
       }
       console.log(x); // 10(函数作用域,仍可访问)
       }
       
       varTest()   
      
    • 无视块级作用域:不够安全

      if(true) {
          let d = 1
          var c = 2
      }
      console.log(d);  // ReferenceError: d is not defined
      console.log(c);  // 2
      
  • let 二弟(更严谨)

    • 声明时可以不初始化,默认值为 undefined;

    • 不允许在同一作用域内重复声明同一变量,会抛出 SyntaxError;

       // let示例
       function letTest() {
       if (true) {
          let y = 20;
          let y = 30; // SyntaxError: Identifier 'y' has already been declared
       console.log(y); // 20
       }
       console.log(y); // 错误:y没定义
      
    • 必须先声明后使用(暂时性死区)

        let f = 1
        if(true) {
            console.log(f);   // 暂时性死区
            let f = 2 
        }
      
  • const 三弟(最严格)

    • 声明时必须初始化,且初始化后不可重新赋值 (但对象属性可修改)

    • 不允许在同一作用域内重复声明同一变量,会抛出 SyntaxError;

        // const示例
        function constTest() {
            const z = 30;
            z = 40; // TypeError: Assignment to constant variable
            const obj = { a: 1 };
            obj.a = 2; // 允许修改对象属性
        }
      
    • 其他特性和 let 相同

4.实际应用建议

    1. 优先使用const :默认使用 const ,仅在需要重新赋值时使用 let
    1. 最小权限原则 :变量声明在最小必要作用域内
    1. 避免全局污染 :减少全局变量,使用模块化方案
    1. 警惕作用域链陷阱 :避免变量名冲突和意外遮蔽
    1. 理解暂时性死区 : let/const 声明前访问会抛出 ReferenceError