JavaScript 函数作用域详解——为什么函数外面访问不到里面的变量?

JavaScript 函数作用域详解------为什么函数外面访问不到里面的变量?

从一个报错说起

javascript 复制代码
function outer() {
    let functionVar = "我是函数作用域变量";

    function inner() {
        console.log(functionVar); // ✅ 可以访问
    }

    inner(); // 输出:我是函数作用域变量
    console.log(functionVar); // ✅ 输出:我是函数作用域变量
}

outer();
console.log(functionVar); // ❌ ReferenceError: functionVar is not defined

最后一行代码报错了:ReferenceError: functionVar is not defined

为什么? 明明 functionVar 已经定义了,为什么说它"未定义"?

答案就藏在 JavaScript 的作用域(Scope) 机制中。


一、什么是作用域?

作用域就是变量的"可见范围"。

你可以把作用域想象成一个房间

在房间里面的人(代码),可以看到房间里的东西(变量)。

但房间外面的人,看不到房间里的东西。

复制代码
┌─── 全局作用域(大街上)─────────────────────────┐
│                                                  │
│   console.log(functionVar) ← ❌ 这里看不到!     │
│                                                  │
│   ┌─── outer() 函数作用域(房间)──────────────┐ │
│   │                                            │ │
│   │   let functionVar = "我是函数作用域变量"     │ │
│   │                                            │ │
│   │   console.log(functionVar) ← ✅ 看得到      │ │
│   │                                            │ │
│   │   ┌─── inner() 函数作用域(房间里的柜子)─┐│ │
│   │   │                                      ││ │
│   │   │  console.log(functionVar) ← ✅ 看得到 ││ │
│   │   │                                      ││ │
│   │   └──────────────────────────────────────┘│ │
│   └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘

二、JavaScript 中的三种作用域

1. 全局作用域

在所有函数和代码块外面声明的变量,拥有全局作用域。

javascript 复制代码
let globalVar = "我是全局变量";

function test() {
    console.log(globalVar); // ✅ 函数内可以访问
}

test();
console.log(globalVar); // ✅ 函数外也可以访问

全局变量就像放在大街上的东西,谁都能看到。

2. 函数作用域

在函数内部声明的变量,只在该函数内部可见。

javascript 复制代码
function myFunction() {
    let localVar = "我是局部变量";
    console.log(localVar); // ✅ 函数内可以访问
}

myFunction();
console.log(localVar); // ❌ ReferenceError!

函数内的变量就像放在房间里的东西,出了这个房间就看不见了。

3. 块级作用域(ES6 新增)

letconst{} 代码块中声明的变量,只在该块内可见。

javascript 复制代码
if (true) {
    let blockVar = "我是块级变量";
    console.log(blockVar); // ✅ 块内可以访问
}

console.log(blockVar); // ❌ ReferenceError!

三、回到那个报错:逐行分析

javascript 复制代码
function outer() {
    // 👇 functionVar 在 outer 函数内部声明
    let functionVar = "我是函数作用域变量";

    function inner() {
        console.log(functionVar); // ✅ 第1处
    }

    inner();
    console.log(functionVar);     // ✅ 第2处
}

outer();
console.log(functionVar);         // ❌ 第3处
位置 代码在哪里? 能否访问 functionVar 原因
第1处 inner() 函数内部 ✅ 能 innerouter 的子函数,子函数可以访问父函数的变量
第2处 outer() 函数内部 ✅ 能 functionVar 就是在 outer 里面声明的,当然能访问
第3处 全局(函数外部) ❌ 不能 functionVar 的作用域仅限于 outer 内部,外面根本不知道它的存在

四、核心规则:作用域链

JavaScript 查找变量时,遵循一条由内向外的链条:

复制代码
当前作用域 → 父级作用域 → 爷爷级作用域 → ... → 全局作用域

只能由内向外找,不能由外向内找!

示例

javascript 复制代码
let a = "全局的 a";

function level1() {
    let b = "level1 的 b";

    function level2() {
        let c = "level2 的 c";

        function level3() {
            let d = "level3 的 d";

            console.log(d); // ✅ 自己的
            console.log(c); // ✅ 往外找一层,找到了
            console.log(b); // ✅ 往外找两层,找到了
            console.log(a); // ✅ 往外找三层,在全局找到了
        }
        level3();
    }
    level2();
}
level1();
复制代码
level3 找变量的过程:

level3 自己 → level2 → level1 → 全局
   d ✅         c ✅      b ✅     a ✅

反过来呢?

javascript 复制代码
let a = "全局的 a";

function level1() {
    let b = "level1 的 b";

    function level2() {
        let c = "level2 的 c";
    }

    level2();
    console.log(c); // ❌ ReferenceError! level1 看不到 level2 里面的 c
}

level1();
console.log(b); // ❌ ReferenceError! 全局看不到 level1 里面的 b

📌 记住:儿子可以用爸爸的东西,爸爸不能翻儿子的口袋。


五、varletconst 的作用域区别

这里有一个很容易踩的坑:

javascript 复制代码
function test() {
    if (true) {
        var varVariable = "var 声明";
        let letVariable = "let 声明";
        const constVariable = "const 声明";
    }

    console.log(varVariable);   // ✅ "var 声明"  ------ 居然能访问!
    console.log(letVariable);   // ❌ ReferenceError
    console.log(constVariable); // ❌ ReferenceError
}
关键字 作用域范围 说明
var 函数作用域 只认函数边界,不认 {} 块边界
let 块级作用域 {} 块边界
const 块级作用域 {} 块边界

用图来理解:

复制代码
function test() {
┌──────────────── 函数作用域 ──────────────────┐
│                                              │
│   if (true) {                                │
│   ┌──────────── 块级作用域 ────────────────┐ │
│   │  var varVariable = "var 声明";   ← 逃出去了│
│   │  let letVariable = "let 声明";   ← 被困住了│
│   │  const constVariable = "const";  ← 被困住了│
│   └────────────────────────────────────────┘ │
│                                              │
│   console.log(varVariable);   ✅ var 逃出来了 │
│   console.log(letVariable);   ❌ let 出不来   │
│   console.log(constVariable); ❌ const 出不来  │
└──────────────────────────────────────────────┘

这也是为什么现代 JavaScript 推荐使用 letconst ,而不是 varvar 的作用域行为容易导致意想不到的 bug。


六、一个经典的坑:for 循环中的 var

javascript 复制代码
// ❌ 用 var
for (var i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}
// 输出:3 3 3(不是 0 1 2!)

// ✅ 用 let
for (let i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}
// 输出:0 1 2 ✅

原因

  • var i 是函数作用域,整个循环共享同一个 i,循环结束后 i = 3
  • let i 是块级作用域,每次循环都会创建一个新的 i

七、实际应用:利用函数作用域保护变量

既然函数外部访问不到函数内部的变量,我们可以利用这一点来保护数据

模拟私有变量

javascript 复制代码
function createCounter() {
    let count = 0; // 外部无法直接访问和修改

    return {
        increment() {
            count++;
            console.log(`当前计数:${count}`);
        },
        decrement() {
            count--;
            console.log(`当前计数:${count}`);
        },
        getCount() {
            return count;
        }
    };
}

const counter = createCounter();
counter.increment(); // 当前计数:1
counter.increment(); // 当前计数:2
counter.decrement(); // 当前计数:1

console.log(counter.count);    // ❌ undefined,无法直接访问
console.log(counter.getCount()); // ✅ 1,只能通过方法访问

count 被安全地藏在 函数内部,外部只能通过我们提供的方法来操作它------这就是闭包(Closure) 的基本思想。

避免全局污染

javascript 复制代码
// ❌ 不好的写法:变量全暴露在全局
let userName = "张三";
let userAge = 25;
function greetUser() { /* ... */ }

// ✅ 好的写法:用函数包裹,避免污染全局
(function () {
    let userName = "张三";
    let userAge = 25;
    function greetUser() { /* ... */ }

    // 在这里执行你的代码...
    greetUser();
})();

// 外面访问不到 userName、userAge、greetUser

这种写法叫做 IIFE(立即调用函数表达式),在模块化出现之前被广泛使用。


八、总结

要点 说明
函数内部的变量,外部无法访问 这就是报错的根本原因
作用域链是单向的 只能由内向外查找,不能由外向内
var 是函数作用域 会"穿透" iffor 等代码块
let / const 是块级作用域 更安全、更可控,推荐使用
利用作用域可以保护数据 闭包、IIFE 等模式

最后,用一张图记住一切:

复制代码
🌍 全局作用域
│
│   能看到:全局变量
│   不能看到:任何函数内部的变量 ❌
│
└── 📦 函数 outer()
    │
    │   能看到:全局变量 + outer 自己的变量
    │   不能看到:inner 内部的变量 ❌
    │
    └── 📦 函数 inner()
        │
        │   能看到:全局变量 + outer 的变量 + inner 自己的变量 ✅
        │   (由内向外,一路畅通)

核心一句话:变量在哪个作用域声明,就只在那个作用域及其子作用域中可见。

理解了作用域,你就理解了 JavaScript 变量管理的核心,也为后面学习闭包模块化打下了坚实的基础。🚀

为什么 var + setTimeout 输出 3 3 3

先看代码

javascript 复制代码
for (var i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}
// 1秒后输出:3 3 3

要理解这个问题,你需要搞清楚两件事


第一件事:var 只有一个 i

var函数作用域 ,不是块级作用域。所以整个循环从头到尾,共享同一个变量 i

javascript 复制代码
// 你以为的样子(每轮循环有自己的 i):
// 第1轮:i = 0  ← 独立的
// 第2轮:i = 1  ← 独立的
// 第3轮:i = 2  ← 独立的

// 实际的样子(只有一个 i,不断被修改):
var i;  // 👈 只有一个 i,被提升到外面
for (i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i);  // 👈 三个箭头函数都指向同一个 i
    }, 1000);
}

可以这样想象:

复制代码
     ┌──────────────────────┐
     │   var i (只有一个!) │
     └──────────┬───────────┘
                │
        ┌───────┼───────┐
        │       │       │
        ▼       ▼       ▼
    箭头函数1  箭头函数2  箭头函数3
    console    console   console
    .log(i)   .log(i)   .log(i)

    三个函数都盯着同一个 i !

第二件事:setTimeout 是"稍后执行"

setTimeout 里的函数不会立刻执行,而是等到指定时间后才执行。

所以真正的执行顺序是这样的:

复制代码
时间线:
──────────────────────────────────────────────────►

【第1步】for 循环开始,飞速执行完毕(几乎瞬间)
         i=0 → i=1 → i=2 → i=3(循环结束)

【第2步】1秒后,三个 setTimeout 的回调函数开始执行
         此时去读 i,i 已经变成 3 了!

逐步模拟

复制代码
🔄 循环第1轮:i = 0
   → 注册一个 setTimeout(1秒后执行 console.log(i))
   → 注意:这里只是"注册",不是"执行"

🔄 循环第2轮:i = 1
   → 又注册一个 setTimeout(1秒后执行 console.log(i))

🔄 循环第3轮:i = 2
   → 再注册一个 setTimeout(1秒后执行 console.log(i))

🔄 i++ 变成 3,3 < 3 不成立,循环结束

   此时 i = 3 ✅
   此时有 3 个 setTimeout 在等待中 ⏳

⏰ 1秒后...

   箭头函数1 执行:console.log(i) → i 是 3 → 输出 3
   箭头函数2 执行:console.log(i) → i 是 3 → 输出 3
   箭头函数3 执行:console.log(i) → i 是 3 → 输出 3

一个生活中的比喻 🏠

想象这个场景:

墙上挂了一块白板 ,上面写着一个数字(就是 var i)。

你叫了三个人(三个 setTimeout),跟他们说:
"1秒后,去看白板上的数字,然后大声念出来。"

然后你飞快地擦掉白板上的数字:

  • 先写 0,擦掉
  • 再写 1,擦掉
  • 再写 2,擦掉
  • 最后写上 3

1秒后,三个人走到白板前,看到的都是 3


let 为什么就正常了?

javascript 复制代码
for (let i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}
// 输出:0 1 2 ✅

let块级作用域 ,每轮循环都会创建一个全新的 i

复制代码
第1轮循环 ┌─ 块作用域 ─┐
          │  let i = 0  │ ← 独立的 i
          │  setTimeout(回调 → 记住这个 i = 0)
          └─────────────┘

第2轮循环 ┌─ 块作用域 ─┐
          │  let i = 1  │ ← 独立的 i
          │  setTimeout(回调 → 记住这个 i = 1)
          └─────────────┘

第3轮循环 ┌─ 块作用域 ─┐
          │  let i = 2  │ ← 独立的 i
          │  setTimeout(回调 → 记住这个 i = 2)
          └─────────────┘

用白板比喻来说

let 相当于每轮循环都拍一张白板的照片 📸

每个人手里拿着的是各自的照片,不是去看白板。

所以 1 秒后:

  • 第 1 个人看照片:0
  • 第 2 个人看照片:1
  • 第 3 个人看照片:2

对比总结

复制代码
var:一个变量,三个函数共享
┌─────────┐
│  i = 3  │ ← 循环结束后
└────┬────┘
     │  ┌──── 读取 ────┐
     ├──→ 回调1: log(3) │
     ├──→ 回调2: log(3) │  → 输出 3 3 3
     └──→ 回调3: log(3) │
         └──────────────┘

let:三个变量,各自独立
┌─────────┐
│  i = 0  │──→ 回调1: log(0)
└─────────┘                     → 输出 0 1 2
┌─────────┐
│  i = 1  │──→ 回调2: log(1)
└─────────┘
┌─────────┐
│  i = 2  │──→ 回调3: log(2)
└─────────┘

一句话记住

var 只有一个变量,循环结束后才执行回调,读到的是最终值。
let 每轮创建新变量,每个回调记住的是当轮的值。

后记

2026年4月16日于上海, 在opus 4.6辅助下完成。

相关推荐
黑臂麒麟1 小时前
React Hooks 闭包陷阱:状态“丢失“的经典坑
javascript·react native·react.js·ecmascript
1314lay_10071 小时前
Vue+C#根据配置文件实现动态构建查询条件和动态表格
javascript·vue.js·elementui·c#
SuperEugene1 小时前
Vue3 前端配置驱动避坑:配置冗余、渲染性能、扩展性问题解决|配置驱动开发实战篇
前端·javascript·vue.js·驱动开发·前端框架
DS数模1 小时前
2026年Mathorcup数学建模竞赛A题思路解析+代码+论文
开发语言·数学建模·matlab·mathorcup·妈妈杯·2026妈妈杯
叶子野格2 小时前
《C语言学习:编程例题》8
c语言·开发语言·c++·学习·算法·visual studio
Java面试题总结2 小时前
Python 入门(四)- Openpyxl 操作 Excel 教程
开发语言·python·excel
gCode Teacher 格码致知2 小时前
Javascript提高:Math.round 详解-由Deepseek产生
开发语言·javascript
织_网2 小时前
Nest.js:Node.js后端开发的现代企业级解决方案,赋能AI全栈开发
javascript·人工智能·node.js
广州灵眸科技有限公司2 小时前
瑞芯微(EASY EAI)RV1126B QT GUI例程方案
linux·服务器·开发语言·网络·人工智能·qt·物联网