作用域和闭包(Scope and Closures)
JavaScript 的作用域机制和闭包概念:从基础使用到底层原理
📖 入门:基础使用
1. 作用域(Scope)
作用域定义:作用域是变量和函数的可访问范围。
1.1 全局作用域(Global Scope)
特点:
- 在函数外部声明的变量
- 在整个程序中都可以访问
示例:
let globalVar = "I'm global"; // 全局变量
function test() {
console.log(globalVar); // ✅ 可以访问
}
test();
console.log(globalVar); // ✅ 可以访问1.2 函数作用域(Function Scope)
特点:
- 在函数内部声明的变量
- 只能在函数内部访问
var声明的变量属于函数作用域
示例:
function test() {
var functionVar = "I'm in function"; // 函数作用域
console.log(functionVar); // ✅ 可以访问
}
// console.log(functionVar); // ❌ 报错:functionVar is not defined
test();1.3 块级作用域(Block Scope)
特点:
- 在
{}代码块内声明的变量 - 只能在代码块内访问
let和const声明的变量属于块级作用域
示例:
if (true) {
let blockVar = "I'm in block"; // 块级作用域
console.log(blockVar); // ✅ 可以访问
}
// console.log(blockVar); // ❌ 报错:blockVar is not defined2. 作用域链(Scope Chain)
概念:当前作用域找不到变量时,会向上级作用域查找,形成作用域链。
示例:
let global = "global";
function outer() {
let outerVar = "outer";
function inner() {
let innerVar = "inner";
console.log(innerVar); // "inner"(当前作用域)
console.log(outerVar); // "outer"(上级作用域)
console.log(global); // "global"(全局作用域)
}
inner();
}
outer();3. 闭包(Closure)
概念:闭包是函数能够访问其外部作用域变量的能力。
3.1 闭包的基本使用
示例:
function outer() {
let outerVar = "I'm outside";
function inner() {
console.log(outerVar); // 访问外部变量
}
return inner; // 返回内部函数
}
let closure = outer();
closure(); // "I'm outside"(仍然可以访问 outerVar)3.2 闭包的常见应用
1. 数据私有化:
function createCounter() {
let count = 0; // 私有变量
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
let counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 22. 函数工厂:
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
let double = createMultiplier(2);
let triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15🚀 提高:底层原理
1. 词法作用域(Lexical Scope)
1.1 词法作用域的定义
概念:词法作用域(静态作用域)是在代码编写时确定的作用域,而不是运行时。
原理:
- JavaScript 使用词法作用域
- 作用域由代码的书写位置决定
- 函数的作用域在定义时就确定了
示例:
let x = "global";
function outer() {
let x = "outer";
function inner() {
console.log(x); // "outer"(词法作用域,查找定义时的外层作用域)
}
return inner;
}
let x = "new global";
let func = outer();
func(); // "outer"(不是 "new global")1.2 作用域链的构建
原理:
- 每个函数都有内部属性
[[Scope]] [[Scope]]保存了函数定义时的作用域链- 函数执行时,会创建新的执行上下文
- 新的执行上下文的作用域链 = 当前变量对象 +
[[Scope]]
示例分析:
let global = "global";
function outer() {
let outerVar = "outer";
function inner() {
let innerVar = "inner";
console.log(innerVar, outerVar, global);
}
return inner;
}
let func = outer();
func();作用域链结构:
inner 执行上下文
├── 变量对象(VO)
│ └── innerVar: "inner"
└── 作用域链
├── outer 变量对象
│ └── outerVar: "outer"
└── 全局对象
└── global: "global"
2. 执行上下文(Execution Context)
2.1 执行上下文的创建
创建阶段:
- 创建变量对象(Variable Object)
- 建立作用域链(Scope Chain)
- 确定
this绑定(This Binding)
执行阶段:
- 变量赋值
- 函数引用
- 执行代码
示例:
function test(a, b) {
var c = 10;
function inner() {}
return a + b + c;
}
test(1, 2);执行上下文创建过程:
创建阶段:
1. 创建变量对象
- arguments: { a: 1, b: 2 }
- c: undefined
- inner: function reference
2. 建立作用域链
- [当前 VO, 全局对象]
3. 确定 this 绑定
执行阶段:
1. c = 10
2. 执行 return a + b + c
3. 闭包的原理
3.1 闭包的形成机制
原理:
- 当函数返回内部函数时,内部函数会保留对外部作用域的引用
- 即使外部函数执行完毕,其变量对象也不会被销毁
- 内部函数仍然可以访问外部函数的变量
示例:
function outer() {
let outerVar = "outer";
function inner() {
console.log(outerVar);
}
return inner; // 返回内部函数,形成闭包
}
let closure = outer();
closure(); // 仍然可以访问 outerVar内存模型:
outer 执行上下文(即使执行完毕,仍保留在内存中)
└── 变量对象
└── outerVar: "outer"
inner 函数对象
└── ``[[Scope]]``: [outer 的变量对象, 全局对象]
closure 变量
└── 引用 inner 函数对象
3.2 闭包的内存管理
内存泄漏风险:
function createHandler() {
let largeData = new Array(1000000).fill(0); // 大数组
return function() {
// 即使不使用 largeData,闭包仍然持有引用
console.log("Handler");
};
}
let handler = createHandler();
// largeData 不会被垃圾回收,因为闭包持有引用解决方案:
function createHandler() {
let largeData = new Array(1000000).fill(0);
return function() {
console.log("Handler");
};
// 如果不再需要 largeData,可以显式设置为 null
// largeData = null; // 但这样会失去闭包的作用
}
let handler = createHandler();
// 如果不再需要 handler,应该设置为 null
handler = null; // 释放闭包4. 变量提升和闭包
4.1 变量提升对闭包的影响
示例:
var functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i); // 所有函数都引用同一个 i
});
}
functions[0](); // 3
functions[1](); // 3
functions[2](); // 3问题:所有函数都引用同一个 i,因为 var 是函数作用域。
解决方案 1:使用 let:
let functions = [];
for (let i = 0; i < 3; i++) { // let 创建块级作用域
functions.push(function() {
console.log(i); // 每个函数引用不同的 i
});
}
functions[0](); // 0
functions[1](); // 1
functions[2](); // 2解决方案 2:使用 IIFE:
var functions = [];
for (var i = 0; i < 3; i++) {
(function(j) { // 立即执行函数,创建新的作用域
functions.push(function() {
console.log(j);
});
})(i);
}
functions[0](); // 0
functions[1](); // 1
functions[2](); // 25. 闭包的性能考虑
5.1 内存占用
闭包会占用更多内存:
- 闭包会保留外部作用域的变量对象
- 即使外部函数执行完毕,变量对象也不会被销毁
- 需要谨慎使用,避免内存泄漏
5.2 性能优化
避免不必要的闭包:
// 不好的做法
function processData(data) {
return data.map(function(item) {
return item * 2; // 不需要闭包
});
}
// 更好的做法
function processData(data) {
return data.map(item => item * 2); // 使用箭头函数
}📝 最佳实践
- 理解作用域:清楚变量的作用域范围
- 合理使用闭包:利用闭包实现数据私有化
- 避免内存泄漏:注意闭包的内存占用
- 使用
let和const:避免var带来的作用域问题
🔗 相关链接
- 变量声明 — 变量声明和作用域的关系
- 函数 — 函数的执行上下文和闭包
- 执行上下文 — 深入理解执行上下文(如果存在)
- ECMAScript 作用域规范