深入理解编程函数:从基础概念到设计技巧与重构实战 1. 从数学课到代码编辑器函数到底是一种什么东西1.1 数学里的函数和代码里的函数其实是一家人很多人在初学编程时会觉得函数是个有点吓人的术语。一提到函数立刻想起数学课上的 f(x) x² 1脑子里浮现出一条抛物线。但说实话数学里的函数和代码里的函数本质上是同一个东西。我当时在给某高校做模拟项目X的教学辅助系统时就发现不少同学卡在为什么我写了函数程序还是错的这个问题上。后来我把数学函数和代码函数放到一起讲大家一下子就通了。数学函数的核心是给定一个输入经过某种规则得到一个输出。y f(x)x 是输入f 是规则y 是输出。同一个 x只要规则不变得到的 y 永远一样。这叫做确定性。代码里的函数本质上也是在干这件事你把数据递进去函数内部执行一串操作然后把结果交还给你。输入是参数规则是函数体里的语句输出是返回值。不同的是数学函数通常只有一个输入和一个输出而且它永远是干净的——它不碰外部世界的任何东西。但代码里的函数可以一个参数都不接受也可以返回一堆东西甚至可以不返回任何值去干点别的活儿比如写日志、改数据库、发请求。这也就引出了一个特别重要的概念副作用。这个概念我后面会专门展开讲你先记住一句话数学函数是纯粹的映射代码函数是映射加上可能的额外动作。1.2 编程函数的核心三件套输入、处理、输出咱们拆开来看代码函数逃不出这三样东西入参、处理逻辑、返回值。听起来很简单但很多人写函数写得一团糟恰恰是因为这三样东西没有想清楚。先说入参。入参是函数对外界的唯一请求——当然如果你用了全局变量那就不止了这是后话。入参设计得好不好直接决定这个函数的通用性和可读性。我见过有人写一个计算订单折扣的函数里面不传订单金额而是直接读一个全局变量。当时我就觉得这函数换一个场景就废了。正确做法是把函数需要的一切数据都通过参数显式地传给它。比如下面这个例子# 不推荐偷偷依赖全局变量 discount_rate 0.8 def calculate_final_price(): return base_price * discount_rate # 推荐所有数据通过参数传入 def calculate_final_price(base_price, discount_rate): return base_price * discount_rate第一种写法一旦 discount_rate 在别处被修改这个函数的结果就不可预测了。第二种写法函数像一个独立的工具你给它什么它就基于什么来算。这就是入参设计的核心原则显式优于隐式。再说处理逻辑。这是函数内部的一堆语句。理想情况下处理逻辑应该聚焦于完成这一件事。如果函数名叫 get_user_name那里面就不应该出现更新用户积分这种操作。处理逻辑写得清楚靠的是把大问题拆成小步骤每一步用一个语义明确的局部变量过渡。我习惯把函数体内的逻辑控制在十行左右超过二十行我就会琢磨是不是该拆函数了。最后是返回值。返回值是函数向外界传达结果的方式。没有返回值的函数在 Python 里其实也返回了一个 None在 JavaScript 里返回 undefined。你写函数时要明确回答一个问题调用者拿到这个返回值之后下一步要做什么如果返回值的设计不能让调用者顺畅地做下去那这个函数的接口就是失败的。我后面会专门聊返回值设计的一些坑。1.3 函数不是语法糖而是你手里第一把抽象武器总有人觉得函数不就是把一段代码包起来换个名字反复调用吗这种理解没错但视角太低了。函数真正厉害的地方在于它让你拥有了抽象的能力。什么叫抽象就是你把怎么做的藏起来只暴露做什么。别人用你的函数不需要知道里面是用了循环还是递归是用了 SQL 还是内存计算。这就好比你去餐馆点菜你只需要说来一份番茄炒蛋不需要知道后厨是怎么洗菜、怎么切菜、怎么掌握火候的。函数就是菜单上的菜名函数体就是后厨的流程。菜单让食客省心函数让调用者省心。这个抽象能力对你的编程生涯有多重要我给你打个比方。小学时我们学算术先学加减再学乘除再学分数。如果一直用加减法表达一切那3 × 7就得写成3 3 3 3 3 3 3写到手酸。乘号就是一种抽象它把重复的加法打包了。函数同样如此把重复的逻辑打包然后你只需要关心调它、传参、拿结果。所以在正式讲使用技巧之前我想先立一个总原则不要为了用函数而用函数而是当你发现一段逻辑可以被命名、被复用、被单独测试时你就该把它变成函数了。函数是你组织代码的最小单元就像积木里最小的那块标准方块。后面所有的高级用法比如闭包、纯函数、高阶函数全都是在这个基础概念上长出来的。2. 我拆过无数烂函数之后总结出这几个设计基本功2.1 命名一个好名字胜过一次重构我接手过不少项目有一类代码让我最头疼函数名叫 doSomething、 handleData、 processInfo 的。这种名字我看十次也记不住它到底是干嘛的每次调用都得跳进函数体里读代码。这浪费的时间比写代码的时间还多。命名的核心原则是看到一个函数名你就能说出它的输入是什么、输出是什么、干了什么事。比如 calculate_total_price(price, quantity)任何人一看就知道输入单价和数量输出总价。再比如 send_email_to_user(user_email, content)一看就知道给指定邮箱发内容。反例就是 process_data(data) 这种那到底怎么处理是过滤、是排序、是聚合还是全部都要名字里完全没有信息量。我给函数命名的经验就三条。第一动词开头动作感要强。get、set、calculate、parse、fetch、validate、handle……这些动词本身就在说明函数的行为类别。第二名字里带出关键名词这个名词通常就是入参或返回值的核心对象。第三长度不重要清晰最重要。calculate_total_cost 虽然长但比 calc 好一百倍。我记得在某跨平台系统项目里有一个函数起初叫 analyze后来我改成了 detect_anomaly_and_alert虽然长了几个字符但所有读代码的人瞬间就明白了它的职责边界。有些同学可能觉得命名嘛差不多得了反正代码能跑。但我可以负责任地说代码是写给人看的顺便给机器执行。你和同事的沟通成本大半都花在读懂彼此代码上。多花三十秒想名字能帮你和后人省下三个小时跟代码纠缠的时间。2.2 参数设计少即是多稳即是善函数的参数设计是另一个重灾区。我见过一个函数参数有九个调用的时候得一行行对传错一个位置就是隐藏 bug。参数多到一定程度这函数就不是工具而是负担了。参数设计的第一原则是能少就少。如果参数超过三个你就得想想是不是该把这些参数打包成一个对象或者结构体。比如# 四个参数调用时容易搞混 def create_order(user_id, product_id, quantity, address): ... # 合理做法用对象收敛参数 def create_order(user: User, item: Product, quantity: int, address: str): ...把 user_id 替换成 User 对象product_id 替换成 Product 对象表面上看参数数量没变但语义强了很多而且以后想增加用户的其他信息不需要改函数的参数列表。这就是内聚的思想相关的数据打包传递。第二个原则是参数的语义要单一。我遇到过有人写一个函数第二个参数有时候传的是金额带两位小数有时候传的是分数整数函数的内部逻辑也随着调用方的不同产生了分支。这种设计简直是定时炸弹。一个参数就应该只有一种含义、一种格式。如果你发现同一个参数在调用时总是传不同类型的数据那说明这个函数抽象错了应该拆成两个函数。第三个原则是关于默认参数的。默认参数很好用但它有个隐蔽的坑。在 Python 里默认参数只在函数定义时求值一次。如果你写 def append_to_list(item, lst[])那么多个调用之间会共享同一个列表因为你用的是同一个默认列表对象。正确的写法是 def append_to_list(item, lstNone)然后在函数内部 if lst is None: lst []。这个坑我踩过当时在模拟项目X里做数据聚合时连续几个订单的数据莫名叠加查了大半天才定位到是默认参数共享导致的。对这种细枝末节一定要保持警惕。2.3 返回值宁可简单别玩花活返回值设计看似简单其实暗藏不少问题。第一种常见问题返回类型不稳定。有时候返回一个列表有时候返回 None有时候返回一个布尔值。这种函数对调用者极不友好因为调用者必须写一堆防御代码去判断返回值的类型。我的原则是一个函数的返回类型应该稳定最好永远一样。如果你想让调用者知道函数到底成没成功可以用异常也可以用专门的结果对象但不要一会儿返回数据、一会儿返回 None让调用者在那边抓瞎。第二种常见问题函数什么都返回。一个函数既返回计算的结果又返回一个状态码还返回一条日志字符串。这就把函数变成了一个杂物袋调用者必须小心翼翼地从里面挑自己需要的东西。正确的做法是函数只返回一个核心结果。如果确实需要多个信息就把它们封装成一个结果对象。比如// 不好返回一个数组靠约定去取 function findUserById(id) { // ... return [success, user, errorMessage]; } // 好返回一个明确的对象 function findUserById(id) { // ... return { success: true, user: user, errorMessage: null }; }第二种写法调用者可以用 result.success 来判断可以在 result.user 里取数据结构清晰可扩展性也强。还有第三种常见问题函数没有返回值但调用者需要结果。这个情况最容易出现在异步场景里你在一个函数内部发了一个 HTTP 请求但是用 forEach 循环去收集结果结果发现拿不到数据因为 forEach 不处理返回的 Promise。解决办法是用 for...of 加 await或者 Promise.all。这种问题我在做某图像处理 Demo 的时候也遇到过以后有机会专门写一篇聊异步的坑这里先埋个伏笔。2.4 一个函数只干一件事怎么判断才算干一件事单一职责原则这个说法大家应该都听过。但问题来了什么叫一件事一个函数里先读文件再清洗数据再训练一个模型最后把指标写入数据库这叫四件事。那如果我把这些操作分成四个函数read_data、clean_data、train_model、save_metrics每一件都算一件事了。但总有一种模糊地带当一个函数内部有三四个步骤它们到底算是一件事的多个步骤还是四件事我的判断标准很简单你能不能用一个不含和字的短句概括这个函数做了什么如果能那基本就是单一职责。举个例子normalize_and_validate_user_input这个函数名叫规范化并校验用户输入中间有和字这其实是两件事。更合理的拆法是先 normalize_user_input再 validate_user_input。虽然拆成两个函数会让调用方多写一行代码但每一行代码的意图都更清晰了。再举个例子我曾经在某跨平台系统里看到一个函数叫 save_and_notify。这个函数先保存订单再发通知。刚开始设计时觉得很方便一个调用全搞定。但后来需求变了有的订单不需要发通知有的订单要在保存失败时发告警这个函数越来越臃肿最后被迫拆掉了。如果一开始就按单一职责拆开后面根本不用返工。所以我的经验是别嫌拆开麻烦拆开是给未来留余地。哪怕现在调用方只有一两个等需求真的膨胀到你无法收拾的时候你会发现提前拆好的函数就像搭好的积木怎么组合都顺手。3. 作用域、闭包和副作用函数用着用着就翻车的三个重灾区3.1 作用域你的变量到底活在哪个世界作用域这个词听起来有点玄其实就是在问一个问题一个变量到底在代码的哪一块区域里可以被访问。函数内部声明的变量在函数外部通常访问不到全局变量虽然到处都能访问但它的代价是你失去了对哪里修改了它的掌控。很多初学者爱用全局变量因为省事不用在函数之间传递参数。但省事的背后是巨大的维护成本。你想想如果 discount_rate 是一个全局变量你在 A 函数里改了它B 函数里用到它时结果莫名其妙变了。更可怕的是B 函数压根不知道自己依赖了一个会被别人改的变量。这种隐式依赖大多数情况下是程序 bug 的温床。那正确的心态是什么尽量把变量的作用域控制到最小。能用局部变量的不要用全局变量能传参数的不要偷偷读全局。函数是变量的天然边界你把变量包在函数里它就像躲在一个保险箱里外面的人想改也改不了。这种隔离感会让代码的安全性高很多。举一个实际调试过的例子。某次我在做一个订单金额计算功能有位同事写了一个函数里面直接引用了模块级变量 TAX_RATE。后来另一个同事在后来的代码里改了它的值导致之前的计算全部出错。排查了很久最后发现是两个模块的初始化顺序问题。从那次之后我在团队里立了一个规矩函数里用到的数据要么是参数传进来的要么是函数内部创建的坚决不读外部可变状态。如果你确实需要一个全局配置那就把它作为参数传入函数。虽然多写了一行参数但程序的可预测性完全不一样了。3.2 闭包会记住现场的函数用不好就是幽灵闭包是函数进阶里绕不过去的一个概念。简单说如果一个函数内部定义了另一个函数并且内部函数引用了外部函数的变量那么内部函数就形成了一个闭包——它会把外部函数的变量记在心里即使外部函数已经执行完毕。闭包本身是个很强大的工具比如用来实现计数器、缓存、防抖。但用不好就会踩坑最经典的就是循环里的闭包。来看这段 JavaScript 代码for (var i 0; i 5; i) { setTimeout(function () { console.log(i); }, 100); } // 输出5 5 5 5 5 而不是 0 1 2 3 4为什么因为 setTimeout 的回调函数在 100 毫秒后才执行到那时循环已经跑完了i 已经是 5。而闭包记住的是变量 i 本身不是 i 的每一个值。所以五个回调函数看到的是同一个 i也就是最终值 5。这就是闭包记住了现场但现场已经变了。解决方案有几种。第一种用 let 声明let 是会为每次循环创建独立绑定环境的这是 ES6 之后最干净的解法。第二种用 IIFE 立即执行函数把 i 的当前值作为参数传进去强行拷贝一份。第三种用 bind。我以前带的新人经常在这里卡壳我通常让他们记住一句话闭包抓的是变量引用不是变量值。只要记住了这句话循环闭包的坑你就踩不中了。闭包也不全是坑。它最优雅的应用场景是隐藏状态。比如你要写一个计数器function createCounter() { let count 0; return function () { count 1; return count; }; } const myCounter createCounter(); myCounter(); // 1 myCounter(); // 2外部根本碰不到 count 变量但内部函数可以一直更新它。这种私有状态的能力在面向对象里要用 class 加私有字段才能实现而闭包用几行代码就搞定了。所以闭包不是幽灵而是你手里的一把双刃剑关键是理解它的机制。3.3 副作用纯函数为什么让我在加班夜里感到安心刚才提到数学函数是干净的代码函数里那些额外动作比如打印日志、修改全局状态、写文件、发请求统称为副作用。有副作用的函数不一定不好——很多程序的功能本身就是副作用比如存储用户数据、发送邮件。但副作用越多函数的不确定性越大越难测试越难预测。我特别推崇一种编码习惯把函数的绝大多数逻辑写成纯函数。所谓纯函数有两个条件第一相同的输入永远得到相同的输出第二没有任何可观察的副作用。什么意思给它 1 和 2它永远返回 3而且它不会偷偷改你外面的变量不会打乱你的全局状态。有人会说那我写一个函数连日志都不打印调试怎么办我的建议是打印日志这种副作用放在调用层处理不要塞进纯函数里。比如你要算一个订单应缴税额calculate_tax(amount, rate) 就应该是纯函数只负责计算。至于要不要打印日志、要不要落库那是调用方的事。这样一来calculate_tax 可以放心地用单元测试去测不需要清理文件、不需要 mock 网络、不需要担心污染数据。我用一张表来对比纯函数和带副作用的函数你感受一下差别对比项纯函数有副作用的函数相同输入永远相同输出结果可能受外部状态影响可测试性直接传参、断言返回值即可需要 mock 外部依赖并发安全性天然安全不共享状态容易竞争、相互干扰可预测性从代码到行为一目了然需要追踪外部状态变化调试难度低出 bug 就是输入/逻辑问题高可能是外部环境造成我在开发某图像处理 Demo 时把核心的图像变换算法全写成纯函数传入图片像素数组返回新数组。整个项目跑起来非常稳测试覆盖率也高。而那些读写文件的函数只有薄薄几层所有复杂逻辑都在纯函数层里。这种分层设计让我的加班时间至少少了一半。你也试一次就回不去了。4. 函数的重构实战从一个反例到测试全覆盖的完整过程4.1 一个宁可复制粘贴也不愿看第二遍的函数长什么样空谈概念没意思我们来一个真实的例子。假设某项目里有个需求从原始数据中找出异常值并生成摘要报告。最刚开始同事是这样写的风格类似非真实源码def process(data): results [] for item in data: if item[value] 100 or item[value] -100: results.append(item) total 0 count 0 for item in results: total item[value] count 1 avg total / count if count 0 else 0 out open(report.txt, w) out.write(异常数量: str(count) \n) out.write(平均异常值: str(avg) \n) out.close() return results这个函数有什么问题第一函数名 process 太泛看不出它做什么。第二它同时做了筛选异常值、计算均值、写入报告这三件事。第三文件写操作塞在计算逻辑中间如果 report.txt 不能写入整个函数就异常中断。第四这个函数几乎没有复用价值——如果我想只筛选异常值却被迫写入文件如果我想只生成报告却被迫从头计算。这就是典型的坏味道函数。我当时拿到这个代码的第一反应不是去删除重写而是先理清职责边界。我画了三条线第一筛选逻辑应当独立第二统计逻辑应当独立第三文件输出应当独立。这三条线画完重构的方向就清楚了。4.2 只抽函数还不够组合设计才是重构的灵魂于是我把 process 拆成了三个小函数再在外面包一层组合函数def filter_anomalies(data, threshold100): return [item for item in data if abs(item[value]) threshold] def calculate_avg(values): if not values: return 0 return sum(values) / len(values) def write_report(file_path, count, avg): with open(file_path, w) as f: f.write(f异常数量: {count}\n) f.write(f平均异常值: {avg}\n) def process(data, report_pathreport.txt): anomalies filter_anomalies(data) avg calculate_avg([item[value] for item in anomalies]) write_report(report_path, len(anomalies), avg) return anomalies拆完之后每一个函数都可以独立测试和复用。filter_anomalies 是纯函数calculate_avg 是纯函数write_report 负责副作用。process 作为组合入口负责协调。这样的设计比原来的单一巨无霸函数强了多少我觉得至少强了三个维度第一是可读性。读 process 的人不需要关心 filter_anomalies 内部是大于还是小于只看名字就知道它筛异常。第二是可测性。filter_anomalies 和 calculate_avg 可以直接用单元测试覆盖不需要准备文件系统。第三是可扩展性。如果以后想改阈值、想生成 CSV 报告、想加一个最大值统计只需要在组合层调整小函数本身不用大动。这个例子体现了一个重要的思路重构不是把大函数机械地切成小函数而是让每个小函数都有清晰的边界再通过组合把它们编织起来。组合本身就是一种设计能力。真正的重构是把一团乱麻理顺成一根根线再按逻辑重新编成网。4.3 用单元测试给函数上保险边界值别放过函数拆好了接下来最关键的一步是给每一个纯函数写单元测试。很多人觉得测试麻烦但我恰恰认为没有测试的重构就是在走钢丝。你拆完了函数怎么知道行为没有变全靠测试来兜底。就拿 filter_anomalies 来说我需要测几类情况def test_filter_anomalies(): # 正常情况能筛出超过阈值的项 data [{value: 50}, {value: 150}, {value: -20}] assert len(filter_anomalies(data)) 1 # 边界情况正好等于阈值算不算异常需要明确约定 data [{value: 100}] # 如果约定是 100 才算异常那么这条应该不筛出来 assert len(filter_anomalies(data, threshold100)) 0 # 空列表不能崩溃 assert filter_anomalies([]) [] # 负阈值也要正常工作 data [{value: -10}, {value: 10}] assert len(filter_anomalies(data, threshold5)) 1注意边界值的处理。我见过很多 bug都出在到底是大于还是大于等于这种一字节之差上。写测试时把边界值专门拎出来测一遍就能把这一类的坑提前堵住。calculate_avg 也要测空列表、单元素列表、全是负数、浮点数精度等场景。浮点数尤其要小心0.1 0.2 不等于 0.3 的经典问题就不用我多说了。写完测试之后跑一轮绿了再跑一次原来的 process确认行为和重构前一致。这个过程看起来简单却是整个重构里最重要的安全网。没有安全网你就不敢动代码不敢动代码坏味道就永远留在那里。4.4 避坑记录三个我亲历的重构事故说了这么多应该怎么做我想再聊聊我踩过的坑免得你重蹈覆辙。第一个坑是过度拆分。有一段时间我特别追求函数短小结果把一个简单的登录流程拆成了七八个函数每个函数只有两三行。看起来每个都很干净但读起来反而要跳来跳去七八次。后来我明白了一个道理拆分的目的是降低认知负担如果拆完反而增加了认知负担那就是过度设计。函数多短算短能在一个屏幕里看清逻辑就行拆到什么程度以语义完整为边界不要为了拆而拆。第二个坑是重构过程中改动了行为。有一次我重构一个字符串解析函数顺手优化了它的返回值格式结果下游有个调用方一直依赖旧格式程序上线后出了问题。从那以后我立了规矩重构和加功能要分开要么你先用测试锁住旧行为要么你重构期间绝不动行为。重构的本质是行为不变、结构更好这一点一定要记住。第三个坑是没有及时更新调用方。你拆了函数改了函数名但调用方散落在项目各处有些调用还在用旧函数。这就是为什么我上面强调要在测试里跑全量回归。在大型项目里光靠肉眼找调用方是不现实的一定要借助 IDE 的全局搜索和编译器的类型检查。如果你用的是动态语言更要在代码审查时特别留意有没有旧接口的残留引用。5. 函数之上从写好单个函数到构建函数式思维5.1 函数是模块的胚芽也是团队协作的最小契约当你把一个个函数都写好了你会发现函数不仅是代码组织单元还是一种沟通单元。你和同事协作时你们不需要了解对方写的每一个循环和判断只需要知道对方提供了哪些函数、每个函数接受什么参数、返回什么结果。函数签名就是人与人之间的契约。我记得在某图像处理 Demo 的开发中团队里有人负责图像格式解析有人负责滤镜算法有人负责渲染输出。我们提前约定好接口解析模块提供 load_image(path) 返回统一格式的像素对象滤镜模块提供 apply_filter(image, filter_type) 返回新图像对象渲染模块负责把图像对象输出到屏幕或文件。只要这几个函数的接口不变每个人都可以在自己的模块里放心地重构内部实现。这就是函数级的模块化接口即契约实现即自由。从这个角度看函数身上承载的不仅是逻辑还有工程协作中的信任边界。你要对调用你函数的人负责名字要清晰参数要稳定返回值要可预期副作用要收敛。一个不守约定的函数就像一个说话不算数的合作伙伴初期看不出问题后期到处是雷。5.2 把行为当成值来传递代码会变得很不一样函数式的下一步是高阶函数概念——把函数当作值传来传去。你可能已经用过不少比如 JavaScript 里的 map、filter、reducePython 里的 sorted(key...)。它们的共同点是接受一个函数作为参数。这有什么用它让通用的循环逻辑变得可以复用。举个例子你有两个数组一个要筛选出大于 10 的数一个要筛选出包含error的日志。不用高阶函数你得写两个循环逻辑几乎一样只是判断条件不同。用高阶函数你只需要写一次过滤的骨架把判断条件作为函数传进去const numbers [5, 15, 25]; const logs [info: ok, error: fail, warn: slow]; const bigNumbers numbers.filter(n n 10); const errorLogs logs.filter(line line.includes(error));filter 这个函数自己只负责遍历数组、对每个元素调用回调、收集返回值为 true 的元素。判断逻辑完全由传入的回调函数决定。这就是把行为当成值传递。函数不再只是一个被调用的实体它可以被存储、被传递、被组合像数据一样流动。这种思维方式一旦建立你写代码的抽象层次就上了一个台阶。你不再关心怎么遍历而是关心要筛选什么条件不再关心怎么排序而是关心按什么规则比较。那些重复的循环、分支、遍历骨架都被通用函数收走了。你会发现你的代码变得更短、更声明式、更像是在描述要什么而不是怎么要。5.3 日常训练函数的三大建议讲到这里你应该已经感受到函数这个概念从浅到深真的是一个可以持续打磨的技能。最后我分享三个我一直在用的训练方法你可以照着练。第一条每周刻意重构一段旧代码。不用多挑一个函数试着用今天讲的思路分析它——命名是否清晰、参数是否简洁、职责是否单一、副作用是否收敛、有没有可以拆分的部分。然后动手改跑测试验证。坚持一个月你对函数的感觉会明显不一样。第二条写测试逼自己把函数设计成可测试的形状。如果一个函数很难测那大概率说明它的设计有问题要么依赖外部状态太多要么职责太杂要么返回类型不稳定。把可测试性当作函数质量的一个度量标准非常有效。第三条读开源项目的时候只看函数签名。挑一个你熟悉领域的开源库先不读源码只看它公开了哪些函数、每个函数的参数和返回类型是什么。猜一猜它是怎么实现的然后再去读源码验证。这个练习能极快地提升你抽象设计的能力因为它逼你站在调用者的角度看问题。函数是编程世界里最小也最重要的一个概念。它看起来简单用起来却有无穷的层次。你可以在一个下午学会它的语法却要花很久才能把它用得炉火纯青。但没关系每一个写过的函数每一次重构每一份测试都是在帮你打磨这件趁手的工具。我自己有个习惯写新代码时每写完一个函数都会停下来问自己三句话——它叫什么名字外部怎么调用它如果三个月后有人来维护它看得懂吗如果你的答案都能脱口而出那这个函数基本就合格了。希望这篇文章能帮你把函数这两个字的重量真正掂量出来在以后写代码时多一份笃定少一些返工。