写代码这些年我几乎每天都要和“函数”打交道。从刚开始学C语言时照着课本抄main函数到后来写JavaScript时研究回调函数和闭包再到看论文时面对损失函数、核函数这些概念——函数这个词出现在编程的每个角落又往往被一笔带过。很多人以为函数就是“把代码包起来重复调用”但真正把函数实现弄清楚其实牵扯到参数传递、作用域、返回值、异步回调、模块化这一整条链路。这篇文章不打算只讲某个语言而是把“函数的实现”这件事拆开从最基础的声明和调用到不同语言里的坑再到我踩过的那些奇葩报错。适合刚入门的同学也适合写了好几年代码但有些细节一直没搞明白的同行。1. 先搞明白函数到底是在“实现”什么1.1 函数是一台“按需运转的小机器”函数最朴素的定义就是把一段逻辑封装起来给它起个名字然后在需要的时候反复调用。你可以把它想象成一台小机器从入口塞进去原料机器内部完成处理再从出口吐出来结果。原料就是参数结果就是返回值机器本身是函数体而“按一下开关”这个动作就是函数调用。我经常跟身边的新人讲判断一段代码要不要提成函数就看两件事一是这段逻辑有没有被重复使用二是它能不能独立描述一个意图。比如你要计算一批商品的总价先算单价乘数量再考虑折扣最后加上运费。这一长串计算如果散落在业务代码里过两周你自己都看不懂但如果封装成一个calcOrderTotal(order)别人一眼就知道这里在做什么。函数真正的价值不只是“少写几行代码”而是把复杂度拆成一个个有名字、有边界、可单独测试的小单元。在C语言里函数从int main()开始在JavaScript里函数可以是声明、表达式也可以是箭头函数在Python里def 一个函数只要一行开头。但不管语法长什么样它们的本质都一样输入 → 处理 → 输出。把这一步想透了后面所有的进阶概念比如回调、闭包、生成器、损失函数其实都是在“输入”和“输出”这两个口子上做文章。1.2 声明、定义、调用三个词别搞混很多初学者分不清函数“声明”和“定义”的区别。声明是告诉编译器或者解释器存在一个函数它叫什么名字、接收什么参数、返回什么类型定义则是把函数体写出来说明它具体做什么。在C语言里你可以在文件顶部写int add(int a, int b);这叫声明然后在后面某处写int add(int a, int b) { return a b; }这叫定义。声明可以重复定义只能有一次。JavaScript里有一个很经典的坑叫“函数声明提升”。同样写一个函数用function foo() {}这种方式声明它会被提升到作用域顶部所以你在定义之前调用也没问题但如果你用const foo function() {}或者箭头函数它就只是个变量赋值在赋值之前调用会报Cannot access foo before initialization。我实际开发中更喜欢用函数表达式因为代码是从上往下读的先定义后调用更符合直觉也避免依赖“提升”这种隐式行为。还有一个容易混淆的概念函数声明和函数调用。声明是“有这个函数”调用是“执行这个函数”。新手最容易犯的错误是在绑定事件的时候写成了btn.onclick handleClick()括号一加函数立刻执行了返回的结果被赋给了 onclick而不是把函数本身交给按钮。等点击按钮时执行的是一个 undefined毫无反应。判断一个位置该不该加括号就看你此刻是需要“执行它”还是“引用它”。1.3 return到底返回了什么bool、int与无返回值return 是函数和外部世界交流的通道。C语言的函数要提前声明返回类型int函数就得返回整数bool函数就得返回真或假void函数表示“我不需要返回东西”。但实际上即便你写一个void函数函数执行完也会有一个隐式的“结束”状态只是在代码层面不向外传递数据而已。bool 类型函数的返回值我见过最多的用途就是判断和校验。比如isValidEmail(str)、hasPermission(user)这类函数返回的是“条件是否成立”调用方直接拿来写if就好。Python 里没有 bool 关键字但一样有True和False而且 Python 的函数无返回值时默认返回None在条件判断里None会被当成假值处理。JavaScript 则更随意函数不写 return 就会返回undefined这也是undefined这个坑的来源之一。再说一个我想单独提醒的细节C 里函数返回引用和返回值的区别很容易翻车。如果你返回的是函数内部局部变量的引用函数结束栈帧销毁这个引用就悬空了访问它属于未定义行为。我见过有人写int getValue() { int x 42; return x; }编译时不报错运行时偶尔能拿到 42偶尔拿到乱码排查半天最后发现是局部变量的生命周期问题。所以返回值这件事“返回什么”和“返回的东西还能不能用”是两码事后者往往更致命。2. 参数传递与作用域最容易翻车的一环2.1 值传递、引用传递与对象引用函数参数是怎么进到函数里的这个问题在不同语言里有完全不同的答案。C语言默认是值传递你把变量 a 传进去函数拿到的是 a 的一份拷贝在函数里改参数外面的 a 一点不受影响。想要让函数能修改外部变量C语言的办法是传指针通过指针间接操作原数据。C 在此基础上加了引用语法上比指针友好一些但本质上还是“同一个对象换了个名字”。Python 和 JavaScript 则是另一套逻辑基本类型传值对象类型传引用更准确说是传对象的引用。这就产生了一个非常经典的现象你在函数里给参数重新赋值外部变量不变但你在函数里修改参数对象的某个属性外部对象的属性会跟着变。比如 Python 里这样写def add_item(item, container): container.append(item) my_list [] add_item(hello, my_list) print(my_list) # [hello]这里my_list本身没被重新赋值但它指向的那个列表对象被append改了所以外部能看到变化。很多从C语言转过来的人在这里容易晕觉得“传引用就是可以改外部变量”但实际规则比这精细你能修改对象的内容但改变不了“外部变量指向哪个对象”这件事。我一直推荐在团队规范里明确约定函数要么修改传入的对象要么返回新值不要两头都做。否则一个函数既能改参数又有返回值调用方很难判断当前数据到底被处理成什么样了。我自己现在写代码的原则很简单——基本类型尽量返回新值大对象尽量传入后原地修改但一次只选一种方式并且写进注释里。2.2 作用域、闭包与延迟函数作用域决定了函数能看到哪些变量。全局变量所有人都能看局部变量只有函数内部能看这个天经地义。真正微妙的是嵌套函数形成的作用域链内部函数可以访问外部函数的变量而外部函数访问不到内部函数的局部变量。这种嵌套结构一旦再结合“内部函数被当作值返回或传出”就产生了闭包。闭包可以理解成“函数背着一个小背包”背包里装着它出生时所在作用域里的变量。一个最经典的例子是计数器function createCounter() { let count 0; return function() { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2外层函数执行完了count按理说应该被销毁但因为返回的内部函数还握着对它的引用count就被保留了下来。这个特性在做数据私有化、做缓存、做防抖节流的时候特别有用。但也正因为闭包持有外部变量它容易造成内存泄漏——如果你把一个闭包挂到了长期存活的对象上它背包里的所有变量都不会被回收。闭包和延迟函数放在一起看更有意思。JavaScript 的setTimeout就是延迟函数的常用形态但延迟函数里用到的变量是在函数真正执行时才去取值的而不是注册那一刻。这就解释了为什么循环里用var定义变量并注册定时器最后打印出来的全是循环结束后的值改成let或者用闭包把每次的 i 独立包一层才能打印出正确的序号。所谓“延迟函数怎么写”表面上是查个语法实际上拼的是对作用域和闭包的理解。CAPL 这种车载总线脚本语言里延迟函数也依赖 Timer 回调同样逃不开“回调执行时变量是否还在、是否被污染”这两个问题。2.3 默认参数的可变默认值陷阱默认参数是降低函数调用成本的好东西也是藏着隐蔽 bug 的地方。Python 有一个非常出名的坑默认参数如果是可变对象这个对象只在函数定义时创建一次以后每次调用如果没传该参数用的都是同一个对象。我在实际代码里见过大量类似写法def add_item(item, container[]): container.append(item) return container第一次调用add_item(a)返回[a]第二次调用add_item(b)返回[a, b]——你以为每次都新建了一个空列表实际上它们共享了同一个列表。排查这类问题不用多少高深技巧只要记住一个原则默认参数如果不需要被修改就老老实实用不可变类型如果确实需要默认容器在函数内部写if container is None: container []每次调用都新建一个。JavaScript 的默认参数没有这个坑因为 JS 默认参数值是在每次调用时重新求值的不会跨调用共享。Excel 函数里的可选参数理解起来就更直白比如VLOOKUP的第四个参数不填默认是近似匹配填0才是精确匹配。这种“默认行为”和编程里的默认参数一样最怕的是调用者不知道默认值是什么。所以设计函数时默认值应该选“最安全、最不容易出意外”的那个选项而不是“最方便写”的那个。3. 从回调到生成器函数的高级形态3.1 回调函数与浏览器插件调用页面JS函数回调函数简单说就是把一个函数作为参数传给另一个函数由后者在合适的时机调用。这个模式在 JavaScript 里无处不在事件监听器、定时器、网络请求回调统统都是回调。为什么要这么设计因为很多操作是异步的你不知道什么时候完成与其阻塞等待不如告诉系统“做完了之后请调用我这个函数”。生活里类似“到了饭点叫我一声”的嘱咐就是把一个动作委托给了别人去触发。“浏览器插件如何调用页面js函数”这个问题本质上也是在玩回调与函数注入。我做过几个浏览器插件最常用的做法有三种。第一种是在 content script 里往页面注入一段 script 标签把函数挂到window上页面代码通过window.myFunction调用第二种是通过CustomEvent派发自定义事件页面里预先监听这个事件并执行对应逻辑第三种是用chrome.scripting.executeScript直接执行一段函数字符串通过args传参与取回返回值。前两种适合页面和插件长期互动第三种适合一次性执行。这里面有个容易踩坑的点content script 和页面脚本不在同一个 JavaScript 上下文里你直接在 content script 里定义函数页面是访问不到的。我习惯用一个桥接函数让页面调用桥接函数时先dispatchEvent发一个带参数的事件content script 监听这个事件再把数据转成插件端能用的格式。这套方案不需要页面事先知道插件存在只要页面里有对应的监听函数就能通信。每次我调试这种跨上下文调用都会先写一个最小示例确认事件能通再往里面塞业务逻辑——不然混在一起报错都分不清是插件的问题还是页面脚本的问题。3.2 箭头函数写法与 this 绑定箭头函数是 ES6 带来的语法糖写法很简洁() {}单个参数可以省略括号单行表达式可以省略大括号和 return。比如const double x x * 2一行搞定。但箭头函数不只是简写它最核心的特点是不绑定自己的 this。普通函数的 this 取决于调用方式箭头函数的 this 取决于定义位置的词法作用域。这个特性在回调函数里特别实用。以前写事件处理或定时器经常要var self this或者const that this把外层的 this 存下来才能在回调里用。有了箭头函数直接写setTimeout(() this.update(), 1000)this 指向的还是定义箭头函数时的对象。代价是箭头函数不能用作构造函数也没有自己的arguments对象。我见过有同事用箭头函数定义类的方法结果方法里需要动态 this 指向时完全失效就是没分清“箭头函数该用在固定 this 的场景”这件事。还有一个经常被忽视的细节箭头函数和普通函数混用会导致 this 指向混乱。比如在 Vue 或 React 的组件里如果注册事件监听时用了普通函数而不是箭头函数this 会被绑定到触发事件的 DOM 元素上调用组件方法就报错了。排查这类问题第一步就是看所有回调是不是统一用了箭头函数第二步看有没有在非必要的场合滥用箭头函数比如在需要动态 this 的构造函数原型方法里。统一风格比争论“哪种写法更优雅”重要得多。3.3 Generator迭代器封装函数迭代器和生成器是函数进阶形态里比较烧脑的一块但实际用过之后你会发现它非常顺手。迭代器是一种对象它实现了next()方法每次调用next()返回一个{ value, done }结构。数组、Set、Map 都有默认迭代器所以你可以用for...of遍历它们。那 Generator 是什么它是一个用function*声明的函数执行后不马上跑完而是返回一个迭代器对象每次调用next()就执行到下一个yield处停下。我经常用 Generator 封装自定义的遍历逻辑。比如要生成一个从 start 到 end、步长为 step 的范围序列用普通函数写会一次性生成整个数组数据量大时白白占内存用 Generator 写则是惰性的每次next()才算下一个值function* range(start, end, step 1) { for (let i start; i end; i step) { yield i; } } for (const num of range(1, 10, 2)) { console.log(num); // 1 3 5 7 9 }这个封装的好处是调用方能按需取值配合Array.from可以把生成器转成数组也可以只取前几个值就中断后面的根本不会被计算。Python 里的生成器表达式和yield也是同一个思路。理解 Generator 的关键是不要把它想成一个“一次跑完的函数”而要想成一个“可以被暂停和恢复的函数”。它和回调函数正好互补回调是别人调你生成器是你掌握着主动暂停和继续的权力。4. 跨界函数从损失函数到Excel公式4.1 高阶函数与常用内置函数函数能作为参数传给另一个函数也能作为返回值被返回这种“把函数当值来用”的风格就是函数式编程的底色。高阶函数是其中最典型的工具JavaScript 的map、filter、reducePython 里同名的内置函数还有 SQL 里的SELECT子句本质上都在做同一件事把你传入的处理规则批量应用到数据上从而避免手写一堆循环和临时变量。除了这些批量处理函数编程语言内置函数里藏着大量“小而美”的工具。比如 Python 的abs别看它简单但在计算误差、距离、差值时绕不开平方根函数sqrtC 语言里有math.hJavaScript 在Math.sqrtExcel 直接是SQRTPython 要import math再调用split几乎是所有语言处理字符串的标配把一个字符串按分隔符切成数组一行代码省掉很多手写解析逻辑。这些函数的共同点是参数和返回值都很明确用起来没有太多心智负担但正因为简单很多人反而忽略了它们在不同语言里的差异参数顺序有没有错、是修改原值还是返回新值、边界情况返回什么。我自己写代码时会刻意把“数据变化过程”用高阶函数串起来。比如有一段字符串数组要过滤掉空字符串、转为大写、再拼接成一个句子const arr [hello, , world]; const result arr .filter(item item ! ) .map(item item.toUpperCase()) .join( );三个步骤各自独立每一步都是一个小函数组合在一起就成了流水线。再往上抽象一点pipe函数可以把多个单参函数从左到右组合成一个新函数让数据像水一样顺着管道流动。这种写法的最大优势是每一步都可以单独测试出了错也知道卡在哪一环。当然也别为了函数式而函数式循环在某些场景下可读性更好。4.2 那些“不走寻常路”的函数当我第一次接触机器学习里的“损失函数”时第一反应是这和我们写代码时的函数是一个东西吗后来想明白了是也不是。说是因为它同样是一个输入到输出的映射规则说不是因为它的“实现”往往不是一段代码控制流而是一个数学表达式用来衡量“模型预测结果离真实结果有多远”。GAN 的损失函数里生成器和判别器各自有损失两股力量相互对抗最终训练出一个能生成以假乱真数据的模型。YOLO 这类目标检测模型的损失函数更复杂常常包含三块定位损失衡量预测框和真实框的差距置信度损失衡量“这个框里有没有目标”的判断是否准确分类损失衡量目标类别分得对不对。这三块会按权重加在一起组成最终训练时反传的总损失。这和我们写一个函数返回多个误差值的总和没什么本质区别只是它运行在张量计算框架里并且参与自动求导而已。同样的思路也适用于核函数、能量函数、壁面函数、atan2 函数。核函数在支持向量机里做的就是用一种巧妙的计算方式把低维空间不好分开的数据投影到高维空间壁面函数是计算流体力学里处理近壁湍流的一个经验函数关系atan2 在图形学和嵌入式里用来求角度但很多MCU没有现成的数学库只能自己做定点化和多项式近似。包括 ADAMS 这类动力学仿真软件里的“力的函数”本质上都是产品内置的一种表达式接口你按照它规定的语法写出一条曲线或公式软件在每一步仿真时去求值。这些场景给我最大的启发是“函数”这个词跨领域使用时核心并没有变——它就是一种规则。理解一个陌生领域里的函数第一步永远是搞清楚它的输入是什么、输出是什么、内部是理论推导还是统计拟合。搞清楚这三点你再去看 GAN 损失函数、核函数、壁面函数的文档就不会觉得是天书了。4.3 Excel函数、自定义函数与未来函数检测Excel 大概是普通用户接触函数最多的场景。从最简单的SUM、IF到进阶的VLOOKUP、CHOOSE再到数组公式Excel 函数其实也是一门小型的函数式编程。网上那些“Excel函数公式大全”收藏夹里会存几百个函数但我实际工作中常用的不超过二十个关键是组合使用。比如 “怎样用函数比对两列打乱的数据并找出不重复的数据”我最常用的是COUNTIF配合IF先统计某列值在另一列出现的次数次数为 0 的就是不重复项也可以用VLOOKUP返回#N/A来判断匹配不到。Excel 里还支持自定义函数通常在 VBA 编辑器里写比如定义一个函数传入两个日期返回它们之间的工作日天数。自定义函数的好处是可以复用复杂的计算逻辑但要注意 Excel 对自定义函数的调用时机有自己的调度如果函数运行太久表格会卡得让人怀疑人生。另一个容易被忽略的问题Excel 函数是用在单元格里的公式函数里引用其他单元格区域时区域范围的写法决定了公式是“动态”还是“静态”。说到这里就不得不说“未来函数”了。在数据处理和回测类公式里“包含未来函数”是一个严重的数据完整性问题。所谓未来函数就是公式在计算某个时间点的结果时引用了这个时间点之后才能产生的数据。比如用整列的最大值去填充前半段记录或者引用了一个包含后续更新的辅助区域这会让公式在历史时点上“提前获知未来信息”得出的结果看起来很准实际上是假的。检测公式是否包含未来函数我一般从两个角度入手一个是审查公式引用区域是否跨越了当前行另一个是看引用的单元格范围是否固定到了包含后续数据的整列。更稳健的做法是在公式里用OFFSET或INDEX限制引用范围让公式只能看到当前行和它之前的数据这样无论表格怎么更新结论都不会掺入未来信息。5. 不同语言里的函数实现与常见报错5.1 C语言字符串函数与C虚函数C语言本身没有字符串类型字符串就是字符数组所以操作字符串要借助标准库函数strlen算长度、strcpy复制、strcat拼接、strcmp比较。这些函数实现起来都不复杂但处处是坑。比如用strcpy之前不检查目标缓冲区够不够大就可能造成缓冲区溢出strlen不包含末尾的\0所以给字符串分配空间时要记得多留一个字节。后来很多代码规范都建议用安全版本比如strncpy、snprintf把最大长度显式传进去。C 在函数概念上往前走了一大步类成员函数、虚函数、纯虚函数。虚函数的关键字是virtual它让派生类可以重写基类的方法并且在通过基类指针或引用调用时动态地找到实际对象的实现。举例来说基类Animal有个虚函数speak()派生类Dog重写了它那么用Animal*指向一个Dog对象时调用speak()实际执行的是 Dog 版本。这背后靠的是虚函数表vtable对象里藏着一个指向函数表的指针调用的具体是哪个函数运行时才确定。关于虚函数有个老生常谈但值得重复的建议析构函数尽量声明为虚函数。如果基类析构函数不是虚函数那么通过基类指针删除派生类对象时只会调用基类的析构函数派生类里申请的资源可能不会被释放造成内存泄漏。这属于那种“平时不炸、一炸就要加班排查”的坑。C 里还有一个有意思的现象类新建对象后可以自动调用自定义函数这就是构造函数派生类构造时会先调用基类构造函数再调用自己的构造函数顺序搞反了初始化逻辑很容易出错。5.2 Python的class函数与构造初始化Python 的类方法有三类实例方法、类方法、静态方法。普通实例方法的第一个参数是self代表这个具体实例通过obj.method()调用Python 会自动把对象传进去类方法用classmethod装饰第一个参数是cls代表类本身可以通过类名直接调也常用于操作类级别的属性静态方法用staticmethod装饰不接收隐式的 self 或 cls本质上只是放在类命名空间里的普通函数。class User: def __init__(self, name): self.name name classmethod def from_json(cls, data): return cls(data[name]) staticmethod def is_valid_name(name): return len(name) 0很多新手搞混这三者是因为不知道self和cls都是约定俗成的名字虽然换成别的也能跑但没人那么写。真正要区分的是语义方法需要访问实例属性用实例方法方法只依赖类或者要返回类的新实例用类方法方法既不碰实例也不碰类只做独立逻辑用静态方法。至于构造初始化Python 的__init__是创建对象后最先被自动调用的方法C# 里也有对应机制类新建对象后自动调用构造函数。如果你想在对象建立初期就塞入默认配置、验证参数、建立数据库连接这段逻辑放在__init__里就是最规范的时机。5.3 函数分文件、main函数参数与模块化函数写多了就不能全堆在一个文件里。C 语言的做法是头文件.h里放函数声明和宏定义源文件.c里放具体实现其他文件#include头文件就能调用。这个设计奠定了模块化的基本思想——接口和实现分离。我见过很多新手把函数定义写在.h里多个源文件一包含链接时就会报重定义错误原因就是声明可以重复定义必须唯一。Python 的分文件相对自由一个.py文件就是一个模块import进来后通过module.func()调用。JavaScript 从 ES6 开始有了正规的模块语法export导出、import引入。分文件不是单纯地“把一个函数挪到另一个文件”而是在规划你的依赖边界谁负责工具函数谁负责业务逻辑谁负责配置层级关系要清晰。我自己的习惯是纯计算、无副作用的函数放一个独立的utils模块里方便到处复用业务相关函数跟着业务模块走尽量不要跨层调用。main函数参数是另一个常见的模糊地带。C 和 C 的main(int argc, char *argv[])argc是命令行参数个数argv是参数数组argv[0]通常是程序名。Python 里对应的是sys.argvJavaScript 跑 Node 时是process.argv。新手最容易犯的错是把argv[0]当成第一个业务参数导致参数全部错位。遇到这种问题先打印一遍整个参数数组确认下标再写解析逻辑。C 里还有线程函数这个概念线程本质上需要提供一个入口函数给操作系统很多时候这个入口函数就是个普通函数但要求它不能随便返回局部变量因为线程函数的生命周期和主线程不一定同步。5.4 常见报错与排查cmdlet、notifycallbackdata、vsc.attach函数相关的报错里出现频率最高的几个我已经形成条件反射了。第一个是运行npm或claude、codex这类命令时终端提示“无法将‘xxx’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。这个报错其实和函数一点关系都没有它说的是当前终端环境找不到这个可执行文件。原因九成是环境变量 PATH 没配好或者安装的时候没有勾选自动添加 PATH也可能改了 PATH 之后没重启终端。排查路径是先用where npm或者Get-Command npm看能不能找到找不到就直接定位到 Node.js 的安装目录手动把路径添加到系统环境变量里然后重开终端。不要一个个猜先确认文件在哪再确认 PATH 有没有。第二类是运行时提示 “无法找到函数 notifycallbackdata”。这种错误我一般在插件开发、脚本注入场景里见得多十有八九是函数本身没有被定义或者定义所在的环境还没加载完调用却提前发生了。排查时先全局搜索这个函数名确认代码里有没有定义再确认调用时机是不是在 DOM 加载完成、SDK 初始化完成之后。浏览器插件里如果页面脚本还没执行完content script 就去调用页面的函数也会报找不到函数这时候要让通信建立在事件机制上而不是直接调用。第三类是 VS Code 里报“没有 .vsc.attach 这个函数”。这种报错通常和某个插件配置有关比如调试扩展期待你在settings.json里配置 attach 参数但你用了别的写法或者扩展版本和配置文件不匹配。先打开命令面板查插件文档确认当前版本支持什么配置项再回去改配置不要盲目抄网上的旧版配置。还有一类常见问题是函数返回值“看起来没生效”C 里int函数写了 return 却返回了乱码先检查函数所有分支是不是都有 return再看是不是返回了局部变量的引用。这类问题不用硬背错误码关键是主动打印中间变量把“当前到底走到了哪一行、返回了什么”摆到台面上。6. 把一个函数写扎实实操心得函数实现到这一步语法、机制、坑都聊完了剩下的是设计层面的问题。我见过太多能跑但极其难维护的函数参数七八个内部逻辑上百行变量名是a、b、tmp改一个需求要顺着代码捋半天。我自己现在写函数有几个硬性标准每个函数尽量只做一件事一个函数超过 30 到 50 行就要考虑拆分参数控制在三个以内再多就合并成一个配置对象函数名用动词开头或者用is、has这类判断词开头一眼能看出它是干什么的。还有一个我很看重的习惯优先写纯函数。纯函数就是同样的输入永远得到同样的输出不修改外部状态不依赖全局变量也不触发副作用。这种函数最好测试也最容易复用。比如在函数里直接改全局配置或者每次都console.log一堆调试信息短期看方便长期看就是把函数的“行为契约”搞浑浊了。真需要日志和状态变更就把它们抽出来放在调用层让底层函数保持干净。关于注释我坚持一个原则注释写“为什么”不写“是什么”。代码本身能说明它做了什么注释的价值在于解释当初为什么这么设计、有哪些限制条件、哪块逻辑容易踩坑。比如一个函数里有个看起来很奇怪的 1注释写一句“这里加一是因为数据库的自增ID从1开始但业务展示需要从0对齐”比写“计数器加一”有用一百倍。写完函数之后留一点时间回头看看能不能砍掉重复代码能不能把命名改得更贴切这三五分钟的投入抵得上后面几次排查 bug 的成本。最后再分享一个我常用的“小工具”思维函数其实是你的代码资产不是一次性消耗品。每次你写下function或def都是在创造一个新的接口调用你的人包括三个月后的你自己对你的信任程度取决于这个函数的边界清不清楚、行为稳不稳定。把这层想明白了你自然会在写函数之前多想一会儿。
