首页/文章/javascript

哈士奇的学习笔记:聊聊闭包和它的应用场景

2024-09-11
25889 分钟
...

闭包(Closure)

闭包是指函数“记住”它定义时的词法作用域(即外部函数的变量),即使这个函数在其作用域之外执行。

闭包的核心特点是:函数可以访问定义它时外部的变量,即使这个函数在其他地方被调用

举例:

function outer() {
  const outerVar = 'I am from outer scope';
  
  return function inner() {
    console.log(outerVar); 
  };
}

const closureFunction = outer();
closureFunction(); 

在这个例子中,inner 函数在 outer 函数内部定义,即使 outer 函数已经执行完毕并返回了,inner 函数依然可以访问 outer 中的 outerVar。这就是闭包的行为。

  • 为什么闭包重要?
    • 它允许我们创建私有变量,隐藏状态,防止外部访问或修改。
    • 它让函数拥有“记忆”,即函数内部可以保持对外部变量的引用。

闭包的作用

闭包的作用在于允许内部函数访问并“记住”它的外部函数的变量,即使外部函数已经执行完毕。闭包是 JavaScript 中非常强大且常用的功能,它提供了多种编程优势和灵活性。以下是闭包的几大主要作用和应用场景:

1. 数据持久化与封装

闭包可以将变量的状态保存下来并隐藏在作用域内,外部无法直接访问这些变量,只能通过内部函数操作。这种封装和持久化的能力类似于面向对象编程中的私有变量。

示例:创建私有变量

function counter() {
  let count = 0; 
  return function() {
    count++;
    return count;
  };
}

const increment = counter(); 
console.log(increment()); 
console.log(increment()); 
console.log(increment()); 

在这个例子中,count 变量是私有的,外部无法直接修改它。只有通过返回的 increment 函数,才能对 count 进行操作。

2. 柯里化(Currying)

柯里化是将一个接受多个参数的函数,转化为一系列只接收一个参数的函数。每个函数返回一个新的函数,接受下一个参数,直到所有参数都被提供完毕

换句话说,柯里化是一种将多参数函数转化为嵌套的单参数函数的技术。

举例:
function getArea(width, height) {
  return width * height;
}

const area1 = getArea(10, 20);
const area2 = getArea(10, 30);
const area3 = getArea(10, 40);



function getArea(width) {
  return (height) => {
    return width * height;
  };
}
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;
  };
}

然后函数会返回一个新的函数

(height) => {
    return width * height;
  };

这时候原来的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)); 

上面的 curriedSum 是一个三参数的柯里化函数,它接收一个参数 a,返回一个函数,再接收一个参数 b,再返回一个函数,最终接收 c 参数并计算 a + b + c


闭包与柯里化的区别与联系:

  • 闭包:指的是函数记住了它定义时的作用域,并在执行时可以访问外部作用域的变量。
  • 柯里化:是将一个多参数函数分解成一系列单参数函数的过程。

柯里化通常依赖于闭包,因为每次返回的嵌套函数都“记住”了前面传入的参数。例如,在柯里化过程中,每个函数会“闭包”捕获之前的参数,直到最后一个函数被调用时,所有的参数才会被使用。

3. 防止污染全局命名空间

在 JavaScript 中,全局作用域的污染会导致命名冲突。通过使用闭包,可以将变量局限在一个函数的作用域中,而不是暴露给全局环境,从而避免变量污染。

示例:避免全局变量污染

(function() {
  let privateVar = 'I am private';
  console.log(privateVar); 
})();

console.log(privateVar); 

通过自执行函数的闭包,privateVar 只能在内部访问,避免了它在全局作用域中的泄漏。

4. 创建工厂函数或生成器

闭包可以用来创建类似“工厂”的函数,工厂函数可以通过传递不同的初始参数,生成带有不同行为的函数。

示例:生成器函数

function createAdder(x) {
  return function(y) {
    return x + y;
  };
}

const addFive = createAdder(5); 
console.log(addFive(10)); 
console.log(addFive(20)); 

在这个例子中,createAdder 是一个工厂函数,它生成了可以与固定值(x)相加的新函数。

5.回调和异步

回调(Callback) 是一种编程技术,指的是将一个函数作为参数传递给另一个函数,当这个函数完成某些任务后,调用传递进去的函数。换句话说,回调函数是在一个函数执行完成后执行的函数。

回调机制在 JavaScript 中尤其常见,尤其是在处理异步操作时,如事件处理、网络请求等。

回调的基本原理

在 JavaScript 中,函数是一等公民,可以作为参数传递给其他函数。因此,我们可以将一个函数作为参数传递给另一个函数,待某个任务完成后执行这个函数,这就是回调的核心思想。

示例 1:同步回调

这是一个最简单的回调,函数执行时直接调用另一个函数。

function greet(name, callback) {
  console.log(`Hello, ${name}`);
  callback(); 
}

function sayGoodbye() {
  console.log('Goodbye!');
}

greet('Alice', sayGoodbye); 

在这个例子中,greet 函数接收两个参数,一个是 name,另一个是 callback 函数。greet 打印了欢迎信息后,调用了 callback(),即 sayGoodbye() 函数。

回调的应用场景:异步操作

回调在处理异步操作时非常常用,比如:网络请求、定时器、文件读取等。在异步操作中,函数不会立即返回结果,而是在操作完成后执行回调函数。

示例 2:异步回调

这是一个异步回调的例子,使用 setTimeout 来模拟一个延时操作。

function fetchData(callback) {
  console.log('开始获取数据...');
  setTimeout(() => {
    console.log('数据获取完成');
    callback('数据内容'); 
  }, 2000); 
}

function processData(data) {
  console.log(`处理数据: ${data}`);
}

fetchData(processData); 

在这个例子中,fetchData 函数异步获取数据(用 setTimeout 模拟),完成数据获取后执行回调函数 processData,将获取的数据传递给它。回调函数 processData 负责处理获取到的数据。

常见的回调场景

  1. 事件处理:当用户触发某个事件(如点击、输入等),会调用注册的事件处理函数。

    button.addEventListener('click', () => {
      console.log('按钮被点击了');
    });
    
  2. 网络请求:发送网络请求后,服务器返回结果,调用回调函数处理响应数据。

    axios.get('https://api.example.com/data')
      .then(response => {
        console.log('获取的数据:', response.data);
      })
      .catch(error => {
        console.error('请求失败:', error);
      });
    
  3. 定时器:在设定的时间后执行回调函数。

    setTimeout(() => {
      console.log('3 秒后执行这个回调函数');
    }, 3000);
    

回调函数的优缺点

优点

  • 灵活:可以将不同的函数传递作为回调,根据需求动态决定任务完成后做什么。
  • 常用于异步处理:JavaScript 是单线程的,回调函数是处理异步操作的主要方式(如网络请求、文件操作等)。

缺点

  • 回调地狱(Callback Hell):当回调嵌套过多时,代码的可读性和维护性都会变差,导致“回调地狱”。```javascript
    asyncOperation1(() => {
    asyncOperation2(() => {
    asyncOperation3(() => {
    console.log('操作完成');
    });
    });
    });

解决回调地狱:Promise 和 async/await

为了避免过度的回调嵌套(回调地狱),JavaScript 引入了 Promiseasync/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(); 
  console.log(`处理数据: ${data}`);
}

process();

总结:

  • 回调函数是将函数作为参数传递给另一个函数,等到特定任务完成后再调用这个函数。
  • 回调在处理异步操作时非常重要,如网络请求、定时器、事件监听等。
  • 回调容易导致嵌套过多(回调地狱),可以通过 Promiseasync/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); 

闭包在防抖函数中保存了 timer,从而确保函数在指定的时间间隔内只执行一次。

7. 回调函数中访问外部状态

闭包使得函数可以在不改变函数签名的情况下访问额外的上下文信息。在事件处理、网络请求等场景中,闭包常用于回调函数的场景中以携带上下文信息。

示例:事件处理中的闭包

function setupButton(buttonId, message) {
  document.getElementById(buttonId).addEventListener('click', function() {
    console.log(message);
  });
}

setupButton('btn1', 'Button 1 clicked'); 
setupButton('btn2', 'Button 2 clicked'); 

这里,通过闭包,message 被保存下来,每个按钮点击时都能输出正确的消息。

总结:

闭包的作用主要体现在以下几个方面:

  1. 数据持久化:在函数外部无法访问内部的变量,变量值会被持久化。
  2. 函数柯里化:可以实现参数部分预先传递,提高代码的复用性。
  3. 命名空间隔离:避免全局变量污染。
  4. 工厂函数生成:通过闭包创建带有特定初始值的函数。
  5. 异步编程:在异步操作中,闭包可以访问函数创建时的上下文环境。
  6. 节流和防抖:闭包可以存储计时器状态,减少事件频繁触发带来的性能问题。

闭包是 JavaScript 中非常强大且灵活的功能,能够帮助开发者在多种场景下实现更优雅和高效的代码。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2026-02-24
navigator.sendBeacon全指南
在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户 关闭页面 、 刷新 或 跳转路由 时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。 但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— 。 一、 痛点与传统方案的挣扎 场景还原 当用户点击关闭按钮时,浏览器会触发生命周期事件( 或 )。如果我们直接使用普通的异步 AJAX ( 或 ) 发送请求,浏览...
学习
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录