作用域是什么

8 阅读5分钟

作用域是程序中定义变量的区域,它规定了变量和函数的可访问范围,以及变量的生命周期。

1. 作用域的核心概念

1.1 作用域的基本作用

  • 确定变量和函数的可见性:在哪些地方可以访问这些标识符
  • 确定变量的生命周期:变量何时创建、何时销毁
  • 避免命名冲突:不同作用域可以有同名变量
  • 提供访问控制:实现私有和公共的访问级别

2. JavaScript中的作用域类型

2.1 全局作用域(Global Scope)

// 全局作用域中的变量
var globalVar = "I'm global";
let globalLet = "I'm also global";
const globalConst = "Me too";

function globalFunction() {
  console.log("I'm a global function");
}

// 这些都可以在代码的任何地方访问
console.log(window.globalVar); // 浏览器中,var变量会成为window对象的属性

2.2 函数作用域(Function Scope)

function outer() {
  // 函数作用域开始
  var functionScoped = "I'm only available inside outer";
  let alsoFunctionScoped = "Me too";
  
  function inner() {
    // 可以访问外层函数的变量
    console.log(functionScoped); // 正常访问
    var innerVar = "I'm only in inner";
  }
  
  inner();
  // console.log(innerVar); // 报错:innerVar is not defined
}
// console.log(functionScoped); // 报错:functionScoped is not defined

2.3 块级作用域(Block Scope)

// ES6引入的let和const具有块级作用域
{
  // 块级作用域开始
  let blockScoped = "I'm only in this block";
  const blockConst = "Me too";
  
  console.log(blockScoped); // 正常
}

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

// 实际应用场景
for (let i = 0; i < 3; i++) {
  // 每个i都是独立的块级作用域
  setTimeout(() => console.log(i)); // 0, 1, 2
}

// 对比var(没有块级作用域)
for (var j = 0; j < 3; j++) {
  setTimeout(() => console.log(j)); // 3, 3, 3
}

2.4 模块作用域(Module Scope)

// module.js
const privateVar = "I'm private to this module";
export const publicVar = "I'm exported";

// 在模块外无法访问privateVar
// 只能访问通过export暴露的变量

3. 作用域链(Scope Chain)

3.1 什么是作用域链

当访问一个变量时,JavaScript引擎会从当前作用域开始查找,如果找不到,就向上一层作用域查找,直到全局作用域,这个查找路径就是作用域链。

let global = "global";

function outer() {
  let outerVar = "outer";
  
  function inner() {
    let innerVar = "inner";
    
    console.log(innerVar);   // 1. 在当前作用域找到
    console.log(outerVar);   // 2. 在外层作用域找到
    console.log(global);     // 3. 在全局作用域找到
    console.log(undeclared); // 4. 报错:在所有作用域都找不到
  }
  
  inner();
}

outer();

3.2 作用域链的创建

// 作用域链在函数定义时确定(词法作用域)
let globalVar = "global";

function createFunctions() {
  let outerVar = "outer";
  
  function inner1() {
    console.log(outerVar); // 可以访问outerVar
  }
  
  let inner2 = function() {
    console.log(outerVar); // 也可以访问outerVar
  };
  
  // 即使函数在其他地方执行,仍然能访问定义时的作用域链
  return [inner1, inner2];
}

const [func1, func2] = createFunctions();
func1(); // "outer" - 仍然能访问outerVar(闭包)
func2(); // "outer"

4. 词法作用域 vs 动态作用域

4.1 JavaScript使用词法作用域(静态作用域)

let x = 10;

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

function bar() {
  let x = 20;
  foo(); // 输出10,不是20
}

bar();

// 解释:foo在定义时确定作用域链,访问的是定义时的x,而不是调用时的x

4.2 对比动态作用域(JavaScript不是动态作用域)

// 伪代码,展示动态作用域的概念
// 如果JavaScript是动态作用域:
x = 10

function foo() {
  print(x) // 在动态作用域中,这里会查找调用时的作用域
}

function bar() {
  x = 20
  foo() // 会输出20
}

5. 特殊的作用域行为

5.1 变量提升(Hoisting)

// var的变量提升
console.log(a); // undefined,不会报错
var a = 5;

// 实际执行顺序:
var a;          // 声明提升到作用域顶部
console.log(a); // undefined
a = 5;          // 赋值留在原地

// let/const的暂时性死区(Temporal Dead Zone)
console.log(b); // 报错:Cannot access 'b' before initialization
let b = 5;

5.2 没有块级作用域的var

if (true) {
  var noBlockScope = "I escape the block";
  let hasBlockScope = "I stay in the block";
}

console.log(noBlockScope);   // "I escape the block"
console.log(hasBlockScope);  // 报错:hasBlockScope is not defined

6. 作用域的实际应用

6.1 避免全局污染

// 不好的做法:污染全局作用域
var global1 = "data";
var global2 = "more data";

// 好的做法:使用IIFE或模块
(function() {
  // 私有作用域
  var privateData = "hidden";
  // 对外暴露必要的接口
  window.myModule = {
    publicMethod: function() {
      return privateData;
    }
  };
})();

6.2 创建私有变量

function createCounter() {
  let count = 0; // 私有变量
  
  return {
    increment() {
      count++;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.count); // undefined,无法直接访问
console.log(counter.getCount()); // 0

6.3 循环中的块级作用域

// 常见问题
var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() {
    console.log(i); // 全部输出3
  });
}

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

// 解决方案2:使用let(推荐)
for (let i = 0; i < 3; i++) {
  funcs.push(function() {
    console.log(i); // 0, 1, 2
  });
}

7. 现代JavaScript中的作用域

7.1 ES6模块作用域

// module.js
export const publicValue = 42;
const privateValue = "secret";

// main.js
import { publicValue } from './module.js';
console.log(publicValue); // 42
console.log(privateValue); // 报错:privateValue is not defined

7.2 类作用域

class MyClass {
  // 类作用域
  #privateField = "I'm private";
  publicField = "I'm public";
  
  constructor() {
    this.instanceField = "I'm on the instance";
  }
  
  method() {
    console.log(this.#privateField); // 可以访问私有字段
  }
}

const instance = new MyClass();
console.log(instance.publicField); // "I'm public"
console.log(instance.#privateField); // 报错:Private field must be declared in an enclosing class

总结表格

作用域类型关键字特点示例
全局作用域var, let, const整个程序可访问var global = 1;
函数作用域var只在函数内可访问function foo() { var x = 1; }
块级作用域let, const在{}内可访问{ let x = 1; }
模块作用域export, import只在模块内可访问export const x = 1;

关键要点

  1. 作用域决定了变量的可见性和生命周期
  2. JavaScript使用词法作用域:作用域在代码编写时确定
  3. 作用域链决定了变量的查找顺序:从内到外查找
  4. ES6引入的let/const有块级作用域,解决了许多var的问题
  5. 合理使用作用域可以避免命名冲突、实现封装

理解作用域是掌握JavaScript闭包、this绑定、模块化等高级概念的基础。