JavaScript 函数:作用域、闭包与高阶函数
在上一篇文章中我们了解了 JavaScript 函数的基本概念——如何定义函数、传递参数、返回值以及箭头函数的用法。但会写函数、会调用函数只是入门的第一步。你有没有想过为什么一个函数可以访问在它外面声明的变量为什么一个函数在创建它的外层函数已经执行完之后还能访问那个变量为什么一个函数可以被当作参数传给另一个函数这些行为背后涉及 JavaScript 三个核心概念作用域、闭包和高阶函数。本文带你一步步理解它们并用实例说明它们之间的联系。如果你在手机上调试网页也可以先了解一下如何让 iPhone Safari 在后台打开新标签页方便同时查看代码和文档。一、理解作用域作用域决定了变量在代码中的哪些位置可以被访问。看这个例子const name Dini; function greet() { console.log(name); } greet();greet()函数可以访问name尽管name并不是在函数内部声明的。这是因为name声明在外部作用域中。再看另一个例子function greet() { const message Hello, Dini!; console.log(message); // Hello, Dini! } greet(); console.log(message); // ReferenceError: message is not defined第一个console.log()正常运行因为message是在greet()内部声明的。但第二个会报错因为message只存在于函数的作用域里。全局作用域 └── greet() └── messagemessage在greet()内部可以访问但从外部访问不到。这就是作用域的核心一个变量只能在它被定义的作用域以及嵌套在它内部的作用域中被访问。JavaScript 有以下几种作用域全局作用域脚本模式下所有代码的默认作用域。模块作用域模块模式下代码的作用域。函数作用域由函数创建的作用域。块级作用域由一对花括号代码块创建的作用域。注意代码块只对let和const声明生效对var不生效。{ var x 10; } console.log(x); // 10{ const x 10; } console.log(x); // ReferenceError: x is not defined二、词法作用域与作用域链JavaScript 使用词法作用域也就是说变量的可访问性由代码书写的位置决定而不是由函数调用的位置决定。const name Dini; function outer() { const greeting Hello; function inner() { console.log(greeting); console.log(name); } inner(); } outer();inner()可以同时访问greeting和name。为什么因为 JavaScript 通过作用域链来查找变量。inner() ↓ outer() ↓ 全局作用域当 JavaScript 执行console.log(greeting)时先在inner()内部找greeting没找到再到外层outer()找找到了查找停止。而执行console.log(name)时inner()和outer()里都没有name于是继续往全局作用域找最终在那里找到。这就是作用域链。要记住这个关系只能从内层往外层走。内层函数可以访问外层作用域的变量反过来不行。这个概念非常重要因为它是理解闭包的基础。三、什么是闭包理解了作用域和作用域链之后来看一个初看可能觉得奇怪的现象function createGreeting() { const message Hello, Dini!; return function () { console.log(message); }; } const greet createGreeting(); greet();输出是Hello, Dini!你可能会疑惑createGreeting()已经执行完了greet()为什么还能访问message按常理外层函数执行完后它内部的变量应该被回收才对。这就是闭包在起作用。函数会记住它诞生时的作用域当 JavaScript 创建createGreeting()返回的那个内部函数时这个函数会保持对其创建时所处作用域中所有变量的访问权。greet ↓ function () { console.log(message); } ↓ 记住了对 ↓ message Hello, Dini! 的访问也就是说即使createGreeting()已经执行完毕返回的函数仍然能访问它创建时的环境。这种行为就叫闭包。简单定义闭包是指函数在其外层函数执行完毕后仍然保持对外层词法作用域中变量的访问能力。关键点在于函数记住的不是变量的值的拷贝而是对变量本身的引用。闭包可以保存状态看一个计数器的例子function createCounter() { let count 0; return function () { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3count声明在createCounter()内部。执行完const counter createCounter()后createCounter()已经结束了。但每次调用counter()count的值仍然存在并且在之前的基础上累加。这是因为返回的函数形成了对count的闭包在多次调用之间维持了这个变量的状态。闭包创建独立的状态更有意思的是如果创建两个计数器function createCounter() { let count 0; return function () { count; return count; }; } const counterA createCounter(); const counterB createCounter(); console.log(counterA()); // 1 console.log(counterA()); // 2 console.log(counterB()); // 1 console.log(counterB()); // 2虽然两个计数器来自同一个函数但它们各自维护独立的count互不影响。每次调用createCounter()都会创建一个新的词法环境返回的函数闭包在各自的环境上。这就是闭包的强大之处它可以用来创建独立、持久的状态。闭包有什么用闭包不只是 JavaScript 的一个有趣特性它在实际开发中用途广泛在多次函数调用之间保持状态创建函数工厂封装数据创建需要访问外部变量的回调函数处理异步操作举个函数工厂的例子function createMultiplier(multiplier) { return function (number) { return number * multiplier; }; } const double createMultiplier(2); const triple createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15double记住了multiplier 2triple记住了multiplier 3。同一个工厂函数可以生成不同的函数各自保留自己的状态。这就是闭包的核心思想一个函数可以携带它创建时所处环境的访问权。四、函数是值JavaScript 函数还有一个重要特性函数就是值。这意味着函数可以像其他值一样对待——可以赋值给变量、可以传给另一个函数、也可以从函数中返回。把函数赋值给变量const greet function () { console.log(Hello!); }; greet();这里greet存储的不是函数的执行结果而是对函数本身的引用。所以要加括号greet()才会执行。还可以把同一个函数赋值给另一个变量const greet function () { console.log(Hello!); }; const sayHello greet; sayHello();把函数作为参数传递function greet(name) { console.log(Hello, ${name}!); } function processUser(name, callback) { callback(name); } processUser(Dini, greet);注意我们传的是greet不是greet()。传greet是把函数本身传过去传greet()会立刻执行函数传过去的是它的返回值。这种被传给另一个函数、稍后再被执行的函数通常叫做回调函数。从函数中返回另一个函数function createGreeting(name) { return function () { console.log(Hello, ${name}!); }; } const greetDini createGreeting(Dini); greetDini();这个例子和闭包那一节很像。返回的函数不仅是一个函数值它还闭包在了name变量上。const greetDini createGreeting(Dini); const greetNaufal createGreeting(Naufal); greetDini(); // Hello, Dini! greetNaufal(); // Hello, Naufal!每个返回的函数都能访问自己那份name。函数可以被存储、传递和返回——这就是所谓的一等函数。JavaScript 把函数当作一等公民来对待这也是函数如此强大的原因。五、什么是高阶函数既然函数可以接收另一个函数作为参数或者返回一个函数那么操作其他函数的函数叫什么这就是高阶函数。定义高阶函数是满足以下至少一个条件的函数接收另一个函数作为参数返回一个函数作为结果两者都做。看一个简单例子function processNumber(number, operation) { return operation(number); } function double(number) { return number * 2; } const result processNumber(5, double); console.log(result); // 10processNumber()接收double作为参数然后执行它。它不需要知道operation具体做什么只需要接收函数并调用它。这让函数更加灵活和可复用。高阶函数 vs 回调函数这两个概念紧密相关但描述的是不同角色processNumber()是高阶函数它接收另一个函数double()是回调函数它被传给高阶函数稍后被调用。回调函数是被传过去稍后执行的函数高阶函数是接收或返回其他函数的函数。高阶函数也可以返回函数回到之前createMultiplier的例子function createMultiplier(multiplier) { return function (number) { return number * multiplier; }; } const double createMultiplier(2); const triple createMultiplier(3);createMultiplier()返回了一个新函数所以它也是高阶函数。返回的函数同时闭包在了multiplier上。这就是高阶函数和闭包协同工作的典型例子。高阶函数有什么用高阶函数让代码更灵活、可复用、可组合。我们不需要为每种操作写一个独立的函数而是写一个通用函数在调用时传入具体的行为。function calculate(number, operation) { return operation(number); } const double calculate(10, number number * 2); const square calculate(10, number number ** 2); console.log(double); // 20 console.log(square); // 100实际上你大概率已经用过很多次高阶函数了。比如数组的map()const numbers [1, 2, 3, 4, 5]; const doubled numbers.map(number number * 2);map()接收一个函数作为参数所以它是高阶函数。类似的还有filter()、forEach()、reduce()、find()、some()、every()等。六、数组方法中的高阶函数用同一个数据集来演示const users [ { name: Dini, age: 25, active: true }, { name: Naufal, age: 30, active: false }, { name: Nabil, age: 28, active: true }, ];map()map()通过对原数组的每一项进行变换返回一个新数组。const names users.map(user user.name); console.log(names); // [Dini, Naufal, Nabil]用法规则当你想把每一项都变换成另一种形式时用 map()。filter()filter()筛选出满足条件的项返回新数组。const activeUsers users.filter(user user.active); console.log(activeUsers);回调返回true的项被保留返回false的被剔除。用法规则当你想从数组中挑选出符合条件的若干项时用 filter()。forEach()forEach()对每一项执行一次函数但不返回新数组。users.forEach(user { console.log(user.name); }); // Dini // Naufal // Nabil注意forEach()返回undefined。如果你需要得到一个新数组应该用map()。用法规则当你只想对每一项执行某个操作、不需要返回新数组时用 forEach()。reduce()reduce()把数组的多项合并成一个最终结果。const totalAge users.reduce((total, user) { return total user.age; }, 0); console.log(totalAge); // 83第二个参数0是累加器的初始值。回调接收两个参数total当前累加值和user当前项。reduce()的结果不一定是数字也可以是对象const usersByName users.reduce((result, user) { result[user.name] user; return result; }, {}); console.log(usersByName);用法规则当你想把整个数组归约成一个值或一个对象时用 reduce()。总结对比方法作用返回值map()变换每一项新数组filter()筛选符合条件的项新数组forEach()对每一项执行操作undefinedreduce()归约为一个结果单个累加结果这些方法虽然用途不同但模式是一样的数组方法负责怎么遍历回调函数负责对每一项做什么。这就是高阶函数的威力——我们不需要自己写循环逻辑只需要描述想要的行为。比如不使用filter()时我们要这样写const activeUsers []; for (const user of users) { if (user.active) { activeUsers.push(user); } }使用filter()后const activeUsers users.filter(user user.active);第二版更关注我们要什么活跃用户而不是怎么遍历。这就是现代 JavaScript 中高阶函数如此常用的原因。七、总结本文梳理了 JavaScript 函数相关的几个核心概念作用域决定变量在哪里可以被访问。词法作用域与作用域链决定 JavaScript 从哪里开始查找变量。闭包让函数在外部作用域结束后仍能访问其中的变量。函数是值函数可以被存储、传递和返回。高阶函数接收或返回其他函数的函数如map()、filter()、reduce()。理解这些概念之间的联系是写出清晰、优雅 JavaScript 代码的基础。写代码时遇到环境问题可以参考Windows 网络相关设置以及其他实用教程。相关阅读如何手动和自动把 iPhone 备份到 iCloud如何在 Safari 浏览器中允许或拦截弹窗如何关闭查找我的 iPhone

相关新闻

Hadoop HDFS存储平台设计与实战调优指南

Hadoop HDFS存储平台设计与实战调优指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 5:00:35 阅读更多 →
游戏引擎架构 001:从团队分工到底层架构

游戏引擎架构 001:从团队分工到底层架构

游戏引擎架构 001:从团队分工到底层架构Bilibili 同步视频🧑‍💻 游戏不是程序员一个人的狂欢:游戏工作室是怎么运转的?1. 工程师:引擎世界的建造者2. 艺术家:内容为王,撑起游戏的皮…

2026/9/24 5:00:35 阅读更多 →
STC8H8K64U USB下载踩坑记:P3.2引脚操作细节全解析

STC8H8K64U USB下载踩坑记:P3.2引脚操作细节全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 5:00:35 阅读更多 →

最新新闻

PostGraphile wrapPlans 解析器仿真警告(wpr)深度解析:成因、风险与三种解决方案

PostGraphile wrapPlans 解析器仿真警告(wpr)深度解析:成因、风险与三种解决方案

后端API网关 【免费下载链接】crystal 🔮 Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more! 项目地址: https://gitcode.com/gh_mirrors/cry/crystal 点击查看 免费下载 本篇文章围绕 PostGraphi…

2026/9/24 7:05:49 阅读更多 →
村田MLCC料号解码:0603电容替料的12个关键参数陷阱

村田MLCC料号解码:0603电容替料的12个关键参数陷阱

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 7:05:49 阅读更多 →
GPT-6 Sol/Luna 与 Opus 5.5 同日降价:价格表里最该看的,是缓存那一行

GPT-6 Sol/Luna 与 Opus 5.5 同日降价:价格表里最该看的,是缓存那一行

一、发生了什么 北京时间 9 月 23 日凌晨,Anthropic 与 OpenAI 同一天先后发布更便宜的模型:Anthropic 推出 Claude 5.5 系列首款 Claude Opus 5.5,OpenAI 则为 GPT-6 家族补充 GPT-6 Sol 与 GPT-6 Luna 两档(来源:两家…

2026/9/24 7:05:49 阅读更多 →
嵌入式开发入门路线:从STM32裸机到Linux应用

嵌入式开发入门路线:从STM32裸机到Linux应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 7:05:49 阅读更多 →
鸿蒙用户注意!你的PDF文件正在经历一场“编辑荒漠”

鸿蒙用户注意!你的PDF文件正在经历一场“编辑荒漠”

如果你手里拿的是华为手机或平板,正跑在HarmonyOS上,大概率遇到过这样的情况:收到一份PDF合同,想改几个字;翻开一份课件,想加两行批注;拿到一张发票,想提取里面的文字——然后你发现…

2026/9/24 7:05:49 阅读更多 →
SD-WAN选型全维度解析:从全球覆盖到交付保障

SD-WAN选型全维度解析:从全球覆盖到交付保障

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 7:04:49 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/23 4:55:02 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/23 4:49:06 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/23 9:53:41 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/23 9:53:40 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/23 9:53:40 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/23 9:53:40 阅读更多 →