闭包
1. 作用域
基本概念
JavaScript 有如下的作用域:
- 全局作用域 (Global Scope): 在所有函数和代码块
{}
外面定义的变量,在代码的任何地方都可以被访问。
过度使用全局变量会造成“全局污染”。
- 函数作用域 (Function Scope): 使用
var
在一个函数内部声明的变量,它的“地盘”就是整个函数。 - 块级作用域 (Block Scope): ES6 引入的
let
和const
声明的变量,它的“地盘”是离它最近的一对花括号{}
。这让代码控制更精确。
let globalAge = 23; // 全局作用域
function printAge (age) { // 函数作用域开始
var varAge = 34; // 函数作用域
if (age > 0) { // 块级作用域开始
// constAge 只在这个 if 的“房间”里有效
const constAge = age * 2;
console.log(constAge); // 正确
} // 块级作用域结束
// console.log(constAge); // 错误! 访问不到
} // 函数作用域结束
printAge(globalAge);
// console.log(varAge); // 错误! 访问不到
var
的局限性
let
和 const
的引入解决了 var
带来的许多问题(如变量提升、无块级作用域),是现代 JavaScript 的首选。下面我们详细讲讲 var
的这些问题。
变量提升
在使用 var
声明变量时,该变量的声明会被 JavaScript 引擎“提升”到其所在作用域的顶部,但赋值操作会留在原地。这会导致如下奇怪的结果:
console.log(myVar); // 输出:undefined
var myVar = 10;
console.log(myVar); // 输出:10
它允许你在声明变量之前就使用它(尽管值是 undefined
),这不符合正常的编程逻辑,并且是很多潜在 bug 的来源。而 let
和 const
就避免了这个问题。
无块级作用域
var
声明的变量只遵从函数作用域或全局作用域,它会无视 {}
块的存在。这会导致一些非常反直觉的结果。例如下面的例子:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
这个代码会连续打印5次5。这是因为 var
没有块级作用域,所以整个 for
循环中,从始至终都只有一个 i
变量。而 setTimeout
里的函数是异步的,它会在循环全部结束之后才开始执行。当它们开始执行并准备打印 i
时,它们访问的都是那个全局唯一的、值已经变成5的 i
变量。
而假如我们用 let
声明 i
的话,由于 let
具有块级作用域,在 for
循环中使用 let
时,JavaScript 引擎在每一次循环都会创建一个新的、独立的 i
变量。这就避免了使用 var
造成的问题。
let
和const
提供了块级作用域,变量的生命周期被限制在最小的必要范围内,极大地减少了变量污染和命名冲突的风险
2. 闭包
闭包 (Closure) 是 JavaScript 一个核心且强大的概念。
基本概念
闭包是一个函数以及其**被创建时所在的词法环境(Lexical Environment)**的组合。
想象一个函数是一个“魔法背包”。当这个函数被创建时,它会把它“出生地”周围的所有局部变量都装进这个背包里。之后,无论这个函数被带到哪里去执行,它都可以随时打开这个背包,使用里面存放的变量。这个“魔法背包”就是闭包。它让函数可以 “记住”并访问它被创建时的环境。
function makeAdding (firstNumber) {
// "first" 是 makeAdding 函数这个“出生地”的局部变量
const first = firstNumber;
// 返回的这个函数,会背上“魔法背包”
return function resulting (secondNumber) {
// 关键!它可以访问背包里的 "first",即使 makeAdding 已经执行完毕
return first + secondNumber;
}
}
// add5 是一个背着背包的函数,背包里装着 { first: 5 }
const add5 = makeAdding(5);
// 当我们调用 add5 时,它打开背包,拿出里面的 5,和新传入的 2 相加
console.log(add5(2)) // 输出 7
闭包原理
JavaScript 采用的是词法作用域(Lexical Scoping),意味着函数的作用域在它被定义时就已经确定了,而不是在它被调用时。
在上面的例子中,resulting
函数在 makeAdding
内部被定义,所以它的作用域链包含了 makeAdding
的作用域。即使 makeAdding
执行完毕,因为 add5
变量仍然引用着 resulting
函数,垃圾回收机制就不会销毁被 resulting
函数引用的 first
变量。
3. 工厂函数
工厂函数是一个不使用 new
关键字、不依赖 this
、只负责返回一个新对象的普通函数。
// 构造函数 (旧方法)
const User = function (name) {
this.name = name;
this.discordName = "@" + name;
}
// 使用: const user1 = new User("Bob");
// 缺点: 忘记 new 会污染全局对象,this 指向不明确,instanceof 不可靠。
// 工厂函数 (推荐)
function createUser (name) {
const discordName = "@" + name;
// 直接创建并返回一个对象,清晰直观
return { name, discordName };
}
// 使用: const user2 = createUser("Alice");
工厂函数结合闭包,能创造出拥有私有变量的强大对象,这是实现**封装(Encapsulation)**的关键。
function createPlayer (name) {
// 私有变量,外界无法直接访问
let _reputation = 0;
// 私有函数
const _changeReputation = (val) => _reputation += val;
// 返回的对象是暴露给外界的“公共接口”
return {
name,
giveReputation: () => _changeReputation(1),
takeReputation: () => _changeReputation(-1),
getReputation: () => _reputation // 只提供一个“读取”方法
};
}
const josh = createPlayer("josh");
josh.giveReputation();
josh.giveReputation();
// josh._reputation = 100; // 无效!_reputation 是私有的
console.log(josh.getReputation()); // 输出 2