故事开始(不说废话、直接上干货)
作用域就像是变量和函数的「居住范围」,它决定了:
- 变量和函数可以被访问的区域
- 变量的生命周期(何时创建,何时销毁)
- 同名变量之间的优先级关系
作用域的分类
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. 块级作用域
- 由
{}包裹的代码块(如if、for、while、switch等) - 使用
let或const声明的变量,只在当前代码块内有效 - 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 中变量查找的「路径」,当代码需要访问一个变量时:
- 首先在当前作用域中查找
- 如果找不到,就沿着作用域链向上查找
- 直到找到该变量,或者到达全局作用域仍未找到(此时会报错或创建全局变量,取决于声明方式)
作用域链的形成
- 每个函数在创建时,会记住它的「父级作用域」
- 当函数被调用时,会创建一个新的「执行上下文」
- 每个执行上下文都包含一个「作用域链」,由当前作用域和所有父级作用域组成
作用域链的查找规则
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(); │
└─────────────────────────────────────────────────────────────┘
闭包与作用域链
闭包的定义
闭包是指:
- 一个函数可以访问其词法作用域之外的变量
- 即使外部函数已经执行完毕,其作用域仍被内部函数「记住」并访问
闭包的形成条件
- 存在嵌套函数(内部函数定义在外部函数内部)
- 内部函数引用了外部函数的变量
- 内部函数被外部函数返回,并在外部被调用
其实一旦函数被调用,其实就创建了一个闭包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 声明的变量会「提升」到作用域顶部,可能导致意外行为
解决 :优先使用 let 和 const,它们支持块级作用域,不会发生变量提升
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. 闭包演示

功能:
- 展示闭包如何保留外部函数的作用域
- 演示多个闭包实例的独立性
- 动态显示闭包的创建和调用过程
- 包含详细的闭包信息说明
总结
- 作用域是变量和函数的居住范围,决定了它们的可访问性
- 作用域分类:全局作用域、函数作用域、块级作用域
- 词法作用域:JavaScript 使用的作用域类型,由代码书写位置决定
- 作用域链:变量查找的路径,从当前作用域向上查找
- 闭包:内部函数可以访问外部函数的变量,即使外部函数已执行完毕
- 最佳实践 :优先使用
let和const,避免全局变量污染,注意循环中的闭包问题
通过理解作用域和作用域链,你可以写出更安全、更可维护的 JavaScript 代码。建议结合交互式演示页面,亲自操作体验变量查找和闭包的工作原理。