深入理解 JavaScript 作用域与作用域链

故事开始(不说废话、直接上干货)

作用域就像是变量和函数的「居住范围」,它决定了:

  • 变量和函数可以被访问的区域
  • 变量的生命周期(何时创建,何时销毁)
  • 同名变量之间的优先级关系

作用域的分类

1. 全局作用域

  • 代码中最外层的作用域
  • 在全局作用域中声明的变量,整个程序都能访问
  • 浏览器环境中,全局作用域由 window 对象代表
javascript 复制代码
// 全局作用域
const globalVar = "我是全局变量";

function sayHello() {
  // 函数内部可以访问全局变量
  console.log(globalVar); // 输出:我是全局变量
}

sayHello();
console.log(globalVar); // 输出:我是全局变量

图解:全局作用域

ini 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ globalVar = "我是全局变量"                           │  │
│  └───────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ sayHello() { ... }                                    │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

2. 函数作用域

  • 在函数内部声明的变量,只能在函数内部访问
  • 函数执行完毕后,其作用域内的变量会被销毁(闭包除外)
javascript 复制代码
function myFunction() {
  // 函数作用域
  const localVar = "我是局部变量";
  console.log(localVar); // 输出:我是局部变量
}

myFunction();
console.log(localVar); // 报错:localVar is not defined

图解:函数作用域

sql 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ myFunction() {                                       │  │
│  │   ┌───────────────────────────────────────────────┐   │  │
│  │   │ 函数作用域 (Function Scope)                    │   │  │
│  │   │  localVar = "我是局部变量"                     │   │  │
│  │   └───────────────────────────────────────────────┘   │  │
│  │ }                                                   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3. 块级作用域

  • {} 包裹的代码块(如 ifforwhileswitch 等)
  • 使用 letconst 声明的变量,只在当前代码块内有效
  • ES6 引入,解决了「变量提升」带来的问题
javascript 复制代码
if (true) {
  // 块级作用域
  let blockVar = "我是块级变量";
  console.log(blockVar); // 输出:我是块级变量
}

console.log(blockVar); // 报错:blockVar is not defined

图解:块级作用域

sql 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ if (true) {                                          │  │
│  │   ┌───────────────────────────────────────────────┐   │  │
│  │   │ 块级作用域 (Block Scope)                       │   │  │
│  │   │  blockVar = "我是块级变量"                     │   │  │
│  │   └───────────────────────────────────────────────┘   │  │
│  │ }                                                   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

词法作用域 vs 动态作用域

词法作用域(JavaScript 使用)

  • 定义 :变量的作用域由它在代码中书写的位置决定
  • 特点 :作用域在代码编译阶段就确定了,与函数的调用位置无关
  • 优点:代码的可读性和可维护性更高
javascript 复制代码
const name = "全局";

function outer() {
  const name = "外层";
  
  function inner() {
    // inner 函数的词法作用域包含:自身作用域 → outer 作用域 → 全局作用域
    console.log(name); // 输出:外层(因为 inner 定义在 outer 内部)
  }
  
  return inner;
}

const innerFunc = outer();
innerFunc(); // 调用位置在全局,但作用域由定义位置决定

图解:词法作用域

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│  name = "全局"                                              │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ outer() {                                            │  │
│  │   name = "外层"                                       │  │
│  │   ┌───────────────────────────────────────────────┐   │  │
│  │   │ inner() {                                     │   │  │
│  │   │   // 词法作用域链:inner → outer → 全局        │   │  │
│  │   │   console.log(name); // 输出:外层             │   │  │
│  │   │ }                                             │   │  │
│  │   └───────────────────────────────────────────────┘   │  │
│  │ }                                                   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  innerFunc = outer(); // 获得 inner 函数引用                │
│  innerFunc(); // 调用 inner 函数                           │
└─────────────────────────────────────────────────────────────┘

动态作用域(JavaScript 不使用)

  • 定义 :变量的作用域由函数的调用位置决定
  • 特点 :作用域在代码运行阶段才确定
  • 缺点:代码的行为难以预测,调试困难

图解:动态作用域(对比示例)

scss 复制代码
# 如果 JavaScript 使用动态作用域
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│  name = "全局"                                              │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ outer() {                                            │  │
│  │   name = "外层"                                       │  │
│  │   ┌───────────────────────────────────────────────┐   │  │
│  │   │ inner() {                                     │   │  │
│  │   │   // 动态作用域链:inner → 调用位置的作用域     │   │  │
│  │   │   console.log(name);                          │   │  │
│  │   │ }                                             │   │  │
│  │   └───────────────────────────────────────────────┘   │  │
│  │ }                                                   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  innerFunc = outer(); // 获得 inner 函数引用                │
│  innerFunc(); // 调用位置在全局,所以作用域链:inner → 全局  │
│  // 输出:全局(这是动态作用域的行为,JavaScript 不这样)   │
└─────────────────────────────────────────────────────────────┘

什么是作用域链?

作用域链是 JavaScript 中变量查找的「路径」,当代码需要访问一个变量时:

  1. 首先在当前作用域中查找
  2. 如果找不到,就沿着作用域链向上查找
  3. 直到找到该变量,或者到达全局作用域仍未找到(此时会报错或创建全局变量,取决于声明方式)

作用域链的形成

  • 每个函数在创建时,会记住它的「父级作用域」
  • 当函数被调用时,会创建一个新的「执行上下文」
  • 每个执行上下文都包含一个「作用域链」,由当前作用域和所有父级作用域组成

作用域链的查找规则

javascript 复制代码
// 全局作用域
const globalVar = "全局";

function outer() {
  // outer 作用域
  const outerVar = "外层";
  
  function inner() {
    // inner 作用域
    const innerVar = "内层";
    
    // 变量查找路径:inner 作用域 → outer 作用域 → 全局作用域
    console.log(innerVar); // 内层(当前作用域找到)
    console.log(outerVar); // 外层(向上一级查找)
    console.log(globalVar); // 全局(向上两级查找)
    console.log(nonExistentVar); // 报错:nonExistentVar is not defined(全局作用域也没找到)
  }
  
  inner();
}

outer();

图解:作用域链的形成与查找

ini 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│  globalVar = "全局"                                         │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ outer() {                                            │  │
│  │   outerVar = "外层"                                   │  │
│  │   ┌───────────────────────────────────────────────┐   │  │
│  │   │ inner() {                                     │   │  │
│  │   │   innerVar = "内层"                             │   │  │
│  │   │   ┌─────────────────────────────────────────┐   │   │  │
│  │   │   │ 变量查找过程:                          │   │   │  │
│  │   │   │ 1. 查找 innerVar → inner 作用域找到     │   │   │  │
│  │   │   │ 2. 查找 outerVar → outer 作用域找到     │   │   │  │
│  │   │   │ 3. 查找 globalVar → 全局作用域找到     │   │   │  │
│  │   │   │ 4. 查找 nonExistentVar → 未找到,报错  │   │   │  │
│  │   │   └─────────────────────────────────────────┘   │   │  │
│  │   │ }                                             │   │  │
│  │   └───────────────────────────────────────────────┘   │  │
│  │ }                                                   │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

更详细的作用域链例子

javascript 复制代码
// 全局作用域
const a = 1;

function outer() {
  // outer 作用域
  const b = 2;
  
  function middle() {
    // middle 作用域
    const c = 3;
    
    function inner() {
      // inner 作用域
      const d = 4;
      
      // 作用域链:inner → middle → outer → 全局
      console.log(a); // 1 (全局)
      console.log(b); // 2 (outer)
      console.log(c); // 3 (middle)
      console.log(d); // 4 (当前)
    }
    
    inner();
  }
  
  middle();
}

outer();

图解:多层嵌套的作用域链

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│  a = 1                                                      │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ outer() {                                             │  │
│  │   b = 2                                               │  │
│  │   ┌───────────────────────────────────────────────┐   │  │
│  │   │ middle() {                                      │   │  │
│  │   │   c = 3                                         │   │  │
│  │   │   ┌─────────────────────────────────────────┐   │   │  │
│  │   │   │ inner() {                               │   │   │  │
│  │   │   │   d = 4                                 │   │   │  │
│  │   │   │   console.log(a); // 1 (全局)           │   │   │  │
│  │   │   │   console.log(b); // 2 (outer)           │   │   │  │
│  │   │   │   console.log(c); // 3 (middle)         │   │   │  │
│  │   │   │   console.log(d); // 4 (当前)           │   │   │  │
│  │   │   │   // 作用域链:inner → middle → outer → 全局 │   │   │  │
│  │   │   │ }                                       │   │   │  │
│  │   │   └─────────────────────────────────────────┘   │   │  │
│  │   │ }                                             │   │  │
│  │   └───────────────────────────────────────────────┘   │  │
│  │ }                                                   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  outer();                                                  │
└─────────────────────────────────────────────────────────────┘

闭包与作用域链

闭包的定义

闭包是指:

  • 一个函数可以访问其词法作用域之外的变量
  • 即使外部函数已经执行完毕,其作用域仍被内部函数「记住」并访问

闭包的形成条件

  1. 存在嵌套函数(内部函数定义在外部函数内部)
  2. 内部函数引用了外部函数的变量
  3. 内部函数被外部函数返回,并在外部被调用

其实一旦函数被调用,其实就创建了一个闭包closure(包含了外部函数的活动变量对象或者全局对象)

闭包示例

javascript 复制代码
function createCounter() {
  let count = 0; // 外部函数的变量
  
  return function() {
    // 内部函数引用了外部函数的 count 变量
    count++; // 访问外部变量,形成闭包
    return count;
  };
}

const counter = createCounter(); // 外部函数执行完毕,但 count 变量被闭包保留

console.log(counter()); // 输出:1
console.log(counter()); // 输出:2
console.log(counter()); // 输出:3

图解:闭包的形成与工作原理

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ createCounter() {                                    │  │
│  │   ┌───────────────────────────────────────────────┐   │  │
│  │   │ 函数作用域 (Function Scope)                    │   │  │
│  │   │  count = 0                                     │   │  │
│  │   │  ┌─────────────────────────────────────────┐   │   │  │
│  │   │  │ 返回的匿名函数:                         │   │   │  │
│  │   │  │ function() {                              │   │   │  │
│  │   │  │   count++;                                │   │   │  │
│  │   │  │   return count;                           │   │   │  │
│  │   │  │ }                                          │   │   │  │
│  │   │  │ // 引用了外部函数的 count 变量             │   │   │  │
│  │   │  └─────────────────────────────────────────┘   │   │  │
│  │   └───────────────────────────────────────────────┘   │  │
│  │ }                                                   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  counter = createCounter(); // 获得闭包函数                 │
│  // createCounter 执行完毕,但它的作用域被闭包保留         │
│                                                             │
│  console.log(counter()); // 1 → count 被修改为 1           │
│  console.log(counter()); // 2 → count 被修改为 2           │
│  console.log(counter()); // 3 → count 被修改为 3           │
└─────────────────────────────────────────────────────────────┘

多个闭包实例的独立性

javascript 复制代码
function createCounter() {
  let count = 0;
  
  return function() {
    count++;
    return count;
  };
}

const counter1 = createCounter(); // 闭包实例 1
const counter2 = createCounter(); // 闭包实例 2

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1(独立的闭包,count 不共享)
console.log(counter2()); // 2

图解:多个闭包实例的独立性

ini 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ counter1 = createCounter();                           │  │
│  │ ┌─────────────────────────────────────────────────┐   │  │
│  │ │ 闭包实例 1 的作用域                               │   │  │
│  │ │  count = 2 // 调用两次后                          │   │  │
│  │ └─────────────────────────────────────────────────┘   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ counter2 = createCounter();                           │  │
│  │ ┌─────────────────────────────────────────────────┐   │  │
│  │ │ 闭包实例 2 的作用域                               │   │  │
│  │ │  count = 2 // 调用两次后                          │   │  │
│  │ └─────────────────────────────────────────────────┘   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  // 两个闭包实例的作用域相互独立,count 变量不共享          │
└─────────────────────────────────────────────────────────────┘

闭包的经典应用场景

1. 数据私有化

javascript 复制代码
function createPerson(name) {
  let age = 0; // 私有变量
  
  return {
    getName: function() {
      return name;
    },
    getAge: function() {
      return age;
    },
    setAge: function(newAge) {
      if (newAge >= 0 && newAge <= 120) {
        age = newAge;
      }
    }
  };
}

const person = createPerson("张三");
console.log(person.getName()); // 张三
console.log(person.getAge()); // 0
person.setAge(25);
console.log(person.getAge()); // 25
person.age = 100; // 尝试直接修改,无效
console.log(person.getAge()); // 25

图解:闭包实现数据私有化

javascript 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     全局作用域 (Global Scope)               │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ person = createPerson("张三");                       │  │
│  │ ┌─────────────────────────────────────────────────┐   │  │
│  │ │ 闭包作用域 - 私有数据                            │   │  │
│  │ │  name = "张三"                                     │   │  │
│  │ │  age = 25 // 被 setAge(25) 修改后                │   │  │
│  │ └─────────────────────────────────────────────────┘   │  │
│  │  ┌─────────────────────────────────────────────────┐   │  │
│  │  │ 返回的对象,包含闭包函数:                       │   │  │
│  │  │ {                                                │   │  │
│  │  │   getName: function() { return name; },         │   │  │
│  │  │   getAge: function() { return age; },           │   │  │
│  │  │   setAge: function(newAge) { ... }              │   │  │
│  │  │ }                                                │   │  │
│  │  └─────────────────────────────────────────────────┘   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  // 外部无法直接访问 name 和 age,只能通过闭包函数访问       │
└─────────────────────────────────────────────────────────────┘

2. 函数柯里化

javascript 复制代码
function multiply(a) {
  return function(b) {
    return a * b;
  };
}

const multiplyBy2 = multiply(2);
const multiplyBy5 = multiply(5);

console.log(multiplyBy2(3)); // 6
console.log(multiplyBy5(4)); // 20

常见误区与最佳实践

1. 变量提升问题

问题 :使用 var 声明的变量会「提升」到作用域顶部,可能导致意外行为

解决 :优先使用 letconst,它们支持块级作用域,不会发生变量提升

javascript 复制代码
// 变量提升的问题
console.log(hoistedVar); // 输出:undefined(变量被提升,但未赋值)
var hoistedVar = "我被提升了";

// 使用 let 避免提升
console.log(notHoistedVar); // 报错:Cannot access 'notHoistedVar' before initialization
let notHoistedVar = "我不会被提升";

2. 循环中的闭包问题

问题:在循环中创建函数,可能导致所有函数共享同一个变量

解决 :使用 let 声明循环变量,或使用立即执行函数表达式(IIFE)

javascript 复制代码
// 问题代码
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出:3 3 3(所有函数共享同一个 i)
  }, 1000);
}

// 解决方案 1:使用 let
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出:0 1 2(每个函数都有独立的 i)
  }, 1000);
}

// 解决方案 2:使用 IIFE
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j); // 输出:0 1 2
    }, 1000);
  })(i);
}

3. 全局作用域污染

问题:过多的全局变量会导致命名冲突,影响代码的可维护性

解决

  • 减少全局变量的使用
  • 使用模块化(ES Modules、CommonJS 等)
  • 使用命名空间或立即执行函数表达式
javascript 复制代码
// 避免全局变量污染
(function() {
  // 所有变量都在 IIFE 内部,不会污染全局作用域
  const privateVar = "我是私有变量";
  
  function privateFunction() {
    console.log(privateVar);
  }
  
  // 只暴露必要的接口到全局
  window.myApp = {
    publicMethod: privateFunction
  };
})();

myApp.publicMethod(); // 输出:我是私有变量
console.log(privateVar); // 报错:privateVar is not defined

交互式演示

为了帮助你更直观地理解作用域和作用域链,我创建了两个交互式演示页面:

1. 作用域链演示

功能

  • 分步执行代码,观察作用域链的变化
  • 动态高亮显示当前活动作用域和变量
  • 查看变量查找的完整过程
  • 包含执行日志和操作说明

2. 闭包演示

功能

  • 展示闭包如何保留外部函数的作用域
  • 演示多个闭包实例的独立性
  • 动态显示闭包的创建和调用过程
  • 包含详细的闭包信息说明

总结

  1. 作用域是变量和函数的居住范围,决定了它们的可访问性
  2. 作用域分类:全局作用域、函数作用域、块级作用域
  3. 词法作用域:JavaScript 使用的作用域类型,由代码书写位置决定
  4. 作用域链:变量查找的路径,从当前作用域向上查找
  5. 闭包:内部函数可以访问外部函数的变量,即使外部函数已执行完毕
  6. 最佳实践 :优先使用 letconst,避免全局变量污染,注意循环中的闭包问题

通过理解作用域和作用域链,你可以写出更安全、更可维护的 JavaScript 代码。建议结合交互式演示页面,亲自操作体验变量查找和闭包的工作原理。

参考资料

相关推荐
r***013839 分钟前
SpringBoot3 集成 Shiro
android·前端·后端
前端一课41 分钟前
【vue高频面试题】第 11 题:Vue 的 `nextTick` 是什么?为什么需要它?底层原理是什么?
前端·面试
前端一课42 分钟前
【vue高频面试题】第 10 题:`watch` VS `watchEffect` 的区别是什么?触发时机有什么不同?
前端·面试
h***34631 小时前
SpringBoot3.3.0集成Knife4j4.5.0实战
android·前端·后端
Yanni4Night1 小时前
数据可视化神器Heat.js:让你的数据热起来
前端·javascript
Lazy_zheng1 小时前
前端页面更新检测实战:一次关于「用户不刷新」的需求拉扯战
前端·vue.js·性能优化
前端一课1 小时前
【vue高频面试题】第9题:Vue3 的响应式原理是什么?和 Vue2 的响应式有什么区别?为什么 Vue3 改用了 Proxy?
前端·面试
Demon--hx1 小时前
[C++]迭代器失效问题
前端·c++
GISer_Jing1 小时前
前端架构学习
前端·学习·架构