作用域与闭包
作用域决定"变量在哪些地方可见",闭包是"函数记住并访问其定义时作用域的能力"。这两个机制是 JavaScript 最常踩坑也最常被面试问到的点:var 的函数作用域造成循环闭包问题,this 的指向规则在箭头函数中反转,而闭包则是数据私有化、函数工厂、柯里化等模式的地基。理解它们,才能读懂大部分"诡异"的 JavaScript 行为。
提示
记忆锚点:var 属于函数,let/const 属于代码块;普通函数的 this 看调用者,箭头函数的 this 看定义处;闭包 = 函数 + 它出生时的环境。
var 与 let/const
三个声明关键字的核心差异在作用域和提升机制:
| 特性 | var | let / const |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域({} 内) |
| 变量提升 | 声明提升,值为 undefined | 提升但存在暂时性死区(TDZ) |
| 重复声明 | 允许 | 不允许 |
| 推荐度 | 永不使用 | 默认 const,需重赋值用 let |
暂时性死区(TDZ)
let/const 声明的变量在声明语句之前访问会抛 ReferenceError——这个"声明前不可用"的区域就是暂时性死区。它把"用了未定义变量"从静默的 undefined 变成显式报错,是语言变严格的体现。var 没有 TDZ,所以 console.log(x) 在 var x = 1 之前打印 undefined 而不报错,容易掩盖问题。
循环闭包问题
var 的函数作用域导致经典的循环陷阱——回调里的 i 全是同一个变量:
// var 问题:setTimeout 执行时循环已结束,i 是同一个变量(3)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3
// let 解决:每次循环创建独立的块级作用域,i 被"捕获"成三个不同的值
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2var 只创建一个 i,let 为每轮循环创建独立的 i——这就是闭包与作用域交互的典型场景:箭头函数(闭包)捕获的其实是"定义时所在作用域里的变量",var 时所有闭包共享同一个,let 时各闭包各有一个。
this 指向
this 的值在调用时确定(除非是箭头函数):
| 调用方式 | this 指向 | 例子 |
|---|---|---|
方法调用 obj.fn() | 调用者 obj | obj.greet() 中 this = obj |
普通函数调用 fn() | 全局对象(严格模式 undefined) | 直接调用 |
fn.call(obj) / fn.apply(obj) | 显式指定的 obj | 借用方法 |
| 箭头函数 | 定义时外层作用域的 this(不绑定) | 回调里最安全 |
// 普通函数:this 由调用方式决定
function greet() {
console.log(this.name);
}
const obj = { name: 'Alice', greet };
obj.greet(); // 'Alice'(obj 调用)
greet(); // undefined(无调用者)
// 箭头函数:this 固定为定义处的外层
const arrow = () => console.log(this.name);
obj.arrow = arrow;
obj.arrow(); // 仍打印定义处外层的 this,不是 obj为什么回调里要用箭头函数
setTimeout(function(){ console.log(this) }) 里 this 指向全局对象(回调被当作普通函数调用);改成箭头函数后 this 是定义处的值(通常是外层对象)——这是回调场景箭头函数替代普通函数的根本原因。
闭包
闭包 = 函数 + 函数定义时所在的作用域。即使外部函数已经执行完毕,闭包依然能访问它定义时捕获的变量:
function counter() {
let count = 0; // count 被闭包"记住"
return () => ++count; // 返回的函数是闭包
}
const add = counter();
add(); // 1
add(); // 2count 既不全局可见、也不被销毁——闭包同时实现了数据私有化和状态保持。三个经典应用:
| 应用 | 机制 | 例子 |
|---|---|---|
| 数据私有化 | 变量只在闭包内可见 | 计数器、模块内部状态 |
| 函数工厂 | 外层参数被内层记住 | makeAdder(x) 返回 n => n + x |
| 柯里化 | 分步接收参数 | add(1)(2) 逐步求值 |
// 函数工厂:x 被每个生成的函数各自记住
function makeAdder(x) {
return (n) => n + x;
}
const add5 = makeAdder(5);
add5(3); // 8闭包的内存代价
闭包让被捕获的变量无法被垃圾回收,直到闭包本身不再被引用。循环中大量创建闭包(如给 10 万个元素各绑一个回调)会显著增加内存占用——必要时在回调结束后解除引用。
IIFE 立即执行函数
IIFE(Immediately Invoked Function Expression)是"定义后立即执行"的函数,用于创建独立作用域:
// 避免全局污染:内部变量不出现在全局
(function() {
var local = '私有变量';
})();
// 外部访问不到 local// ES6+ 直接用代码块替代 IIFE
{
let local = '私有变量';
}IIFE 在 ES6 之前的模块化中扮演核心角色(jQuery 插件的标准包裹方式);有了 let/const 块级作用域后,大多数 IIFE 场景可直接用代码块替代,但"立即执行 + 返回结果"的写法(如立即初始化配置对象)仍有用武之地。
异步编程 的循环闭包问题、Node.js开发 的事件回调,处处是作用域与闭包的身影——这是 JavaScript 一切行为的底层规则。