Javascript 零基础教程

JavaScript 作用域

作用域决定了变量在你代码不同部分的可访问性

掌握作用域是防止命名冲突、有效管理数据以及构建模块化应用程序的基础。

本章将深入探讨全局作用域和局部作用域的概念,为你理解变量在 JavaScript 函数中的行为打下坚实基础。

1. 理解作用域 (Scope)

在 JavaScript 的语境中,作用域定义了变量的可访问性或可见性。本质上,它决定了你在代码的何处可以访问和修改某个特定的变量。

理解作用域至关重要,因为它可以帮助你防止意外的副作用(Side Effects),并确保你的代码行为是可预测的。JavaScript 中主要有两种类型的作用域:全局作用域局部作用域

2. 全局作用域 (Global Scope)

在任何函数或代码块之外声明的变量拥有全局作用域。这意味着它们可以从你 JavaScript 代码的任何地方被访问和修改,包括在函数内部。

示例:

// 声明一个全局变量
var globalVariable = "我是全局变量";

function myFunction() {
  // 在函数内部访问全局变量
  console.log(globalVariable); // 输出: 我是全局变量
}

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

在这个例子中,globalVariable 既可以在 myFunction 内部访问,也可以在外部访问。

重要考量:

  1. 虽然全局变量看起来很方便,但过度使用会导致命名冲突,并使代码难以维护。如果代码的两个不同部分使用了相同的全局变量名,它们可能会无意中覆盖彼此的值,导致意外行为。
  2. 在 Web 浏览器中,全局作用域就是 window 对象。当你使用 var 声明一个全局变量时,它会成为 window 对象的一个属性(例如 window.globalVariable)。
  3. 在函数或代码块之外使用 letconst 也会创建全局变量,但它们不会成为 window 对象的属性。这是一个微妙但重要的区别。

演示全局变量潜在问题的示例:

var counter = 0; // 全局计数器

function incrementCounter() {
  counter++;
  console.log("incrementCounter 内部的计数器:", counter);
}

function resetCounter() {
  counter = 0;
  console.log("resetCounter 内部的计数器:", counter);
}

incrementCounter(); // incrementCounter 内部的计数器: 1
incrementCounter(); // incrementCounter 内部的计数器: 2
resetCounter();     // resetCounter 内部的计数器: 0
incrementCounter(); // incrementCounter 内部的计数器: 1

在这个例子中,incrementCounterresetCounter 修改的是同一个全局变量 counter。如果这些函数被用于大型应用程序的不同部分,这可能会由于一个函数的变化意外影响另一个函数而导致问题。

3. 局部作用域 (Local Scope / Function Scope)

在函数内部声明的变量拥有局部作用域。这意味着它们只能在该函数内部被访问。它们在函数外部是不可见或不可访问的。

示例:

function myFunction() {
  // 声明一个局部变量
  var localVariable = "我是局部变量";
  console.log(localVariable); // 输出: 我是局部变量
}

myFunction();

// 尝试在函数外部访问局部变量会导致错误
// console.log(localVariable); // 错误: localVariable 未定义 (is not defined)

在这个例子中,localVariable 仅在 myFunction 内部可访问。试图在函数外部访问它会导致错误,因为它超出了作用域。

作用域与 var, let, 和 const:

  • var: 当在函数内部声明时,var 创建一个函数作用域变量。这意味着该变量在该函数内的任何地方都是可访问的,无论它是在函数内的何处声明的。
  • let const: 当在函数内部声明时,letconst 也会创建函数作用域变量。但是,它们同时也是块级作用域 (Block-Scoped) 的。这意味着它们的作用域被限制在定义它们的代码块中(例如:在 if 语句或 for 循环内部)。

3.1 演示 var, let, 和 const 区别的示例:

function testScope() {
  if (true) {
    var varVariable = "var 变量";
    let letVariable = "let 变量";
    const constVariable = "const 变量";
  }

  console.log(varVariable);   // 输出: var 变量 (函数作用域,虽然在 if 块内声明,但漏出来了)
  // console.log(letVariable);   // 错误: letVariable 未定义 (块级作用域,被限制在 if 块内)
  // console.log(constVariable); // 错误: constVariable 未定义 (块级作用域,被限制在 if 块内)
}

testScope();

在这个例子中,varVariable 可以在 if 块外部访问,因为 var 是函数作用域的。然而,letVariableconstVariableif 块外部无法访问,因为 letconst 是块级作用域的。

4. 块级作用域 (Block Scope)

如上所述,letconst 引入了块级作用域的概念。块 (Block) 是指包含在花括号 {} 中的一段代码。这包括 if 语句、for 循环、while 循环,甚至是独立的代码块。

在块内部用 letconst 声明的变量,仅在该块内部可访问。

示例:

{
  let blockVariable = "我是块级变量";
  console.log(blockVariable); // 输出: 我是块级变量
}

// console.log(blockVariable); // 错误: blockVariable 未定义

在这个例子中,blockVariable 仅在定义它的花括号内可访问。

为什么块级作用域很重要?

块级作用域有助于避免命名冲突,并使你的代码更具可预测性。它允许你在不同的块中声明相同名称的变量,而不会相互干扰。这在复杂的函数或循环中特别有用,尤其是当你需要使用临时变量时。

5. 作用域链 (Scope Chain) 与词法作用域 (Lexical Scoping)

当你尝试在 JavaScript 中访问一个变量时,JavaScript 引擎首先会在当前作用域中查找该变量。如果在那儿没找到,它会去外层(包裹它的)作用域中查找,依此类推,直到到达全局作用域。

这种作用域的链条被称为作用域链 (Scope Chain)

作用域链是由你代码的词法结构 (Lexical Structure) 决定的,意思是它基于函数和代码块在你代码中实际物理位置来决定。这也称为词法作用域 (Lexical Scoping)

示例:

var globalVariable = "全局";

function outerFunction() {
  var outerVariable = "外层";

  function innerFunction() {
    var innerVariable = "内层";
    console.log(globalVariable);  // 从全局作用域访问 globalVariable
    console.log(outerVariable);   // 从 outerFunction 的作用域访问 outerVariable
    console.log(innerVariable);   // 从 innerFunction 的作用域访问 innerVariable
  }

  innerFunction();
  // console.log(innerVariable); // 错误: innerVariable 在这里未定义
}

outerFunction();

在这个例子中:

  1. innerFunction 可以访问 globalVariableouterVariableinnerVariable,因为它有权访问它自己的作用域、outerFunction 的作用域以及全局作用域。
  2. outerFunction 可以访问 globalVariableouterVariable,但不能访问 innerVariable,因为 innerVariable 仅在 innerFunction 内部定义。
  3. 全局作用域只能访问 globalVariable

6. 遮蔽 (Shadowing)

如果一个变量在内部作用域和外部作用域中都声明了相同的名称,内部作用域的变量会遮蔽 (Shadow) 外部作用域的变量。这意味着在内部作用域中,内部变量优先,你无法直接访问同名的外部变量。

示例:

var myVariable = "全局变量";

function myFunction() {
  var myVariable = "局部变量"; // 遮蔽了全局的 myVariable
  console.log(myVariable); // 输出: 局部变量
}

myFunction();
console.log(myVariable); // 输出: 全局变量

在这个例子中,在 myFunction 内部,局部的 myVariable 遮蔽了全局的 myVariable。因此,函数内部的 console.log(myVariable) 输出 "局部变量"。然而,在函数外部,全局的 myVariable 仍然可以访问,所以 console.log(myVariable) 输出 "全局变量"。

7. 实践案例与演示

让我们探索一些实际案例来巩固你对作用域的理解。

7.1 案例 1:在循环中使用 letconst

for (let i = 0; i < 5; i++) {
  console.log("循环内部:", i);
}

// console.log("循环外部:", i); // 错误: i 未定义

在这个例子中,i 是使用 letfor 循环内部声明的。因此,它仅在循环的代码块中可访问。尝试在循环外部访问它会导致错误。

7.2 案例 2:利用块级作用域避免命名冲突

function processData(data) {
  if (data.length > 0) {
    let result = "正在处理数据...";
    console.log(result); // 输出: 正在处理数据...
  } else {
    let result = "没有数据可处理。";
    console.log(result); // 输出: 没有数据可处理。
  }
  // console.log(result); // 错误: result 未定义
}

processData([1, 2, 3]);
processData([]);

在这个例子中,result 使用 let 分别在 ifelse 块中声明。因为 let 是块级作用域的,这两个 result 变量是相互独立的,不会发生冲突。这允许你在不同的块中使用相同的变量名,而不会产生意外的副作用。

7.3 案例 3:闭包 (Closures) 与作用域

闭包是一个更高级的话题,但它们与作用域密切相关。闭包是指一个函数可以访问其周围作用域的变量,即使外层函数已经执行完毕。我们将在后面的课程中详细介绍闭包,但了解作用域在其中扮演的角色很重要。

function outerFunction(outerVariable) {
  function innerFunction() {
    console.log(outerVariable); // 访问 outerFunction 作用域中的 outerVariable
  }
  return innerFunction;
}

var myInnerFunction = outerFunction("来自外层的问候!");
myInnerFunction(); // 输出: 来自外层的问候!

在这个例子中,innerFunction 是一个闭包。即使 outerFunction 已经返回了,innerFunction 仍然可以访问来自 outerFunctionouterVariable。这是因为作用域链被保留了下来,允许 innerFunction “记住”它被创建时的环境。