今天最近复习到闭包和它的应用场景的八股文,把以前学习的闭包、柯里化、异步/回调的知识串联起来给大家讲讲闭包的重要性和使用场景。
闭包(Closure)
闭包是指函数“记住”它定义时的词法作用域(即外部函数的变量),即使这个函数在其作用域之外执行。
闭包的核心特点是:函数可以访问定义它时外部的变量,即使这个函数在其他地方被调用。
举例:
function outer() {
const outerVar = 'I am from outer scope';
return function inner() {
console.log(outerVar); // inner 函数能够访问 outer 函数的变量
};
}
const closureFunction = outer();
closureFunction(); // 输出 'I am from outer scope'
在这个例子中,inner 函数在 outer 函数内部定义,即使 outer 函数已经执行完毕并返回了,inner 函数依然可以访问 outer 中的 outerVar。这就是闭包的行为。
- 为什么闭包重要?
- 它允许我们创建私有变量,隐藏状态,防止外部访问或修改。
- 它让函数拥有“记忆”,即函数内部可以保持对外部变量的引用。
闭包的作用
闭包的作用在于允许内部函数访问并“记住”它的外部函数的变量,即使外部函数已经执行完毕。闭包是 JavaScript 中非常强大且常用的功能,它提供了多种编程优势和灵活性。以下是闭包的几大主要作用和应用场景:
1. 数据持久化与封装
闭包可以将变量的状态保存下来并隐藏在作用域内,外部无法直接访问这些变量,只能通过内部函数操作。这种封装和持久化的能力类似于面向对象编程中的私有变量。
示例:创建私有变量
function counter() {
let count = 0; // 这个变量被封装在闭包中,外部无法直接访问
return function() {
count++;
return count;
};
}
const increment = counter(); // 创建了一个闭包,count 被“记住”了
console.log(increment()); // 输出: 1
console.log(increment()); // 输出: 2
console.log(increment()); // 输出: 3
在这个例子中,count 变量是私有的,外部无法直接修改它。只有通过返回的 increment 函数,才能对 count 进行操作。
2. 柯里化(Currying)
柯里化是将一个接受多个参数的函数,转化为一系列只接收一个参数的函数。每个函数返回一个新的函数,接受下一个参数,直到所有参数都被提供完毕。
换句话说,柯里化是一种将多参数函数转化为嵌套的单参数函数的技术。
举例:
// 使用闭包实现函数柯里化
// 获取长方形面积
function getArea(width, height) {
return width * height;
}
// 发现长都是10
const area1 = getArea(10, 20);
const area2 = getArea(10, 30);
const area3 = getArea(10, 40);
// 使用闭包柯里化函数
// 获取width
// 这个函数的作用是返回一个函数,返回的函数接受height参数,返回width * height
function getArea(width) {
return (height) => {
return width * height;//这里面的width是外部函数的width 实际上是getArea的参数
};
}
const getTenWidthArea = getArea(10);
const area = getTenWidthArea(20);
const getTwentyWidthArea = getArea(20);
在这里我们想写一个获取长方形面积的函数,原本需要传入两个值,但是我们可以通过柯里化重新写这个函数,它需要两次调用新的函数,第一次传入10作为长度,这里通过getTenWidthArea保存下来了,整个过程是获取width作为10。
function getArea(width=20) {
return (height) => {
return width * height;//这里面的width是外部函数的width 实际上是getArea的参数
};
}
然后函数会返回一个新的函数
(height) => {
return width * height;//这里面的width是外部函数的width 实际上是getArea的参数
};
这时候原来的getTenWidthArea()函数就变为了接收height,并进行相乘计算的函数,由于我们在第一次的作用域里面创建了width,这时候虽然没有调用了,但是它被保存在作用域里面了,接着进行了height传值,最后会返回width * height。
为什么使用柯里化?
- 代码复用:通过柯里化,可以很容易地创建带有默认参数的函数
- 延迟计算:柯里化使得我们可以延迟计算,直到所有参数都被提供完毕。
- 函数组合:在函数式编程中,柯里化使得我们可以更轻松地将多个函数组合起来。
更复杂的柯里化例子:
function curriedSum(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedSum(1)(2)(3)); // 输出 6
上面的 curriedSum 是一个三参数的柯里化函数,它接收一个参数 a,返回一个函数,再接收一个参数 b,再返回一个函数,最终接收 c 参数并计算 a + b + c。
闭包与柯里化的区别与联系:
- 闭包:指的是函数记住了它定义时的作用域,并在执行时可以访问外部作用域的变量。
- 柯里化:是将一个多参数函数分解成一系列单参数函数的过程。
柯里化通常依赖于闭包,因为每次返回的嵌套函数都“记住”了前面传入的参数。例如,在柯里化过程中,每个函数会“闭包”捕获之前的参数,直到最后一个函数被调用时,所有的参数才会被使用。
3. 防止污染全局命名空间
在 JavaScript 中,全局作用域的污染会导致命名冲突。通过使用闭包,可以将变量局限在一个函数的作用域中,而不是暴露给全局环境,从而避免变量污染。
示例:避免全局变量污染
(function() {
let privateVar = 'I am private';
console.log(privateVar); // 输出: I am private
})();
console.log(privateVar); // 报错:privateVar 未定义
通过自执行函数的闭包,privateVar 只能在内部访问,避免了它在全局作用域中的泄漏。
4. 创建工厂函数或生成器
闭包可以用来创建类似“工厂”的函数,工厂函数可以通过传递不同的初始参数,生成带有不同行为的函数。
示例:生成器函数
function createAdder(x) {
return function(y) {
return x + y;
};
}
const addFive = createAdder(5); // 创建了一个闭包,x = 5
console.log(addFive(10)); // 输出: 15
console.log(addFive(20)); // 输出: 25
在这个例子中,createAdder 是一个工厂函数,它生成了可以与固定值(x)相加的新函数。
5.回调和异步
回调(Callback) 是一种编程技术,指的是将一个函数作为参数传递给另一个函数,当这个函数完成某些任务后,调用传递进去的函数。换句话说,回调函数是在一个函数执行完成后执行的函数。
回调机制在 JavaScript 中尤其常见,尤其是在处理异步操作时,如事件处理、网络请求等。
回调的基本原理
在 JavaScript 中,函数是一等公民,可以作为参数传递给其他函数。因此,我们可以将一个函数作为参数传递给另一个函数,待某个任务完成后执行这个函数,这就是回调的核心思想。
示例 1:同步回调
这是一个最简单的回调,函数执行时直接调用另一个函数。
function greet(name, callback) {
console.log(`Hello, ${name}`);
callback(); // 调用传入的回调函数
}
function sayGoodbye() {
console.log('Goodbye!');
}
greet('Alice', sayGoodbye); // 输出:Hello, Alice 然后输出:Goodbye!
在这个例子中,greet 函数接收两个参数,一个是 name,另一个是 callback 函数。greet 打印了欢迎信息后,调用了 callback(),即 sayGoodbye() 函数。
回调的应用场景:异步操作
回调在处理异步操作时非常常用,比如:网络请求、定时器、文件读取等。在异步操作中,函数不会立即返回结果,而是在操作完成后执行回调函数。
示例 2:异步回调
这是一个异步回调的例子,使用 setTimeout 来模拟一个延时操作。
function fetchData(callback) {
console.log('开始获取数据...');
setTimeout(() => {
console.log('数据获取完成');
callback('数据内容'); // 模拟数据获取完成后调用回调
}, 2000); // 模拟 2 秒的数据获取延时
}
function processData(data) {
console.log(`处理数据: ${data}`);
}
fetchData(processData); // 输出顺序:开始获取数据... -> 2 秒后 -> 数据获取完成 -> 处理数据: 数据内容
在这个例子中,fetchData 函数异步获取数据(用 setTimeout 模拟),完成数据获取后执行回调函数 processData,将获取的数据传递给它。回调函数 processData 负责处理获取到的数据。
常见的回调场景
-
事件处理:当用户触发某个事件(如点击、输入等),会调用注册的事件处理函数。
button.addEventListener('click', () => { console.log('按钮被点击了'); }); -
网络请求:发送网络请求后,服务器返回结果,调用回调函数处理响应数据。
axios.get('https://api.example.com/data') .then(response => { console.log('获取的数据:', response.data); }) .catch(error => { console.error('请求失败:', error); }); -
定时器:在设定的时间后执行回调函数。
setTimeout(() => { console.log('3 秒后执行这个回调函数'); }, 3000);
回调函数的优缺点
优点:
- 灵活:可以将不同的函数传递作为回调,根据需求动态决定任务完成后做什么。
- 常用于异步处理:JavaScript 是单线程的,回调函数是处理异步操作的主要方式(如网络请求、文件操作等)。
缺点:
- 回调地狱(Callback Hell):当回调嵌套过多时,代码的可读性和维护性都会变差,导致“回调地狱”。
asyncOperation1(() => { asyncOperation2(() => { asyncOperation3(() => { console.log('操作完成'); }); }); });
解决回调地狱:Promise 和 async/await
为了避免过度的回调嵌套(回调地狱),JavaScript 引入了 Promise 和 async/await 语法。
- Promise:提供了一种更清晰的方式来处理异步操作。
- async/await:基于
Promise,提供了类似同步代码的写法,让异步代码更简洁。
使用 Promise 替代回调:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('数据内容');
}, 2000);
});
}
fetchData().then(data => {
console.log(`处理数据: ${data}`);
}).catch(error => {
console.error('出错了:', error);
});
使用 async/await 替代回调:
async function process() {
const data = await fetchData(); // 等待 fetchData 完成
console.log(`处理数据: ${data}`);
}
process();
总结:
- 回调函数是将函数作为参数传递给另一个函数,等到特定任务完成后再调用这个函数。
- 回调在处理异步操作时非常重要,如网络请求、定时器、事件监听等。
- 回调容易导致嵌套过多(回调地狱),可以通过
Promise和async/await来解决。
6. 函数防抖和节流
闭包可以用于实现函数防抖(debouncing)和节流(throttling),在高频率的事件触发时限制函数的执行次数。
示例:函数防抖
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const logMessage = debounce(() => console.log('Function executed!'), 1000);
window.addEventListener('scroll', logMessage); // 在用户停止滚动1秒后执行
闭包在防抖函数中保存了 timer,从而确保函数在指定的时间间隔内只执行一次。
7. 回调函数中访问外部状态
闭包使得函数可以在不改变函数签名的情况下访问额外的上下文信息。在事件处理、网络请求等场景中,闭包常用于回调函数的场景中以携带上下文信息。
示例:事件处理中的闭包
function setupButton(buttonId, message) {
document.getElementById(buttonId).addEventListener('click', function() {
console.log(message);
});
}
setupButton('btn1', 'Button 1 clicked'); // 创建闭包,message = 'Button 1 clicked'
setupButton('btn2', 'Button 2 clicked'); // 创建闭包,message = 'Button 2 clicked'
这里,通过闭包,message 被保存下来,每个按钮点击时都能输出正确的消息。
总结:
闭包的作用主要体现在以下几个方面:
- 数据持久化:在函数外部无法访问内部的变量,变量值会被持久化。
- 函数柯里化:可以实现参数部分预先传递,提高代码的复用性。
- 命名空间隔离:避免全局变量污染。
- 工厂函数生成:通过闭包创建带有特定初始值的函数。
- 异步编程:在异步操作中,闭包可以访问函数创建时的上下文环境。
- 节流和防抖:闭包可以存储计时器状态,减少事件频繁触发带来的性能问题。
闭包是 JavaScript 中非常强大且灵活的功能,能够帮助开发者在多种场景下实现更优雅和高效的代码。