ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

编程函数基础:从定义、调用到闭包与回调

编程函数基础:从定义、调用到闭包与回调 你有没有看过这样的报错pnpm : 无法将“pnpm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查...。还没缓过来下一行又来了make、claude、pip一个接一个全都“不认识了”。明明刚装好为什么系统就不认它们了问身边人十有八九得到的答复是“环境变量的问题”。话是没错但我想借这个场景打开今天的话题——函数。当你认真学编程学到第七章第一次系统接触函数时会发现之前那些看不清的报错、写不明白的重复代码、理解不了的回调机制突然都能用函数的视角看懂了。这篇博文就是给你准备的函数基础课从定义、调用讲到参数、返回值、作用域、闭包再往回调函数、内置函数和函数组件延伸最后用一份报错排查手册收尾。语言以Python和JavaScript为主穿插C、SQL、Excel等常见场景。不管你是在校学生、转行初学者还是写了段时间代码想把底子夯实的开发者我都建议静下心把全文读完很多让你抓狂的报错根子都在函数这一章。1. 报错里的“函数”和编程里的函数不是一回事但有关1.1 “无法将pnpm识别为cmdlet、函数、脚本文件”到底是什么你敲下pnpm回车Windows PowerShell 拿到这串字符会先在当前文件夹里找同名文件找不到就去 PATH 环境变量记录的每个目录里挨个找。所有目录都找不到就抛出那句经典报错。这里的 cmdlet、函数、脚本文件本质是 PowerShell 语境里“用名字触发行为”的几种不同形式。报错的意思是名字没登记在册命令没被找到。这个“输入名字—执行逻辑—得到结果”的结构和编程里的函数调用逻辑几乎同构。你还没学会函数却已经在操作系统的命令层体验了一遍函数调用机制。区别只在于命令背后是一个完整的可执行程序而函数背后是一段具体的代码块。遇到这类报错常规解决办法就三步第一确认安装时有没有勾选“Add to PATH”之类的选项很多安装器默认不勾第二改完环境变量后一定要重新打开终端或IDE不重启终端PowerShell 不会重新读取 Path这是最容易被忽略的一步第三手动找到程序安装目录把它加到用户或系统 Path 里然后在终端执行where.exe pnpm验证。记住这是“命令没注册”不是代码逻辑问题别去项目代码里找毛病。1.2 函数的本质给一段逻辑起个名字随叫随到理解函数最好的方式是把它想成一台洗衣机。你不需要每次都手动“注水—加热—转滚筒—排水”只要按下“洗”这个按钮机器就会按照内置流程把整套逻辑执行一遍。编程里的函数就是这个按钮按钮的名字就是函数名它把一堆操作封装成了一段可重复调用的逻辑。很多入门教程喜欢把示例函数命名为fun于是总有人在搜“fun函数的作用”其实他们搜的是“函数的作用”。函数最核心的作用就是两个接受输入参数对外提供结果返回值中间的细节被完全黑盒化。这里要强调一点编程里的函数和数学里的y f(x)并不完全是一回事。数学函数强调“输入到输出的映射”纯到没有任何副作用而编程函数更强调“一系列步骤的包装”它不一定有输入也不一定有输出可以有副作用比如打印、写文件、改全局状态。所以别把函数理解成数学公式它更像一台按流程运转的机器。1.3 为什么跟函数死磕值得跟函数死磕至少有三个实实在在的收益。第一消灭重复代码。同样十行逻辑如果要在三个地方复制粘贴三遍改一个需求就得找三处改漏掉一处就是事故。抽成函数之后一个地方改完所有调用点全部生效。这一点在真实项目里体现得特别明显靠复制粘贴堆起来的代码后面维护的人会有多痛苦谁经历过谁懂。第二拆解复杂问题。人的工作记忆有限一个几千行的大函数很难一次想明白。把任务拆成多个小函数每个函数只负责一件事先分别实现再组合调用复杂问题就变成了“组装积木”。第三建立抽象能力。函数名本身就是对一段逻辑的浓缩注释。调用方不需要知道函数内部怎么实现只需要知道“名字是什么、传什么参数、得到什么返回”。这种“接口与实现分离”的思维是后面学习面向对象、设计模式、模块化的地基越往后学越发现编程的核心就是抽象。2. 定义与调用多语言视角下的第一课2.1 声明、定义、调用别把这三件事混在一起很多初学者看到报错里的“函数声明”就发懵其实这三回事必须分清声明是告诉编译器或解释器“存在这样一个函数它有这个名字、这些参数、这个返回类型”定义是写出函数的具体实现代码块调用是真正执行这段代码。在C/C里这个区别尤其重要。编译器从上往下读源码如果你把函数的定义写在了main后面又在main里直接调用它编译器根本不知道这个函数存在就会报“隐式声明”或“未定义”错误。解决办法就是提前声明一个函数原型或者干脆把定义挪到调用之前。在Python和JavaScript里没有显式的声明步骤但JS里依然存在“函数声明”和“函数表达式”的区别。函数声明会提升hoisting函数表达式不会。function greet() {}这是函数声明const greet function() {}这是函数表达式。如果你在表达式初始化之前调用它就会踩到暂时性死区TDZ的坑报错和“未定义”长得一模一样。2.2 Python与JavaScript函数定义对照我直接给最常用的写法对照背下来就能起步。def greet(name): return fHello, {name} print(greet(张三))function greet(name) { return Hello, ${name}; } const greetArrow (name) Hello, ${name};Python 用def关键字冒号加缩进表示函数体缩进本身就是语法的一部分少一个空格都会报IndentationError。返回用return不写就默认返回None。JavaScript 普通函数用function关键字花括号包裹函数体。这里得专门说说箭头函数写法因为它是前端高频搜索的关键词。箭头函数不是为了好看它的语义有三处关键差异第一箭头函数不绑定自己的this它会继承外层作用域的this普通函数调用时this由调用方式决定第二箭头函数不能用new构造没有prototype属性第三箭头函数没有自己的arguments对象要用就写剩余参数(...args)。所以“箭头函数只是简写”这个认知是错的事件回调、定时器回调里用箭头函数为什么更省心正是因为它的this稳定。2.3 “JS函数是对象吗”——问的不是函数是语言设计这个问题是热词榜上的常客。直接说答案是。typeof一个函数返回的是function但这并不妨碍函数像对象那样拥有属性。function howdy() {} howdy.meta 附加在函数上的属性; console.log(howdy.meta); // 输出附加在函数上的属性在多数编程语言里函数不能挂属性JS 可以。原因在于函数是Function构造函数创建出来的实例它本质上是一种对象。函数可以赋值给变量可以作为参数传给另一个函数也可以作为另一个函数的返回值。这种能力让函数成了“一等公民”后面讲回调函数、闭包全部建立在这个基础上。我觉得“函数是对象吗”这道题的真实意义是在问“函数能不能像数据一样被传递和组合”。这个答案在不同语言里完全不同C 语言里函数指针可以传递但没法天然创建闭包Python 里函数也是对象所以能装饰器满天飞Java 里函数只能以方法的形式挂在类上直到引入函数式接口才稍微灵活。所以这不是一道是非题而是你在考察一门语言对“行为”的抽象方式。3. 参数与返回值看着简单细节一堆3.1 形参、实参、默认参数一次说清楚形参是函数定义时写的占位变量名字实参是调用时真正传进去的值。def greet(name, times1): return f你好{name} * times这里name和times是形参times1是默认参数。调用greet(李四)时不用传times调用greet(李四, 3)时传入的 3 是实参。但 Python 默认参数有一个著名的大坑默认参数如果是可变对象列表、字典它会被多个调用共享。def add_item(item, lst[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2]第二调用竟然带上了一次的结果正确的写法是用None做默认值在函数内部再创建新列表def add_item(item, lstNone): if lst is None: lst [] lst.append(item) return lstJavaScript 的默认参数是 ES6 引入的写法function f(a, b 2) {}。需要注意JS 的默认参数在每次调用时都会重新求值这点反而比 Python 更符合直觉。3.2 值传递、引用传递以及Python的“假装值传递”理论基础很简单值传递传的是副本函数内部怎么改都不影响外面引用传递传的是地址函数内部原地修改会直接影响外部对象。C 语言默认是值传递想影响外部变量就得传指针。Python 的机制严格来说是“传对象引用”但结果因对象类型而异不可变对象int、str、tuple一旦重新赋值等于换了一个引用外部看不到变化可变对象list、dict、set原地修改时外部能看到。def change(x): x.append(1) data [1] change(data) print(data) # [1, 1]如果你不想让函数修改原对象就传入副本data[:]。很多新人在 Python 里困惑的“为什么函数里改不动整数、改得动列表”根子就在这里。JavaScript 的逻辑类似number、string、boolean 是值语义object、array、function 是引用语义。赋值对象时给的是引用两个变量指向同一个堆对象改一个就全变了。这是无数前端 bug 的来源建议早点建立“引用”的肌肉记忆。3.3 C返回字符串为什么会翻车热词榜上“c 函数返回字符串”特别典型因为新手真的会被整懵。常见错误写法是这样的const char* getText() { char buf[64]; // 往 buf 里写内容 return buf; }buf是栈上的局部数组函数一返回这 64 个字节就归还给运行时了。调用方拿到指针指向的地址已经失效读出来的内容可能是垃圾值随时可能崩溃。这不是“返回值”本身的问题而是返回了局部对象的地址。正确做法之一返回std::string。现代 C 有移动语义返回std::string时不会触发深拷贝内部会搬运数据安全又高效或者把输出缓冲区作为参数传入由调用方提供存储空间。记住一句话返回对象本身通常安全返回指向局部区域的指针或引用才会悬垂。4. 嵌套、作用域与闭包开始理解函数的内存图景4.1 Python函数嵌套定义和嵌套调用“嵌套定义”和“嵌套调用”是两个容易被混在一起的概念。嵌套调用是函数 A 内部去调用另一个函数 B日常编码几乎天天都有比如sorted(data, keylen)里len就被调用了很多次。嵌套定义是指在函数 A 内部用def再定义函数 BB 只在 A 内部可见外面访问不到。def outer(x): def inner(y): return x y return inner嵌套定义有它自己的用途一是封装内部逻辑不让辅助函数污染全局命名空间二是实现闭包为装饰器、偏函数这些高级特性打底。你搜“python函数嵌套定义和嵌套调用”大概率是在学习装饰器之前先遇到了inner函数。这个示例里inner引用了外层x当你把它作为返回值交出去时有趣的事情就发生了。4.2 作用域变量到底在哪一层Python 查找变量的顺序是 LEGBLocal局部→ Enclosing外层嵌套函数→ Global全局→ Built-in内置命名空间。x 10 def f(): x 20 print(x) # 20局部变量遮蔽全局变量局部x会遮蔽全局x。如果你想在函数里改全局变量用global声明想在外层嵌套函数里改变量用nonlocal声明。这俩关键字就是告诉解释器“这个变量不在我这层去上面那层找”。JavaScript 这边var有函数作用域和变量提升let/const是块级作用域。闭包之前必须先理解作用域边界。很多经典 bug 就是for循环里用var造成所有回调共享同一个变量换成let就各存各的了。4.3 闭包内层函数拿住了外层变量继续用嵌套定义的例子这次做一个真正的闭包def counter(): count 0 def inc(): nonlocal count count 1 return count return inc c counter() print(c()) # 1 print(c()) # 2按常规理解counter()已经执行完了count应该被回收。但inc在定义时“闭合”了count所以这个变量被一直保留在堆上只有通过inc才能访问到它。这就是闭包的本质内层函数引用了外层函数的变量导致外层变量逃脱了生命周期限制。闭包是回调函数、事件处理、装饰器、React Hooks 等一系列高级写法的底层桥梁。看不明白闭包后面看 Promise、看useCallback都会一头雾水。也要注意闭包不是没有代价的如果一个很大的对象被闭包长期引用GC 回收不了就形成了内存泄漏。写长生命周期应用时用完之后要把闭包引用置空。5. 函数的更多形态从回调到函数组件5.1 回调函数把“下一步动作”当作参数传出去回调函数就是把一个函数作为参数传给另一个函数由对方决定在合适时机调用。这在 JS 里随处可见setTimeout(() console.log(2秒后触发), 2000);定时器就是一个通用工具它不管你想做什么只负责到点执行你给的回调。事件监听、Promise 的.then()、Node.js 的 I/O 操作全都在用回调。Python 里也有大量回调场景只是名字不那么直接。例如sorted(data, keylen)的key就是一个回调函数它告诉排序逻辑“按长度排”。回调的威力在于把“下一步动作”解耦出去通用函数只负责流程具体行为由调用方注入。这个过程看起来不起眼实际就是函数式编程的核心操作。5.2 内置函数与标准库不用白不用的“官方封装”内置函数是官方帮你预设好的高频封装先查文档再动手别自造轮子。Python 里最常见的len()、range()、enumerate()、zip()、map()、filter()。这几个函数吃透代码量能减一半。函数这个概念绝不只在编程语言里存在。数据库的 MySQL 函数大全里NOW()返回当前时间LEFT(str, n)截取左侧 n 个字符DATEDIFF(date1, date2)求日期差这些都是把高频操作封装成可调用名字的典型。Excel 里更常见。很多人搜“left函数和vlookup函数一起使用注意事项”说明实操中遇到了坑。比如LEFT(VLOOKUP(A1, 区域, 2, FALSE), 3)想从匹配结果里提取左侧三个字符。这里的坑在于VLOOKUP 返回的值如果是数字LEFT 会把数字当成文本处理结果变成文本型数字后续求和、比较全都对不上。解决办法是再套一层VALUE()转换或者在源表里保持文本格式。这类嵌套函数的使用教训和编程里“函数返回类型影响后续操作”是同一个道理。5.3 领域函数与函数组件当你开始用函数描述世界函数还会以各种形态出现在专业领域里理解这一点你对“函数”这个词的外延会完全改观。机器学习里softmax函数把一组任意实数转换成和为 1 的概率分布yolo损失函数定义了目标检测模型的优化目标模型练得好不好很大程度取决于损失函数设定得对不对。这里的函数不是在操作数据而是在定义学习的方向。word2vec的数学推导里也有大量数学函数它们负责把词变成向量。前端领域搜“函数组件”的人多半在学 React。React 函数组件本质上就是纯函数props 进、JSX 出。function Card({ title }) { return div{title}/div; }用状态管理就用 Hooks配合回调、闭包、复用函数式编程思想贯穿始终。嵌入式开发也有专属函数形态比如ch32v在gcc编译器下定义中断函数要用__attribute__((interrupt))这种特殊语法告诉编译器这是一段中断入口函数不能按普通函数优化。甚至信号处理里的互相关函数、流体力学里的壁面函数本质上也都是函数。函数不只是语法更是一种组织计算的方式。6. 新手高频报错排查手册6.1 “cmdlet、函数、脚本文件”报错90%是PATH问题这类报错影响力极大pnpm、make、claude、pip全都能触发。原因前面讲过PowerShell 找不到这个命令在 PATH 里的位置。常见的四个场景和排查方向整理成了一张速查表报错里的命令常见原因解决方向pnpm 无法识别安装时没加 PATH或安装后没重启终端重新打开终端手动把 node_modules 下的 pnpm 全局路径加入 Pathmake 无法识别装了 MinGW 但没有把 bin 目录加入 PATH检查 MinGW/bin 路径加入 Path重启终端claude 无法识别CLI 工具未正常安装或全局目录未加入 PATH按官方文档确认安装目录重装或手动加 Pathpip 无法识别Python 安装时未勾选 Add Python to PATH勾选修复安装或手动加入 Scripts 目录排查套路给你先where.exe 命令名看能不能搜到搜不到就找安装目录目录对了再检查 Path 是否包含它改完环境变量一定要开一个全新的终端窗口再试旧窗口不会重新加载。这是个机械流程但能解决九成 CLI 报错。6.2 函数没定义、重复定义、链接找不到这类报错直接发生在编译或运行阶段更贴近函数语法本身。“函数未定义”常见原因三种调用出现在定义之前C 语言里尤其频繁JS 里用函数表达式在变量初始化前调用会触发暂时性死区Python 里模块级代码在def执行之前就调用了函数。排查顺序先看函数的定义位置是否在调用之前再看是否拼写不一致最后看是不是作用域遮蔽。这里教你一个检查技巧——把报错行代码局部变量名打印出来在 Python 里用locals()在 JS 里直接打断点看typeof能精确定位未定义变量。“重复定义”常见于 C/C 项目头文件被 include 了两次同一个函数被编译进两个编译单元。解决办法是给头文件加#pragma once或者#ifndef ... #define ...。还有一种情况是和自己写的重名函数冲突比如在 LVGL 项目里自定义打印函数与底层库的打印函数重名报一堆重复定义。解决方案就是改自己的函数名或者使用库提供的重定义开关别跟工具链对着干。“链接找不到”编译过了但链接器报未定义的外部符号。这不是代码没法写而是没有把对应库链接进来。比如用了 Linux 的select函数忘了引入头文件链接对应库或者调了 OpenCV 的solvePnP但项目里没链 opencv 库。检查顺序头文件有没有包含 → 库路径有没有配置 → CMake 的target_link_libraries或 makefile 的LIBS有没有加。6.3 从“函数”角度重新认识其他怪异报错有些搜索热词看起来是函数其实本身不是函数却经常把新人绕晕。“vector函数”是重灾区。C 里vector是标准库容器不是函数。真正被调用的其实是容器的成员方法比如push_back()、size()。你看到vectorint v;就以为它在“调用函数”说明还没有建立“类型”和“函数”的边界。可以说搞懂这两个概念的区别C 才算真正入了门。“exit函数”也是常见疑问。exit()确实是个函数但它有个特殊性质调用后进程立即终止不返回任何值。所以你在main里写了return exit(0)这种代码本身就是不合理的exit之后根本轮不到 return 执行。C/C 里要包含cstdlib头文件才能用它。Windows 的InvalidateRect、Linux 的select这些都是系统 API 级的函数。它们和普通标准库函数一样的套路有名字、有参数、有返回值或副作用但你必须引入正确的头文件和库。看到这类报错先记住一句话函数在原代码里的定义代码块要么存在于你的文件里要么存在于某个库文件里要么就存在于系统 API 里。你只要找到它被声明和实现的位置问题就破了一半。我在实际带新人的过程中发现判断一个人有没有真正理解函数只需要看两个细节遇到报错时是先怀疑环境、还是先检查自己对函数的假设写重复代码时是顺手复制粘贴、还是停下来抽一个函数。前者决定排查效率后者决定工程素养。函数基础这一章真正要建立的不是你记住多少语法而是你在面对问题时愿意把逻辑拆成可命名、可复用、可组合的小单元的冲动。最后再分享一个小技巧每当你写下一段超过十行的代码先停下来问自己这段逻辑未来会不会用第二次、会不会需要单独被测试。答案只要是“会”就把它抽成函数。用这种习惯写上半年你会回来感谢这一章的。
返回列表