JavaScript作用域与作用域链详解

JavaScript 中的作用域(Scope)和作用域链(Scope Chain)。这是理解 JavaScript 如何工作的核心概念之一。

什么是作用域 (Scope)?

作用域就是变量与函数的可访问范围,即它控制着变量和函数的可见性与生命周期。换句话说,作用域决定了代码中哪些部分可以访问某个变量或函数。

JavaScript 主要有三种作用域:

  • 全局作用域
  • 函数作用域
  • 块级作用域
  1. 全局作用域

    在任何函数、代码块 {} 之外定义的变量或函数,都拥有全局作用域。

    javascript 复制代码
     // 全局变量
     var globalVar = "我是全局变量";
     let globalLet = "我是用let声明的全局变量";
     const globalConst = "我是用const声明的全局变量";
    
     // 全局函数
     function globalFunction() {
       console.log("我是一个全局函数");
     }
    
     // 在代码的任何地方都可以访问它们
     console.log(globalVar); // "我是全局变量"
     globalFunction(); // "我是一个全局函数"
    
     function someFunction() {
       console.log(globalLet); // "我是用let声明的全局变量" (在函数内也可访问)
       console.log(globalConst); // "我是用const声明的全局变量" (在函数内也可访问)
     }
     someFunction()

    注意:在浏览器环境中,全局作用域是 window 对象。使用 var 声明的全局变量会成为 window 对象的属性,但使用 let 和 const 声明的则不会。

    javascript 复制代码
     var myVar = 10;
     console.log(window.myVar); // 10
    
     let myLet = 20;
     console.log(window.myLet); // undefined
  2. 函数作用域

    在函数内部声明的变量拥有函数作用域。这意味着它们只能在定义它们的函数内部被访问。

    javascript 复制代码
     function myFunction() {
       // 函数作用域变量
       var functionScopedVar = "我在函数内部";
       let functionScopedLet = "我也是!";
    
       console.log(functionScopedVar); // 我在函数内部
       console.log(functionScopedLet); // 我也是!
     }
    
     myFunction();
     console.log(functionScopedVar); // ReferenceError: functionScopedVar is not defined
     console.log(functionScopedLet); // // ReferenceError: functionScopedLet is not defined

    关键点:var 声明的变量是函数作用域的。无论它们在函数内部的哪个位置声明(不包括子函数),在整个函数内部都是可用的(由于变量提升)。

  3. 块级作用域

    由 {} 创建的代码块(如 if 语句、for 循环、while 循环或单独使用 {})会形成一个块级作用域。只有使用 let 和 const 声明的变量才具有块级作用域

    javascript 复制代码
    if (true) {
       // 这是一个块
       var blockScopedVar = "我是用 var 声明的"; // 没有块级作用域
       let blockScopedLet = "我是用 let 声明的"; // 有块级作用域
       const blockScopedConst = "我是用 const 声明的"; // 有块级作用域
     }
    
     console.log(blockScopedVar); // "我是用 var 声明的" (因为 var 会穿透块)
     console.log(blockScopedLet); // ReferenceError: blockScopedLet is not defined
     console.log(blockScopedConst); // ReferenceError: blockScopedConst is not defined

    for循环的经典例子

    javascript 复制代码
     // 使用 var (没有块级作用域)
     for (var i = 0; i < 3; i++) {
       setTimeout(() => console.log(i), 100); // 输出 3, 3, 3
     }
     // 变量 i 是全局(或函数)作用域的,循环结束后值为 3,所有回调都引用同一个 i
    
     // 使用 let (有块级作用域)
     for (let j = 0; j < 3; j++) {
       setTimeout(() => console.log(j), 100); // 输出 0, 1, 2
     }
     // 每次迭代都会创建一个新的块级作用域变量 j,回调函数捕获的是当次迭代的 j

什么是作用域链 (Scope Chain)

当访问一个变量时,JavaScript 引擎会从当前作用域开始查找该变量。如果找不到,它会向外层 的作用域继续查找,一直找到全局作用域为止。这种逐级向上查找的链条关系,就叫做作用域链
作用域链是在函数定义时就被确定下来的,而不是在函数调用时。

javascript 复制代码
  let globalVariable = "I'm global";

  function outerFunction() {
    let outerVariable = "I'm outer";

    function innerFunction() {
      let innerVariable = "I'm inner";
      console.log(innerVariable); // 在当前作用域找到 "I'm inner"
      console.log(outerVariable); // 在当前作用域没找到,向上到 outerFunction 的作用域找到 "I'm outer"
      console.log(globalVariable); // 在当前和 outer 都没找到,继续向上到全局作用域找到 "I'm global"
    }

    innerFunction();
  }

  outerFunction();

查找过程图示
当在 innerFunction 中访问变量时,查找顺序如下:
innerFunction 作用域 -> outerFunction 作用域 -> 全局作用域

这个链条关系在 innerFunction 被定义的时候就已经固定下来了,无论它在哪里被调用。

词法作用域

JavaScript 采用的作用域模型是词法作用域 (也称为静态作用域)。这意味着作用域是由代码中函数和块被书写的位置决定的,而不是由函数被调用的位置决定的。

javascript 复制代码
  let a = 10;

  function foo() {
    console.log(a);
  }

  function bar() {
    let a = 20;
    foo(); // 问:这里会输出什么?
  }

  bar(); // 输出 10,而不是 20!

解释:

函数 foo 在定义时,它的作用域链就已经确定了:foo 作用域 -> 全局作用域

当在 bar 内部调用 foo 时,foo 会沿着它自己定义时的作用域链去查找变量 a,它在全局作用域中找到了 a = 10bar 内部定义的 a = 20 属于 bar 的作用域,不在 foo 的作用域链上,所以 foo 无法访问它。

总结

概念 描述
作用域 变量和函数的可访问性范围。分为全局、函数和块级作用域。
全局作用域 在最外层定义,任何地方都可访问。
函数作用域 在函数内部定义,只能在函数内部访问。var 是函数作用域。
块级作用域 由 {} 创建,let 和 const 声明的变量仅限于块内访问。
作用域链 查找变量时,从当前作用域向外层作用域逐级查找的链条关系。
词法作用域 作用域在代码书写阶段(定义时)就被确定,而不是在执行时。这是理解闭包的关键。
相关推荐
泉城老铁2 小时前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅2 小时前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸2 小时前
Prompt结构化输出:从入门到精通的系统指南
前端
我是日安2 小时前
从零到一打造 Vue3 响应式系统 Day 9 - Effect:调度器实现与应用
前端·vue.js
顾林海2 小时前
Android编译插桩之AspectJ:让代码像特工一样悄悄干活
android·面试·性能优化
poemyang2 小时前
技术圈的“绯闻女孩”:Gossip是如何把八卦秘密传遍全网的?
后端·面试·架构
Mintopia2 小时前
🚀 Next.js 全栈 E2E 测试:Playwright vs Cypress
前端·javascript·next.js
原生高钙2 小时前
JS设计模式指南
前端·javascript