JS基础总结 - 作用域和闭包
1. 作用域
作用域(Scope)指的是代码中定义变量的区域或上下文,它决定了代码中哪些部分可以访问哪些变量。JavaScript 中的作用域主要有两种:全局作用域和局部作用域。
1.1 全局作用域
在最外层定义的变量具有全局作用域,可以在任何地方访问。
var globalVar = "I am a global variable";
function globalScopeTest() {
console.log(globalVar); // "I am a global variable"
}
globalScopeTest();
console.log(globalVar); // "I am a global variable"
1.2 局部作用域(函数作用域)
在函数内部定义的变量具有局部作用域,只能在函数内部访问。
function localScopeTest() {
var localVar = "I am a local variable";
console.log(localVar); // "I am a local variable"
}
localScopeTest();
console.log(localVar); // ReferenceError: localVar is not defined
1.3 块级作用域(ES6新增)
ES6 引入了 let 和 const 关键字,这些关键字定义的变量具有块级作用域,只在定义它们的块中有效。
{
let blockVar = "I am a block-scoped variable";
console.log(blockVar); // "I am a block-scoped variable"
}
console.log(blockVar); // ReferenceError: blockVar is not defined
2. 闭包
2.1 什么是闭包?
闭包(Closure)是指在创建它的环境中捕获并保存外部变量的函数。闭包使得函数可以访问和操作其外部的变量,即使这个函数在其词法作用域之外被调用。
简单的说:闭包是返回函数内部变量(自由变量)的函数。(不仅返回函数,而且返回了该函数的运行环境)
ps: 自由变量
一个变量在当前作用域,没有被定义,到外层作用域中被找到。
所有的自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方!!!
2.2 基本用法:
- 函数作为返回值
- 函数作为参数
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
2.3 实际应用场景
1. 数据隐藏和封装:闭包可以用于模拟私有变量,使得一些数据只能通过特定的函数访问。
function Person(name) {
let _name = name; // 私有变量
this.getName = function() {
return _name;
};
this.setName = function(newName) {
_name = newName;
};
}
const person = new Person("Alice");
console.log(person.getName()); // Alice
person.setName("Bob");
console.log(person.getName()); // Bob
2. 工厂函数:工厂函数是一种设计模式,通过函数返回新对象。这种方法通常用于避免重复创建对象时的冗余代码,同时保持代码的可维护性和灵活性。
// 简单的计数器工厂函数
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
let counter1 = createCounter();
console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter1.decrement()); // 1
let counter2 = createCounter();
console.log(counter2.increment()); // 1
console.log(counter2.getCount()); // 1
console.log(counter1.getCount()); // 1 (counter1 和 counter2 的状态是独立的)
3. 回调函数:在异步编程中,闭包经常用于保持某些数据的状态。
function asyncOperation(message, callback) {
setTimeout(() => {
console.log(message);
callback();
}, 1000);
}
asyncOperation("Hello, World!", function() {
console.log("Callback executed!");
});
2.3 闭包可能导致的内存泄漏
如果不当使用闭包,也可能导致内存泄漏。要理解如何防止闭包导致的内存泄漏,首先需要理解什么是闭包,以及闭包可能导致内存泄漏的原因。
由于闭包持有对外部函数作用域的引用,因此如果闭包函数存在且未被释放,它会阻止外部函数中的变量被垃圾回收。这会导致内存泄漏,尤其是在长时间运行的程序或在处理大量数据时更为明显。
防止闭包导致内存泄漏的方法
-
避免不必要的闭包: 只在确实需要闭包功能时使用闭包,避免在可以使用普通函数或局部变量的地方使用闭包。
-
及时清理闭包: 在不再需要使用闭包时,手动解除对闭包的引用,帮助垃圾回收机制回收内存。例如,在JavaScript中,可以将闭包变量设为
null
。 -
使用IIFE(立即执行函数表达式): IIFE创建的变量在函数执行完毕后不会被继续引用,从而减少内存泄漏的风险。
-
避免在全局对象上创建闭包: 尽量避免在全局对象(如
window
)上创建闭包,这样会使闭包的生命周期与应用程序的生命周期相同,从而增加内存泄漏的风险。 -
使用WeakMap: 在JavaScript中,可以使用
WeakMap
来存储闭包引用。WeakMap
允许对键的弱引用,从而不会阻止垃圾回收。
示例代码
以下是一些示例代码,演示如何使用上述方法防止闭包导致的内存泄漏。
示例1:避免不必要的闭包
function createCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
let counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
counter = null; // 清理闭包,防止内存泄漏
示例2:使用IIFE
(function() {
let count = 0;
function increment() {
count++;
console.log(count);
}
increment();
increment();
})(); // 1 2
// 立即执行函数表达式执行完毕,`count`和`increment`不会被继续引用
示例3:使用WeakMap
const wm = new WeakMap();
(function() {
let obj = {};
function createClosure() {
let count = 0;
return function increment() {
count++;
return count;
};
}
wm.set(obj, createClosure());
})();
// 由于WeakMap的使用,闭包可以被回收,不会导致内存泄漏
通过理解闭包的工作机制并采用适当的方法,可以有效防止闭包导致的内存泄漏问题。
总结
- 作用域:决定了变量和函数的可访问范围。JavaScript 有全局作用域、局部作用域和块级作用域。
- 闭包:是一个函数能够访问其词法作用域中的变量,即使这个函数在其词法作用域之外被调用。闭包用于数据隐藏、回调函数和模块模式等场景。
理解和运用 JavaScript 的作用域和闭包,可以写出更健壮、灵活和可维护的代码。