ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

函数深度解析:从作用域、闭包到Java 8 Function实战

2026/9/29 18:14:13 拓冰建站 浏览量
函数深度解析:从作用域、闭包到Java 8 Function实战 函数Function这个词可能是程序员职业生涯里最早接触、却最晚真正想明白的概念之一。我见过不少人能熟练写出几十个函数但一遇到函数作为参数闭包Function Calling就发怵也见过大量报错像implicit declaration of function fabs、call to undefined function mysqli_connect()本质上都是对函数的理解还停留在背语法层面。这篇博文我想按自己这些年排查问题、写业务代码、带新人的实际经验把 Function 拆开揉碎讲一遍从它到底是什么到作用域、闭包、高阶函数再到调用报错怎么反推原因最后拿 Java 8 的 Function 接口做一个能直接落地的实战。尽量让你读完不只是见过而是真的能用明白。1. 函数到底是个什么东西从数学到编程1.1 函数不是把代码装进盒子这么简单很多教程告诉你函数就是一段可以重复调用的代码块。这话没错但它只解释了形状没解释本质。我更喜欢把函数理解成一个映射关系你给我一批输入我给你一个输出中间的过程封装在黑盒里。这和数学上 y f(x) 是一回事只是编程语言里这个 f(x) 可以干更复杂的事比如读写文件、发网络请求、操作数据库。一旦你从这个角度看很多困惑就解开了。为什么函数要有参数因为参数就是输入为什么要有返回值因为返回值就是输出。那为什么有些函数没有返回值因为它的输出不通过返回值体现而是体现在对外部世界的影响上比如打印了一行日志、修改了一个全局变量、往数据库里插了一条记录。这类函数在编程里有个专门称呼叫**有副作用side effect**的函数。我早期写代码从不区分有返回值和有副作用结果就是函数满天飞测试无从下手排查问题要靠猜后来才明白把函数当成输入输出映射来设计代码的可预测性会大幅提升。还有一层更需要想明白函数不只是代码块它在你写下的那一刻其实是一个可执行的计算单元。你在代码里写function add(a, b) { return a b; }这只是在定义一个计算规则只有当你写add(1, 2)的时候这个规则才真正被执行。定义和调用之间的区别是所有函数相关报错的第一源头。1.2 声明、定义、调用一函数三用很多人把声明和定义混为一谈但在 C 语言和很多编译型语言里这俩是严格区分的。**声明Declaration**是告诉编译器嘿存在一个叫fabs的函数它接收一个 double返回一个 double但它的实现在别处。 在 C 里这通常写在头文件里比如#include math.h。**定义Definition**则是把函数体写出来真正实现那个计算规则。**调用Call**就是使用它。我当年在 C 项目里遇到warning: implicit declaration of function fabs就是典型的三者混淆案例。代码里直接写了fabs(x)但没#include math.h编译器在遇到这个调用时还没有见过任何关于fabs的声明于是它猜了一个隐式声明结果类型对不上轻则警告重则运行时崩溃。在 C99 之后这直接就是错误。所以每当你看到未声明未定义未找到这类报错第一反应不该是去检查函数名拼写而该去检查三件事这个函数声明了没有头文件、接口文件、定义了没有实现文件、依赖库、调用时在不在作用域内你确定你调用的是同一个函数吗。这三个层次排查完90% 的找不到函数问题都能定位。1.3 输入输出模型参数、返回值和副作用一个函数设计得好不好我有个很朴素的标准把参数和返回值画出来看它像不像一条流水线。输入从一侧进来输出从另一侧出去中间不搞小动作这样的函数最好理解、最好测试、最好复用。参数有两种传法这也是新手最容易踩的坑按值传递和按引用传递。按值传递时函数拿到的是输入的一个拷贝你在函数里怎么改都不影响外面按引用传递时函数拿到的是同一块内存的地址你在函数里改了外面也跟着变。像 Java、Python 这类语言对象作为参数传递时本质上传递的是引用在 Java 里叫引用传递Python 里叫对象引用传递很多人因此写出隐蔽 bug。我举一个典型例子Python 里如果你写def append_item(item, lst[])这个默认参数[]在函数定义时只创建一次所有不传该参数的调用共享同一个列表。结果就是第一次调用往列表里塞了东西第二次调用进来列表已经不是空的了。这个坑我踩过一次就再也没忘。函数的行为依赖外部共享状态越少越不容易出诡异问题。所以我做代码评审时看到全局变量修改、默认参数可变、隐式依赖外部环境的函数都会格外警惕。2. 吃透函数必须搞懂这五件事2.1 作用域变量在哪生效在哪失效作用域就是变量的可视范围。生活里类比的话就像房间里的灯你在厨房开了灯客厅不会因此变亮灯的作用域是厨房。局部变量定义在函数内部函数执行完就销毁外面访问不到。全局变量定义在函数外部所有函数都能访问。听起来很简单但嵌套函数、块级作用域如let在{}内、词法作用域lexical scope这些概念一叠加很多人就晕了。比如 JavaScript 里function outer() { let x 10; function inner() { let y 20; console.log(x y); // 30inner 能访问 outer 的 x } inner(); console.log(y); // ReferenceErrorouter 访问不到 inner 的 y }作用域规则是向内看不向外看内层函数可以访问外层函数的变量但外层函数访问不了内层函数的变量。理解了这个你就明白为什么很多库代码喜欢包一层立即执行函数不是闲得慌而是为了把内部变量隔离在自己的作用域里不去污染全局。我有一次排查线上问题发现一个全局变量被两个函数同时读写谁先执行结果就不一样。定位到根因后我没有去加锁而是把共享变量收进了函数内部通过参数传递。代码瞬间安静了。作用域设计的本质是管理变量的可见性和生命周期。你每把一个变量暴露到更外层就是给未来埋了一个潜在的并发和耦合隐患。2.2 匿名函数与回调没有名字的函数也是函数匿名函数就是没有名字的函数。你可以直接把它赋给变量或者作为参数传给另一个函数。这东西在 JavaScript 里无处不在比如数组的map、filterconst numbers [1, 2, 3, 4]; const doubled numbers.map(function (n) { return n * 2; }); // 或者用箭头函数简写 const doubled numbers.map(n n * 2);匿名函数最常见的用途是回调callback你告诉某个函数做完你的事之后帮我调用一下这个逻辑这个逻辑就是回调函数。比如事件监听、定时器、网络请求回调全是这个模式。理解回调的关键是你不是在调用那个函数你是在传递那个函数给别人调用。别人什么时候调、以什么参数调不完全由你决定。这种模式一开始很反直觉因为传统编程是我调用你你算完给我结果的同步思维。回调则把控制权反转了我把一段代码交出去等着别人来叫醒我。后来 Promise、async/await 就是在这个基础上优化体验但底层的思想依然是函数作为一段可传递的代码。2.3 闭包函数记住了它出生时的环境闭包Closure是被问得最多、也最容易讲玄的概念。我的理解很简单当函数被定义在一个特定作用域里时它会把这个作用域打包带在身上即使这个函数以后跑到别的地方去执行它依然能访问当初那个作用域里的变量。看这个例子function counter() { let count 0; return function () { count; return count; }; } const myCounter counter(); console.log(myCounter()); // 1 console.log(myCounter()); // 2按普通作用域规则count是counter函数里的局部变量counter执行完就该销毁了。但返回的那个匿名函数把count的环境闭包住了所以每次调用myCounter()它都还能找到并修改那个count。这就是闭包的核心机制。用生活类比闭包就像一个人从上大学起就保留着宿舍钥匙毕业十年后回去他还能打开当年那个宿舍柜子。只要他还拿着钥匙柜子就不会被清理。闭包的实用价值非常大它可以用来做私有变量在 JavaScript 里模拟私有字段、做惰性计算、做状态封装。也有代价闭包会持续持有外部变量容易造成内存占用尤其在一个循环里创建大量闭包还持有大对象时内存泄漏就是这么来的。我见过一个 Node 服务内存持续上涨查到最后就是闭包持有超大数组GC 回收不掉。所以闭包很好用但不要无脑用。2.4 高阶函数函数也是一种可以传递的数据如果函数可以作为参数传进另一个函数也可以作为返回值从函数里出来那它就是一等公民。支持一等函数的语言里你基本可以把函数当成普通值来使用。接受函数作为参数、或者返回函数的函数叫高阶函数Higher-Order Function。map、filter、reduce都是高阶函数debounce是返回函数的高阶函数中间件框架里层层嵌套的next调用更是高阶函数的集大成者。理解高阶函数能带来什么好处最大的好处是抽象。你不需要关心每个函数内部怎么处理只需要说对这一组数据做这个变换。举个例子你要把一组用户名的首字母大写写成命令式逻辑可能是一大串循环加判断写成高阶函数风格就是一行const formatted users.map(u u.name.charAt(0).toUpperCase() u.name.slice(1));这句话在于把遍历这个重复劳动抽走了你只关心每个元素怎么变。我特别喜欢用高阶函数去做策略模式把不同的处理逻辑封装成不同函数再根据配置选择传哪一个。新增一种策略只需要新增一个函数不需要改调用方代码。这种代码维护起来是真的舒服。2.5 Function Calling当函数成为 AI 的工具Function Calling是近两年特别热的概念但它本质还是函数调用只是调用方变成了大模型。大致流程是你把一组函数的定义函数名、参数 schema、功能描述提供给模型模型理解用户请求后输出一个结构化的调用意图比如search_flight(departure: 北京, arrival: 上海, date: 2025-08-01)你的程序收到这个结构化结果执行对应函数再把结果返回给模型让模型组织成自然语言回复。这个模式最妙的地方在于模型不直接执行代码它只负责决策由你的程序负责执行。这既保证了安全边界又拓展了模型的能力边界。比如我做一个聊天机器人需要实时天气、库存查询、工单创建这些模型本身不知道但我把查询函数注册给它它就能通过函数调用获得真实数据。要理解 Function Calling核心还是函数本身你定义的每个 function 的入参、出参、语义描述是否清晰。如果函数描述含糊、参数设计混乱模型给出的调用就会莫名其妙。所以就算是在 AI 时代基本功依然是把你的函数写好。很多时候我看到有人抱怨 Function Calling 不准低头一看函数的参数说明写得跟没有似的那能准才怪。3. 那些年和函数有关的报错全是理解函数的好教材3.1 implicit declaration of function你用错了顺序还是忘了头文件C 语言的implicit declaration of function fabs是我见过最多的函数报错之一。报错发生在编译器碰到一个函数调用但在该位置之前没有看到任何声明时。很多初学者看到这个就慌了以为是函数写错了其实问题就两类头文件没引入或者函数定义在使用之后。fabs是数学库函数返回 double 的绝对值。要正确使用需要#include math.h #include stdio.h int main() { double result fabs(-3.14); printf(%f\n, result); return 0; }如果你把#include math.h去掉编译器只能靠猜测来推断fabs的签名这一猜就猜出了问题。虽然 C 语言早年的隐式声明规则允许这样但默认参数会被当成 int和 double 的实际定义冲突轻则结果错误重则栈上数据被当成指针导致段错误。这个报错给我们的教训是编译器不是万能的它需要你提供足够的信息来理解你的代码。头文件就是函数的身份证没有它编译器只能靠猜。报错说 implicit意思是我猜的但我猜得没把握这时候你应该去把事实告诉它而不是反复检查函数名。3.2 call to undefined function不是所有函数都在你的进程里Fatal error: Uncaught Error: Call to undefined function mysqli_connect()是 PHP 场景特别常见的错误。字面意思是你调用了一个不存在的函数但实际情况通常是这个函数存在但它所在的扩展没有启用。mysqli_connect是 PHP 连接 MySQL 的函数属于mysqli扩展如果你在 PHP 配置里没有启用这个扩展PHP 的符号表里就根本没有这个函数。我看到很多新手在这个报错前疯狂检查函数名、检查拼写其实方向错了。正确的排查顺序是确认函数确属当前语言/框架内置mysqli_connect不是 PHP 核心函数是扩展函数。确认扩展是否加载跑php -m | grep mysqli看输出里有没有 mysqli没有就去php.ini里启用extensionmysqli。确认环境差异本地开发环境可能装了这个扩展部署到服务器后服务器 PHP 没装代码就炸了。这个报错的深层含义是你认为应该存在的函数在当前的运行环境里并不存在。这提醒我们在写代码时就要清楚哪些函数是语言自带的哪些是扩展库的哪些是自己项目里定义的。跨环境部署前把依赖清单写清楚能省掉一堆半夜排查问题的痛苦。3.3 参数个数与类型不匹配函数签名就是合同函数签名Function Signature是函数的合同它规定了这个函数接受什么类型的参数、有几个参数、返回什么类型。调用方必须遵守这份合同否则运行时就可能抛出异常或返回错误结果。举个例子MATLAB 里常见的报错Undefined function or method optimoptions有一部分原因是参数没配对。optimoptions是 MATLAB 优化工具箱里的函数用来创建优化选项对象。如果你连工具箱都没装它会直接说函数未定义如果你装了但是调用时传的参数类型不对比如把求解器名称传错了它也可能报找不到。我在 Python 里也常遇到类似的事情比如把关键字参数名拼写错Python 会报TypeError: unexpected keyword argument。这时候不要去翻函数实现而是去看函数签名help(函数名)或者在 IDE 里悬停看提示签名会清清楚楚告诉你这个函数收什么、退什么。把函数签名看成合同还有一个好处你在设计自己的函数时会格外小心地设计参数顺序、默认值、可空性。我自己的原则是能不传就不传、能传对象就不传散装参数、有默认值就放在最后。这三点能避免绝大多数调用错乱的糟心事。3.4 运行时和框架差异MATLAB、DirectX 为什么总说找不到函数函数是否存在不仅取决于语言还取决于运行时和框架。比如 DirectX 报错里出现过DirectX function m_swapchain-Present相关的失败这不是说Present函数没有定义而是说这个函数调用执行失败了比如DXGI_ERROR_DEVICE_REMOVED错误消息里带着 function 字样但它跟语法层面的函数不存在是两码事。这里想提醒的是当报错消息里出现 function 这个词时先看清楚是编译期错误还是运行期错误。编译期错误是这东西不存在运行期错误是这东西存在但执行不了。前者的排查方向是头文件、扩展、命名空间、依赖库后者的排查方向是设备状态、权限、资源是否耗尽、版本是否兼容。我用一个表格帮大家快速区分报错类型常见触发场景排查方向编译期隐式声明C 语言缺头文件检查 include编译期函数未定义链接时缺实现文件检查编译/链接选项运行期函数不存在PHP 扩展未启用检查运行时扩展运行期函数执行失败DirectX 设备丢失检查硬件/驱动/资源状态运行期参数类型错误Python 传参不匹配查看函数签名3.5 loadstring 与动态函数运行时生成的代码有多香就有多险loadstring(utf8.char(...))这类写法本质上是在运行时从字符串动态编译出一个函数。Lua 的loadstring接受一个字符串返回一个函数utf8.char 的作用是把一堆 Unicode 码点转成字符两两组合等于把字符串拆成数字再拼回来。动态生成函数的能力很强大比如你可以利用模板拼接代码实现一些高度灵活的配置逻辑或者在沙箱里热加载业务规则。但必须清醒字符串拼接代码 把自己的程序打开一口锅往里扔什么都可能煮熟。在线上环境里我强烈不建议直接执行来源不可信的字符串代码。哪怕来源是可信的只要代码里有一个配置项可以被外部改写风险就被放大了。我的处理原则很简单能用数据结构表达的逻辑就不要用字符串代码表达非要动态执行也必须走白名单、严格校验输入、尽可能在隔离环境里执行。安全不是上线之后补的是在写loadstring的那一刻就要想的。如果你发现自己正在通过拼接字符串构造代码先停下来问一句这个变量能被外部控制吗如果能换个方案。4. 实战把函数当参数传进去Java 8 Function 为例4.1 为什么要传函数而不是传结果很多 Java 开发者在 Java 8 之前习惯了用传结果的方式来复用逻辑把数据处理完把结果传给下一个方法。这种写法的问题是一旦每个调用方的处理逻辑不一样你就得复制粘贴或者用大量 if-else 来区分。传函数则不一样——你传的不是已经处理好的结果而是处理这个数据的能力。举个例子。假设两个业务场景都需要对一批用户数据做转换但转换规则不同一个要大写姓名一个要去掉手机号中间四位。如果传结果你得写两套循环如果传函数你只需要写一套通用的处理骨架把转换规则作为参数传进去。这种设计的好处非常实际核心流程只写一遍变化的部分通过函数参数去扩展。这就是传说中的开闭原则对扩展开放对修改关闭。你新增一种转换规则时不需要去改原来的骨架代码只需要新增一个函数然后传入调用点。代码量减少测试面变小出 bug 的概率也随之降低。4.2 Java 8 的 Function 接口长什么样Java 8 引入java.util.function.FunctionT, R它代表一个接收类型 T 的参数、返回类型 R 的结果的函数。核心方法是R apply(T t)还提供了compose、andThen这些默认方法用来组合函数。看一个最简单的例子import java.util.function.Function; public class FunctionDemo { public static void main(String[] args) { FunctionString, Integer lengthFunction s - s.length(); Integer result lengthFunction.apply(Hello); System.out.println(result); // 5 } }这里的lengthFunction是一个对象但它背后就是一段函数逻辑。你可以把它存进变量、放进集合、作为参数传递跟传一个普通对象没有任何区别。这就是函数是一等公民在 Java 里的落地形态。除了Function还有几个常见的兄弟接口得一起记PredicateT接收 T返回 boolean常用于过滤场景。ConsumerT接收 T不返回结果常用于消费场景比如打印。SupplierT不接收参数返回 T常用于懒加载。这四个接口基本覆盖了日常 90% 的函数式编程需求。记不住没关系你只要能理解这是一个可以像数据一样传来传去的函数,用的时候查一眼 javadoc 几分钟就能上手。4.3 一个真实场景用 Function 重构重复的字段处理逻辑我之前处理过一个权限系统的需求不同角色的用户查出来的用户对象需要做不同的脱敏处理。管理员要看完整手机号运营要看中间四位打码的版本外部接口调用方只能看到尾号。改造前的代码是三个方法每个方法里循环用户列表然后各自做自己的脱敏——大量重复。用Function重构后的骨架大概是这样的public ListUserVO convertUsers(ListUser users, FunctionUser, UserVO converter) { return users.stream() .map(converter) .collect(Collectors.toList()); }然后定义不同的转换函数FunctionUser, UserVO adminConverter user - new UserVO(user.getName(), user.getPhone()); FunctionUser, UserVO operatorConverter user - new UserVO(user.getName(), maskPhone(user.getPhone())); FunctionUser, UserVO externalConverter user - new UserVO(user.getName(), user.getPhone().substring(7));调用点就非常清晰ListUserVO adminList convertUsers(users, adminConverter); ListUserVO operatorList convertUsers(users, operatorConverter); ListUserVO externalList convertUsers(users, externalConverter);这段代码的核心价值在于遍历和集合装配的流程只写了一次真正变化的脱敏规则被抽象成了可插拔的函数。以后要加一个新的脱敏策略我只需要新增一个Function在调用点传进去不用碰convertUsers的代码。这个改造做下来测试只需要覆盖新的Function本身既有逻辑完全不受影响非常舒服。4.4 传参时的三个注意事项把函数当参数传有三件事我建议你务必记住。第一注意空指针。Function作为一个对象完全可能被传入null。如果调用方传了null而你在循环里直接map(converter)运行时就炸了。所以公共方法入口处要加判空或者用Objects.requireNonNull(converter, converter must not be null)。这不是偏执我在生产代码里真遇到过因为上游传了 null 导致整个批量任务失败的情况。第二注意异常处理。函数式接口的apply默认不声明受检异常checked exception。如果你的转换逻辑里需要抛受检异常比如解析日期抛ParseException直接写会编译不过。常规解法是包装成RuntimeException但这样做会让调用方难以捕获并精确处理。我建议在业务层面定义一个自己的异常类型把上下文信息比如用户名、当前操作类型一起带上这样排查问题时能看到是哪个转换、哪个对象出了问题。第三注意调试成本。函数式代码一旦组合多了调用栈会变得非常抽象。你看到一个lambda$convertUsers$3的堆栈很难一眼看出这是哪个业务规则。我的实践经验是不要在单个函数里塞太长的逻辑。每个Function最好只做一件事并且起一个有意义的名字不要所有地方都写u - { ... 一大串 ... }。命名清晰、函数短小这种风格在函数式代码里比命令式代码更重要因为它没有天然的函数名作为阅读锚点。5. 函数设计避坑指南与问题速查5.1 函数设计的五个坏味道写函数人人都会但要写得让人三个月后还能看懂、改得动就需要避开一些坏味道。按我的经验最常见的有五个。第一个是函数过长。一个函数几十上百行干了好几步事这种代码说白了就是不敢拆。拆函数不是炫技是为了让每段逻辑有名字、有边界、可单独测试。我一般的标准是一个函数如果注释里需要写然后超过两次就该拆了。第二个是参数太多。超过五六个参数调用点就会变成天书。这时候考虑引入一个参数对象把相关联的字段打包传进去。参数少的好处不只是好看调用方不容易搞错顺序函数内部的组合也更清晰。第三个是返回值类型模糊。返回Object、返回null表示各种含义、返回Map让调用方自己取 key这些都是坏味道。函数的返回值应该精确表达我给了你什么。我见过太多代码里result.get(success) true这种字符串布尔值一旦拼写不一致就静默出错。第四个是函数内部做了超出名字范围的事。函数名叫getUserInfo结果里面还顺手更新了登录次数。这种顺带做的副作用会让调用方完全无法预判函数行为。我的原则是函数的名字就是它的承诺做了承诺之外的事就是对调用方的背叛。第五个是过度抽象。有些函数本来一行能写完非要从配置文件里参数化十几个选项结果没人知道怎么调。抽象的度是等出现了两三次重复再说不要为了以后可能用到而过度设计。写代码和做设计一样少即是多。5.2 常见报错速查表为了方便你直接对照我把上文提到的和日常最常遇到的函数相关报错整理成一张速查表。报错信息语言/环境根因解决思路implicit declaration of function fabsC/C缺少头文件引入math.h检查函数原型call to undefined function mysqli_connect()PHP未启用 mysqli 扩展检查php.ini确认扩展已加载Undefined function or method optimoptionsMATLAB缺少工具箱或拼写错误安装对应工具箱检查函数签名DirectX function Present failedDirectX设备/驱动异常资源冲突检查显存、驱动版本、重置链状态TypeError: ... is not a functionJavaScript变量不是函数或调用时机错误打印变量类型检查导入/初始化顺序TypeError: unexpected keyword argumentPython函数没有该关键字参数查看函数签名修正参数名uncaught error: call to undefined function各类依赖缺失/命名空间错误确认 require/import/依赖加载cannot find symbol/找不到函数Java编译路径缺类或方法检查依赖和类路径确认方法签名这张表的核心用途不是让你背下来而是帮你建立一种直觉遇到函数相关报错先分类再排查。分类分对了一半问题已经解决了。5.3 我踩过的坑希望你跳过最后分享几个我真实踩过、后来形成条件反射的坑。第一个坑是命名相近导致调用错函数。有一次我把一个工具类里getUserId和getUserIds返回列表弄混了编译器没报错因为类型都是字符串/列表结果业务跑出了诡异数据。当时查了两个小时最后发现就是少了个 s。从那以后我定了个规矩名字能区分就区分彻底禁止用单复数来区分不同行为。第二个坑是函数内部修改了入参对象。Java 里对象引用作为参数传入后函数内部改了对象字段外部引用同一个对象的代码全部受影响。当时我以为传进函数的只是值结果数据被污染。现在我的习惯是明确标注哪些函数会修改输入对象或者干脆传入不可变对象/副本。第三个坑是回调地狱里的匿名函数自引用。写递归的匿名函数时在表达式内部没法直接用函数名调用自己。JavaScript 里可以用arguments.callee但严格模式下禁用更好的做法是先命名函数再引用。这不是什么高深问题但第一次遇到时确实卡了我一阵现在写递归一定先命名。第四个坑是过度依赖高阶函数导致可读性下降。有一阵我特爱用函数的函数、组合再组合结果同事看不懂自己也难调。后来我明白函数式是工具不是教条。在团队协作里代码的可读性永远比炫技重要。如果一个高阶函数方案需要别人花十分钟才能看懂而命令式三分钟能看懂我选命令式。函数这个东西说到底是人类组织计算的一种方式。你把它理解成输入到输出的映射很多语法细节会自然串起来你把调用时的报错当成对这个映射的误解排查问题就有了方向你把函数当成可传递、可组合的零件设计代码的眼光就会上一个台阶。我个人体会最深的一点是写函数时多想想三个月后的自己和同事他们拿着你的函数时会不会由衷地说一句这段代码真清楚。如果会那你就真的理解 Function 了。