作用域是什么
作用域(Scope)决定了 JavaScript 引擎如何查找变量。
JavaScript 有三种作用域:
全局作用域:最外层的作用域,在任何地方都能访问。
函数作用域:由函数创建的作用域,函数内部定义的变量只能在这个函数内部访问。
块级作用域:由 let 和 const 在 {} 块内创建的作用域。
var global = 'I am global';
function foo() {
var functionScoped = 'I am in foo';
console.log(global); // 能访问
if (true) {
let blockScoped = 'I am in block';
const alsoBlockScoped = 'Me too';
console.log(blockScoped); // 能访问
}
// console.log(blockScoped); // ReferenceError!块级作用域在块外访问不到
}
console.log(functionScoped); // ReferenceError!函数作用域在函数外访问不到
作用域链是什么
当 JavaScript 引擎在当前作用域找不到变量时,它会沿着作用域链(Scope Chain)向外层查找,直到找到为止,或者到达全局作用域,如果还找不到就返回 undefined。
const name = 'Global';
function outer() {
const name = 'Outer';
function inner() {
const name = 'Inner';
console.log(name); // 输出 'Inner'
}
function middle() {
// 这里没有定义 name
console.log(name); // 找到 outer 的 name,输出 'Outer'
}
inner();
middle();
}
outer();
inner 函数的执行上下文形成的作用域链是:inner → outer → global。当 inner 查找 name 时,先在 inner 自己的作用域找到,就不再往外找了。
词法作用域
JavaScript 的作用域是词法作用域(Lexical Scope),意思是作用域在代码定义时就确定了,而不是在调用时确定。
function outer() {
const x = 10;
function middle() {
console.log(x); // 输出 10,不是 20
}
function inner() {
const x = 20;
middle(); // 调用时 x 是 20,但 middle 定义时的作用域决定了它能访问 outer 的 x
}
inner();
}
outer();
middle 函数在定义时就已经确定了它的词法环境包含 outer 的 x,所以无论 middle 被谁调用,它能访问的 x 都是 outer 里定义的那个 10。
这就是闭包的基础。
闭包是什么
闭包(Closure)是 JavaScript 最强大也最容易被误解的特性之一。
简单来说,闭包是指一个函数能记住它定义时的词法环境,即使这个函数在定义时的词法环境之外被执行。
function createCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
createCounter 返回的函数形成了一个闭包。它记住了 createCounter 执行时的词法环境,包括 count 变量。即使 createCounter 的执行上下文已经从调用栈弹出,counter 函数仍然能访问和修改 count。
这就是闭包的价值:让函数能携带"记忆"。
闭包的经典应用
模块模式
function createModule(prefix) {
const name = prefix;
return {
getName() {
return name;
},
setName(newName) {
name = newName;
}
};
}
const module = createModule('MyModule');
console.log(module.getName()); // 'MyModule'
module.setName('NewModule');
console.log(module.getName()); // 'NewModule'
name 变量被隐藏在闭包里,外部无法直接访问,只能通过返回的对象提供的方法操作。这是一种实现模块私有变量的方式。
防抖函数
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
防抖函数的实现利用了闭包:timer 变量被闭包持有,每次调用返回的函数都能访问和修改同一个 timer,从而实现"最后一次调用后才执行"的功能。
for 循环中的闭包问题
这是最常见的闭包坑:
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出 3、3、3,而不是 0、1、2
}, 100);
}
因为 var 是函数作用域,不是块级作用域,所以循环结束后 i 的值是 3。所有三个 setTimeout 的回调共享同一个 i,而且 setTimeout 是在循环结束后才执行的。
解决方式是使用 let(块级作用域)或者创建闭包来捕获每次循环的值:
// 方式一:使用 let
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出 0、1、2
}, 100);
}
// 方式二:使用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
(function(capturedI) {
setTimeout(() => {
console.log(capturedI); // 输出 0、1、2
}, 100);
})(i);
}
let 在每次循环迭代都会创建一个新的绑定,所以每个 setTimeout 回调捕获的是不同的 i。
闭包的内存问题
闭包会阻止 JavaScript 引擎垃圾回收那些本该被回收的变量。如果闭包长期存在,对应的词法环境就不会被垃圾回收。
function createLargeArray() {
const hugeArray = new Array(1000000).fill('data');
return function() {
return hugeArray.length;
};
}
const getter = createLargeArray(); // hugeArray 永远不会被回收,因为 getter 闭包持有它
这是一个潜在的内存泄漏源。解决方案是:在不需要闭包时解除对它的引用,或者在使用完毕后把不再需要的引用设为 null。
现代浏览器的垃圾回收器已经能处理循环引用,但在某些场景下(尤其是 IE 浏览器的旧版本),不正确的闭包使用仍然可能导致内存泄漏。
这一章想说的
作用域决定了变量的可访问范围,作用域链让 JavaScript 引擎能逐层向外查找变量。词法作用域是 JavaScript 的默认行为,作用域在代码定义时就确定了。
闭包是函数能记住它定义时的词法环境的特性。这让函数能携带"记忆",是实现模块模式、防抖、节流等高级功能的基础。但闭包也可能导致意外的内存问题,使用时需要谨慎。