JavaScript: JavaScript 作用域

作用域决定了变量"在哪儿能被访问到"。你可以把作用域想象成身份证——出了省,你的市身份证就不好使了;出了国,省身份证也不认了。变量也是一样,出了自己的作用域就找不到了。

1. 作用域的类型与规则

▶ 示例

JAVASCRIPT
// 本节代码示例
const message = "你好,世界!";
console.log(message);
▶ 试一试

这是最简单的示例,后续章节会详细解释。### (1) 全局作用域

在最外层用 varletconst 声明的变量拥有全局作用域,任何地方都能访问。

HTML
<script>
var globalVar = "我是全局变量";
function test() {
  console.log(globalVar); // 能访问
}
test();
</script>

全局变量虽然方便,但容易"污染命名空间"——项目一大,全局变量名冲突是家常便饭。尽量少用全局变量。

(2) 函数作用域

var 声明的变量遵循函数作用域:只在当前函数内有效,出了函数就消失。

HTML
<script>
function greet() {
  var message = "你好";
  console.log(message); // 正常
}
greet();
try {
  console.log(message); // ReferenceError!
} catch(e) {
  console.log("访问失败:" + e.message);
}
</script>

var 的函数作用域有个经典坑:在 for 循环里用 var,循环结束后变量还"漏"在外面。

(3) 块级作用域

letconst 声明的变量遵循块级作用域:只在当前 {} 内有效。

HTML
<script>
if (true) {
  let x = 10;
  const y = 20;
  var z = 30;
  console.log("块内:x=" + x + ", y=" + y + ", z=" + z);
}
console.log("块外:z=" + z); // var 不受块级限制
try {
  console.log(x); // x 在这里不可访问
} catch(e) {
  console.log("x 不可访问:" + e.message);
}
</script>

这就是为什么循环计数器要用 let 而不是 var

(4) 作用域链

内层作用域可以访问外层作用域的变量,就像你在学校里可以用家里的东西(如果带去了的话)。查找变量时,从当前作用域向外一层层找,直到全局——这条链就是作用域链

HTML
<script>
const city = "北京";
function outer() {
  const district = "海淀";
  function inner() {
    const street = "中关村";
    console.log(street, district, city); // 都能访问
  }
  inner();
}
outer();
</script>

外层不能访问内层的变量——就像你不能进学校拿学生口袋里的东西。

(5) 变量提升(Hoisting)

var 声明会被"提升"到作用域顶部,但赋值不会:

HTML
<script>
console.log(a); // undefined(不是报错!)
var a = 5;
// 引擎实际执行顺序:var a; → console.log(a); → a = 5;
</script>

letconst 也有提升,但在声明前访问会报错——这段区域叫暂时性死区(TDZ)

HTML
<script>
try {
  console.log(b); // ReferenceError!
} catch(e) {
  console.log("暂时性死区:" + e.message);
}
let b = 5;
</script>

结论:永远用 letconst,避免 var 的提升陷阱。

(6) 闭包的直观理解

闭包 = 函数 + 它诞生时的变量环境。就像一个人离开了家乡,但心里记得家乡的样子——即使家乡后来变了,他记忆中的家乡不变。

HTML
<script>
function createCounter() {
  let count = 0;        // 这个变量被"关"在闭包里
  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count 变量没有被销毁,因为返回的函数还"记着"它
</script>

createCounter 已经执行完了,按理说 count 该销毁了——但返回的函数紧紧抓着 count 不放,所以它活了下来。这就是闭包。

(7) 闭包的实用场景


▶ 示例:var vs let 在循环中的区别

HTML 📖 仅展示
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>var vs let</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    .block { margin: 16px 0; padding: 12px; background: #f5f5f5; border-radius: 6px; }
    .block h3 { margin-top: 0; }
    .var-result, .let-result { margin: 8px 0; font-weight: bold; }
    .var-result { color: #d9534f; }
    .let-result { color: #5cb85c; }
  </style>
</head>
<body>
  <h2>var vs let:循环中的作用域差异</h2>
  <div id="output"></div>
  <script>
    const varResults = [];
    const letResults = [];

    for (var i = 0; i < 3; i++) {
      varResults.push(`循环内 i = ${i}`);
    }
    varResults.push(`循环外 i = ${i}(var 漏出来了!)`);

    for (let j = 0; j < 3; j++) {
      letResults.push(`循环内 j = ${j}`);
    }
    letResults.push(`循环外访问 j → ReferenceError(let 不会泄漏)`);

    document.getElementById("output").innerHTML = `
      <div class="block">
        <h3>var 声明</h3>
        ${varResults.map(r => `<div class="var-result">${r}</div>`).join("")}
      </div>
      <div class="block">
        <h3>let 声明</h3>
        ${letResults.map(r => `<div class="let-result">${r}</div>`).join("")}
      </div>
    `;
  </script>
</body>
</html>
逻辑代码 41 行(超过 40 行限制,仅展示)

▶ 示例:作用域链演示

HTML 📖 仅展示
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>作用域链</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    .scope { margin: 12px 0; padding: 12px; border-left: 4px solid #4a90d9; background: #f0f7ff; border-radius: 0 6px 6px 0; }
    .scope-inner { margin: 12px 0 12px 20px; padding: 12px; border-left: 4px solid #5cb85c; background: #f0fff0; border-radius: 0 6px 6px 0; }
    .scope-innermost { margin: 12px 0 12px 20px; padding: 12px; border-left: 4px solid #f0ad4e; background: #fffdf0; border-radius: 0 6px 6px 0; }
    code { background: rgba(0,0,0,0.06); padding: 2px 6px; border-radius: 3px; }
  </style>
</head>
<body>
  <h2>作用域链:由内向外查找变量</h2>
  <div id="output"></div>
  <script>
    const country = "中国";

    function outer() {
      const province = "广东";

      function middle() {
        const city = "深圳";

        function inner() {
          const district = "南山";
          const result = `${district}区,${city}市,${province}省,${country}`;
          return result;
        }

        return inner();
      }

      return middle();
    }

    const final = outer();

    document.getElementById("output").innerHTML = `
      <div class="scope">
        <strong>全局作用域</strong>:country = <code>"${country}"</code>
        <div class="scope-inner">
          <strong>outer 函数作用域</strong>:province = <code>"广东"</code>
          <div class="scope-innermost">
            <strong>middle 函数作用域</strong>:city = <code>"深圳"</code>
            <div style="margin-top:12px; padding:12px; border-left:4px solid #d9534f; background:#fff0f0; border-radius:0 6px 6px 0;">
              <strong>inner 函数作用域</strong>:district = <code>"南山"</code>
              <p>最内层能访问所有外层变量 → <strong>${final}</strong></p>
            </div>
          </div>
        </div>
      </div>
    `;
  </script>
</body>
</html>
逻辑代码 50 行(超过 40 行限制,仅展示)

▶ 示例:闭包计数器与私有变量

HTML 📖 仅展示
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>闭包</title>
  <style>
    body { font-family: sans-serif; padding: 20px; text-align: center; }
    .counter { display: inline-block; padding: 20px; background: #f0f7ff; border-radius: 12px; margin: 12px; }
    .count { font-size: 48px; font-weight: bold; color: #4a90d9; }
    button { padding: 10px 24px; font-size: 16px; border: none; border-radius: 6px; cursor: pointer; background: #4a90d9; color: #fff; margin: 4px; }
    button:hover { background: #357abd; }
    .reset { background: #d9534f; }
    .reset:hover { background: #c9302c; }
    .info { color: #888; font-size: 14px; margin-top: 8px; }
  </style>
</head>
<body>
  <h2>闭包计数器</h2>
  <div class="counter">
    <div class="count" id="display">0</div>
    <button id="increment">+1</button>
    <button id="decrement">-1</button>
    <button id="reset" class="reset">重置</button>
    <div class="info">count 变量被闭包保护,外部无法直接修改</div>
  </div>
  <script>
    function createCounter() {
      let count = 0;
      return {
        increment() { return ++count; },
        decrement() { return --count; },
        reset() { count = 0; return count; },
        getCount() { return count; }
      };
    }

    const counter = createCounter();
    const display = document.getElementById("display");

    function updateDisplay() {
      display.textContent = counter.getCount();
    }

    document.getElementById("increment").addEventListener("click", function() {
      counter.increment();
      updateDisplay();
    });

    document.getElementById("decrement").addEventListener("click", function() {
      counter.decrement();
      updateDisplay();
    });

    document.getElementById("reset").addEventListener("click", function() {
      counter.reset();
      updateDisplay();
    });
  </script>
</body>
</html>
逻辑代码 55 行(超过 40 行限制,仅展示)

▶ 示例:变量提升对比

HTML 📖 仅展示
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>变量提升</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    .compare { display: flex; gap: 20px; margin: 16px 0; }
    .col { flex: 1; padding: 16px; border-radius: 8px; }
    .col-var { background: #fff0f0; border: 2px solid #d9534f; }
    .col-let { background: #f0fff0; border: 2px solid #5cb85c; }
    h3 { margin-top: 0; }
    code { background: rgba(0,0,0,0.06); padding: 2px 6px; border-radius: 3px; }
    pre { background: #fff; padding: 12px; border-radius: 6px; overflow-x: auto; }
  </style>
</head>
<body>
  <h2>变量提升:var vs let</h2>
  <div id="output"></div>
  <script>
    var varBefore = "var 声明前访问 → " + typeof aVar;
    var aVar = 5;
    var varAfter = "var 声明后访问 → " + aVar;

    let letAfter = 5;
    let letBefore = "let 声明前访问 → 直接报 ReferenceError!";

    document.getElementById("output").innerHTML = `
      <div class="compare">
        <div class="col col-var">
          <h3>var(提升但不赋值)</h3>
          <pre><code>console.log(typeof aVar);  // ${varBefore}
var aVar = 5;
console.log(aVar);         // ${varAfter}</code></pre>
          <p>⚠️ 不报错,但得到 <code>undefined</code>——更隐蔽的 bug!</p>
        </div>
        <div class="col col-let">
          <h3>let(暂时性死区)</h3>
          <pre><code>console.log(bVar);  // ${letBefore}
let bVar = 5;</code></pre>
          <p>✅ 直接报错,立刻发现问题!</p>
        </div>
      </div>
    `;
  </script>
</body>
</html>
逻辑代码 45 行(超过 40 行限制,仅展示)

❓ 常见问题

Q 为什么 for 循环用 var 会导致所有回调都输出同一个值?
A 因为 var 只有函数作用域,循环结束后只有一个 i,所有回调共享同一个 i(此时已经等于循环终值)。用 let 则每次迭代都有独立的 i 副本。
Q 闭包会造成内存泄漏吗?
A 闭包本身不是内存泄漏——它是有意保持对变量的引用。真正的泄漏是:你不再需要某个闭包了,但它还被别的引用链挂着,导致变量无法被回收。解决办法:将引用置为 null
Q letconst 该选哪个?
A 默认用 const,只有需要重新赋值时才用 let。这个习惯能防止你意外修改不该改的变量。

📖 小节

📝 作业

  1. 写一个函数 makeGreeter(greeting),返回一个函数,调用时返回 greeting + "," + name。用两个不同的 greeting 创建两个问候函数,验证它们互不干扰。
  2. 写一个 createWallet(initialBalance) 函数,返回 depositwithdrawgetBalance 三个方法,balance 从外部无法直接访问(私有变量),只能通过方法操作。
  3. 解释下面代码的输出结果,并说明为什么:
HTML
<script>
for (var i = 0; i < 3; i++) {
  setTimeout(function() { console.log(i); }, 100);
}
</script>

把它改成输出 0、1、2。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏