资讯动态

方法(函数)入门:从语法到设计的编程分水岭

发布时间:2026/10/14 7:46:39 来源:尧图企业网站定制
“方法函数”这个标题在几乎所有编程教材里都占据整整一章而且位置都差不多语法入门之后、对象和类之前。我第一次学这一章时内心毫无波澜觉得函数不就是把几行代码包在一起、起个名字、随时调用吗后来真正写业务、维护老项目、带新人才慢慢意识到这一章是整个编程学习的分水岭函数没吃透后面看对象、看异步、看架构都会隔一层函数想明白了很多代码在你眼里会从“语法列表”变成“设计语言”。这篇文章就拿“第05章-方法(函数)”当出发点把函数背后那些教材里很少展开的东西梳理一遍为什么非要有函数、函数和方法到底怎么区分、声明和调用的几种写法有什么差别、参数和作用域如何影响执行结果、回调与异步怎么一步步上手以及我在实际环境里踩过的那些跟函数相关的坑。适合刚学到这一章的初学者通读也适合写了几年代码、想回头把基础磨一磨的从业者当查漏补缺的笔记。因为方法本身的写法在不同语言里略有差异下文主要以 JavaScript 为例展开但核心思想对 Python、Java、C、SQL 同样适用。1. 函数到底在解决什么问题先理解设计动机1.1 没有函数的世界重复、混乱、难维护很多教程上来就讲声明和调用却很少解释“为什么要有函数”。我习惯用做菜来类比假如你的店里每天要出十份同样的套餐你不会在每一张订单后面把切菜、炒菜、摆盘的整套动作重新写一遍而是把标准做法固定成流程来一单就照着执行一单。函数就是代码里的标准流程。写代码时如果不用函数同一个逻辑就得复制粘贴多遍。比如要计算圆的面积三处用到就写三遍Math.PI * r * r一旦需求改成“面积保留两位小数”你得挨个找到三处改三遍漏掉一处就是线上 bug。用函数把“计算圆面积”抽象出来只写一次参数是半径返回值是面积所有调用点统一走这一个入口需求变更时只改函数内部。这个思想在工程里有个专门的名字DRYDont Repeat Yourself不要重复自己。函数是最基础、最直接的 DRY 实现手段。我见过不少新人写代码函数倒是创建了但一个大函数里塞了几百行内部还在重复处理相似逻辑那其实等于把“复制粘贴”换成了“复制粘贴进函数里”。真正的函数化是让每个函数只做一件事并且这件事能被一个清晰的名字概括。1.2 “函数”和“方法”的区分同一个概念的两个名字先回答一个很多人纠结过的问题为什么有的书叫函数function有的叫方法method这两个词有时候混着用但在严格语境下有区别。我的理解很直接函数是独立的输入参数、输出结果方法是绑在某个对象上的函数它不仅能处理你传进来的参数还能访问所属对象自己的数据。以 JavaScript 为例parseInt(42)是函数调用你不依赖任何对象直接调用而arr.sort()是方法调用它悄悄读取并修改了arr这个数组对象内部的元素。C 语言没有对象的概念所以 C 里只有函数、没有方法Java、Python 这类面向对象语言里定义在类里面的函数就习惯性地叫方法。这个区分不是文字游戏。它影响你查文档的方式查函数时关心的是“给我什么参数、返回什么结果”查方法时还要多关心一个“这个方法会改变对象自身吗、还是返回一个新对象”。数组的sort会改变原数组而字符串的toUpperCase会返回新字符串二者差异极大搞反了就会出现“我怎么调用完原数据没了”的怪象。1.3 黑盒思维不需要知道函数内部怎么实现函数一个很实用的特性是“黑盒”。调用方只需要知道函数名、参数、返回值不需要关心内部实现。就像你用遥控器换台不需要知道电视信号怎么解码。这个特性的价值在于分工和替换。一个函数内部实现可以优化、整改、甚至彻底重写只要对外接口不变调用方完全无感。性能优化时把慢函数内部的算法换成更快的版本所有调用点自动受益重构时把大函数拆成若干小函数只要公开的函数签名没破坏测试也不用大改。这也是函数式编程思想的基础之一把系统拆成一个个纯函数每个函数输入输出明确、不依赖外部状态组合起来就形成可靠的整体。我在实际项目里更看重“纯函数”这个特质后面第 5 章还会说到它和异步、回调的关系。初学阶段至少要先养成“函数内部尽量别乱改外部变量”的习惯否则一个函数运行完整个程序状态莫名其妙变了排查起来极其痛苦。2. 声明、调用与参数先把基本姿势摆对2.1 三种常用声明方式与提升机制的区别以 JavaScript 为例函数至少有三种声明方式实际项目中你大概率都会遇到。只掌握一种会觉得别人代码看不懂所以每种都得能看懂// 方式一函数声明 function greet(name) { return Hello, name; } // 方式二函数表达式 const greet function(name) { return Hello, name; }; // 方式三箭头函数 const greet (name) Hello, name;三种写法最关键的差异是“提升”hoisting。方式一存在函数声明提升把调用写在声明之前代码也不会报错。方式二和方式三不行const声明的变量在初始化之前不能访问调用会报“无法在初始化之前访问”。这个差异在团队协作里常表现为两类问题有人习惯把工具函数全部堆在文件底部主逻辑写在前面用函数声明就没有影响有人用const fn () {}风格却在文件上部提前调用结果报错。两种情况我都见过没有谁对谁错关键是要记住声明风格决定了使用位置。箭头函数除了语法更短还有个重要特点没有自己的this会继承定义时所在的作用域。处理回调、事件监听时非常省事避免传统写法里var self this这类补丁。初学者可以把箭头函数先当成“精简版写法”来用学到this指向时再体会它的深层优势。2.2 参数数量、默认值与 return 的细节函数直观上是“输入-处理-输出”输入是参数输出是return。但这里有好几个容易踩的细节。第一参数数量不一定对得上。JavaScript 里你定义两个参数调用时传三个也不会报错多余的会被忽略只传一个也行缺的参数是undefined。这种灵活性多数时候是便利但容易埋雷少传一个参数时函数不会立刻报错而是带着undefined继续跑最后得出一个奇怪的结果。所以很多语言选择了更严格的方式Java 会在编译期直接报错Python 会用默认参数补齐TypeScript 严格模式也会提示参数不匹配。第二默认参数能省很多判断。比如封装一个请求工具function fetchData(url, timeout 3000) { // 调用方没传 timeout 时自动用 3000 }这样就不用在函数里写一堆if (timeout undefined)的防御代码。Python 也有类似写法def fetch_data(url, timeout3000):思路完全一样。第三return之后不会再执行任何代码。我在代码评审里经常看到“return一个值之后又写了一大段逻辑”的情况那一段永远不会执行新人往往要过几天才能意识到。编辑器其实会给出“unreachable code”之类的提示只是新手不太注意。一个函数里如果有多个return记得每个分支都要有明确出口不然容易出现“有时返回了值、有时返回undefined”的诡异行为。2.3 不同语言里的函数写法对照函数在主流语言里的骨架一致函数名、参数表、函数体、返回值。但细节不同写多了就不觉得乱。C 语言里int add(int a, int b) { return a b; }Python 里def add(a, b): return a bJava 里多数是挂在类上的静态方法或实例方法public static int add(int a, int b) { return a b; }还有一个特殊函数值得留意C 语言的main函数标准签名是int main(int argc, char *argv[])argc是命令行参数个数argv是参数字符串数组。它是程序统一入口函数由系统在启动时调用。也就是说main也是一个函数只是调用者是运行时环境而已。想通这一点对理解“函数是被谁调用的”很有帮助——不是你写了一个函数它就执行必须有人调用它才会跑。3. 参数传递与作用域为什么函数内部改不掉外面的变量3.1 值传递与引用传递复印件和原件的关系这是面试高频点也是日常排查 bug 的关键。用生活例子值传递是交复印件你改复印件不影响原件引用传递是给你一个原件地址你按地址去改原件内容跟着变。let x 10; function change(val) { val 20; } change(x); console.log(x); // 输出 10说明外部变量没变数字、字符串、布尔这类基础类型基本都是值传递函数内部怎么赋值都不会影响外部变量。数组、对象这类引用类型就不一样let arr [1, 2, 3]; function addItem(list) { list.push(4); } addItem(arr); console.log(arr); // 输出 [1, 2, 3, 4]原数组被改了arr本身没有变但函数通过引用修改了它指向的内存内容。想避免这种副作用常见做法是在函数内部先拷贝再操作例如list.slice()或者{...obj}展开。我遇到过很多次“传一个对象进函数函数跑完数据莫名其妙变了”的困惑尤其在后端接口处理和前端状态管理里这是高频坑。所以设计函数时最好明确声明这个函数会不会修改传入的对象如果会函数名和注释里要写清楚比如sortInPlace就比sort更直白。如果不会内部先拷贝。这点理解透了很多团队协作里的“我的数据怎么被动过”问题能减少大半。3.2 作用域函数里的变量从哪来函数体内部的变量默认只在函数内可见这叫局部作用域函数外面定义的叫全局变量JavaScript 里还有用let/const形成的块级作用域。我见过最多的问题是函数里不小心用了跟全局变量同名的变量又漏写了生命关键字导致意外创建了隐式全局变量。轻则是函数内逻辑出错重则是全局状态被污染别的地方跟着出问题。解决办法很简单函数里的变量一律显式用let/const声明全局变量尽量少用能传参就传参不要共享状态。Python 里如果你在函数里给全局变量赋值需要先写global声明否则 Python 会认为你在创建新局部变量——这经常让初学者困惑但本质上就是作用域规则在起作用。理解作用域还有个实用价值看错误提示。报错“某个变量 not defined”时第一反应不是看那个变量拼没拼错而是先确认它是在哪个作用域创建的、当前函数能不能访问到。排查路径能缩短很多。3.3 闭包函数记住了自己的出生环境闭包这个概念劝退过很多人但我发现用一句话讲反而最好懂闭包就是一个函数记住了它定义时所在作用域里的变量哪怕那个作用域已经执行完毕。典型例子是计数器function createCounter() { let count 0; return function() { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2count不是全局变量但每次执行返回的匿名函数都能访问到它。这个匿名函数就是闭包。它的实用场景很多事件处理函数里保存状态、封装私有变量、实现缓存。但闭包也有代价被闭包引用的变量不会被垃圾回收滥用会占内存。如果发现应用越跑越卡、内存只升不降可以排查是否有无意识形成的闭包把大对象长期攥着不放。闭包和“方法”的关系也值得提一句对象的方法本质上也是一个函数它访问的“成员变量”就是其所属对象的数据。从这个角度看方法就是一种自带环境的闭包。4. 内置函数与常用方法不要重复造轮子4.1 数学与类型转换函数不少初学者热衷于自己实现abs、开方、字符串转整数的逻辑写出来费劲还容易出错。标准库里早就有一堆经过无数次验证的内置函数。以 JavaScript 为例开方用Math.sqrt(x)或x ** 0.5绝对值用Math.abs(x)字符串转整数用parseInt(42)判断是不是NaN用Number.isNaN(x)。Python 里对应的是math.sqrt、abs、int(42)、math.isnan。有个容易被忽视的技巧Number.isNaN(x)和isNaN(x)不完全是同一个函数。isNaN(abc)会先把字符串转成数字再判断返回trueNumber.isNaN(abc)不做转换直接看值是不是NaN返回false。很多“为什么我这个判断不对”的问题就出在这类细微差异上。用内置函数前先看一眼文档里的“转换规则”说明能在后面省下大量时间。4.2 数组与字符串的高频方法JavaScript 的数组方法是我这些年用下来觉得最值得记熟的一块尤其是三个“三板斧”map、filter、reduce。const prices [100, 200, 300, 400]; // map每个元素映射成新值不改变原数组 const withTax prices.map(p p * 1.1); // filter筛选出符合条件的元素 const expensive prices.filter(p p 250); // reduce把数组归并成一个值 const total prices.reduce((sum, p) sum p, 0);map对每个元素做转换filter按条件筛选reduce做归约累加。这三个方法本身就是函数式编程的核心也是“高阶函数”的典型例子——它们接收的参数是函数。业务逻辑里“列表转名称数组”“筛选有效数据”“求和/求平均”几乎都能用它们一行搞定比 for 循环加临时变量清晰得多。字符串也很常用split按分隔符切分join按分隔符合并substring/slice截取片段。很多数据清洗工作都离不开这几个。我的建议是不要试图记完所有方法而是把map、filter、reduce、split、join、slice这六个用熟遇到其他需求直接查文档。4.3 数据库与数据处理中的函数函数的概念不止在编程语言里SQL 里也到处是函数而且非常常用。聚合函数SUM、COUNT、AVG负责统计日期函数负责格式化时间JSON 函数负责从 JSON 字段里取数据。我之前做运营后台时经常写类似这样的查询SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders WHERE created_at DATEADD(day, -7, GETDATE()) GROUP BY user_id;COUNT和SUM是聚合函数DATEADD和GETDATE是日期函数。如果不用这些函数业务逻辑就得在应用层里写循环统计慢且易错。SQL 里的函数能下推给数据库执行性能比把全部数据捞到内存里处理高一个量级。不同数据库的函数名差异较大比如字符串拼接 MySQL 用CONCATPostgreSQL 可以用||运算符Oracle 里用||也更通用。写 SQL 时“先查一下当前数据库的函数文档”不是丢人的事是常规操作。5. 进阶视角回调、异步方法与函数式思维5.1 回调函数把函数当作参数传递普通函数的参数是数字、字符串、对象但函数本身也可以作为参数传递这种被当作参数传进去的函数就叫回调函数。最常见的场景是事件监听和定时器button.addEventListener(click, function() { console.log(点击了按钮); }); setTimeout(function() { console.log(一秒之后执行); }, 1000);addEventListener接收一个函数当用户点击时这个函数才被调用。回调的本质是“把一段行为延迟到某个时机再执行”。这种能力让代码更灵活你不需要控制事件何时发生只需要告诉系统“发生了就做这件事”。回调函数也是理解异步方法的前提。很多人觉得异步难其实只要接受一个事实就行函数不一定同步执行它可以被当作值保存、传递等到合适时机再由调用方触发。想通这点回调、Promise、async/await 本质上就是不同的“延迟执行”的包装方式。5.2 异步方法的演变从回调到 Promise 再到 async/await异步这个词经常出现在各种框架的特性说明里但当它和函数放在一起时指的其实是“异步方法”或“异步函数”。JavaScript 里异步方法经历了几个阶段理解这个演进过程能少走很多弯路。最早是回调风格。请求数据成功后在回调里处理结果fetchData(/api/user, function(user) { console.log(user); });问题在于多个请求有先后依赖时回调套回调代码层层缩进很快就变成“回调地狱”fetchData(/api/user, function(user) { fetchData(/api/user/orders, function(orders) { fetchData(/api/order/detail, function(detail) { // 三层嵌套已经让人头疼 }); }); });后来 Promise 出现了把嵌套拍平fetchData(/api/user) .then(user fetchData(/api/user/orders)) .then(orders fetchData(/api/order/detail)) .then(detail { console.log(detail); }) .catch(err console.error(err));再后来有了async/await写法更像同步代码读起来最舒服async function showOrderDetail() { try { const user await fetchData(/api/user); const orders await fetchData(/api/user/orders); const detail await fetchData(/api/order/detail); console.log(detail); } catch (err) { console.error(err); } }await只能在async函数里用这是我经常提醒新人的一个规则看到keyword: async的函数就代表这个函数内部可能含有异步操作。异步函数返回的不一定是普通值而是一个 Promise 对象。这个“函数返回另一种对象”的特性恰好又回到我们说的“函数是值”异步函数也只是一个返回 Promise 的函数没有更多玄机。5.3 高阶函数与工厂方法函数可以生成函数函数不只能作为参数传递还能作为返回值返回。能接收函数或返回函数的函数称为高阶函数。JavaScript 里最常见的例子是防抖debounce函数它接收一个普通函数返回一个被包装后的新函数function debounce(fn, delay 300) { let timer null; return function(...args) { clearTimeout(timer); timer setTimeout(() fn.apply(this, args), delay); }; } const handleInput debounce(function() { console.log(输入完毕); });调用handleInput时实际执行的是防抖包装后的逻辑频繁触发只执行最后一次。这里的debounce就是典型的“函数工厂”——输入一个函数返回另一个函数。业务里处理搜索框、窗口 resize、按钮防连点时会非常方便。还有一个相关概念叫工厂方法常见于面向对象设计用一个函数来封装对象的创建逻辑而不是直接new一个类。比如根据参数不同返回不同类型的对象。工厂方法把“怎么创建对象”的细节收敛到一个函数内部调用方只关心“我要什么类型”不关心“它到底怎么被 new 出来”。这和函数的黑盒特性一脉相承。6. 常见报错、环境坑与排查习惯6.1 环境配置类报错cmdlet 与 PATH 问题学函数时很多人会先照着教程安装环境、执行命令行然后被一类和函数本身无关的报错拦住。最常见的是npm无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这个报错并不是 npm 这个函数有问题而是系统终端找不到 npm 这个可执行程序。本质是环境变量 PATH 里没有包含 npm 所在目录。绝大多数情况下是因为安装 Node.js 时没有勾选“自动添加到 PATH”选项或者安装后没有重开终端。不只是 npm很多命令行工具报“无法将某某识别为 cmdlet”都是同一个原因。解决办法通常是找到 node.exe 所在目录比如C:\Program Files\nodejs\把它加到系统环境变量的 Path 里然后重新打开终端。如果嫌手动配置麻烦重新运行 Node.js 安装包选择“修复并添加 PATH”的选项也可以。这类问题排查思路可以复用到任何命令行工具上遇到“无法识别”先确认那个程序装没装再确认它的可执行文件路径在不在 PATH 里最后确认终端是否重启过。6.2 函数相关报错排查速查表函数日常使用中还会遇到几种高频报错我把它们整理成一张速查表排版好方便收藏报错现象问题本质排查思路调用函数提示 XX is not a function变量类型不是函数可能被覆盖或未正确赋值用 console.log 打印变量看它到底是什么变量未定义 not defined变量作用域不对或者变量声明位置有问题检查变量是否在函数内可访问拼写是否正确无法在初始化之前访问使用了 const/let 声明却提前调用把调用移到声明之后或用函数声明方式函数返回 undefined漏写 return或者分支没有返回值检查每个分支是否有 return 语句参数数量不匹配报错语言编译期检查严格或需要默认值补充默认参数或检查调用方传参循环里用闭包拿到旧值循环变量被闭包共享改用 let 声明循环变量或封装函数传参异步函数返回的是 Promise 而不是结果忘了 await检查是否在 async 函数中使用了 await最后一行很常见新手写异步代码忘记await直接拿返回值打印出来是一个 Promise 对象数据怎么都取不到。看到“函数返回的不是我想要的值”时先想一下这个函数是不是异步函数。还有一个值得提的“未来函数”类问题虽然这个词更多出现在数据处理和策略回测场景中但原理可以迁移到普通代码如果一个函数引用了“当下还不该知道的数据”那结果看起来漂亮本质上却是欺诈。在工程里这对应着隐式依赖外部状态的问题。写函数时尽量只依赖参数和明确的全局常量不要偷偷读取隐状态的变量函数的行为才可预测、可测试。6.3 让函数更好用的几个习惯函数写得多和写得好是两个概念。我自己写函数有几个习惯可能对你有参考价值。第一命名用动词开头越具体越好。getUserList、formatPrice、validateEmail这类命名一眼能看出函数在做什么。尽量不要用doProcess、handleData这种泛泛的名字。函数命名差代码读起来就像看没有目录的书只能逐条翻。第二函数尽量短小单一。一个函数最好只做一件事超过二三十行就考虑拆分。拆分不是机械切块而是按“子任务”切。比如发送邮件这个函数可以拆成“组装邮件内容”和“调用发送接口”两个小函数。好处是出问题时定位很快测试也好写。第三随手给函数写注释但注释解释“为什么”不解释“是什么”。代码本身能表达“是什么”注释讲“为什么这里要传空数组作为初值”“为什么超时时间设成 3000”才有价值。我在交接老项目时最崩溃的就是遇到函数名清楚但内部逻辑毫无注释的代码每一步都能看懂、但完全不知道当初为什么这么写。第四写函数时顺手考虑异常分支。参数传了空数组怎么办对象缺了字段怎么办网络请求失败怎么办函数里加一点防御性判断比调用方在外部到处加 try/catch 要干净。但也要注意别过度防御把所有可能的问题都挡在函数内部会让函数很臃肿——平衡点在于这个函数是内部工具函数还是对外接口对外接口更值得多写防御逻辑。第五重视函数测试。不求全面覆盖但核心工具函数至少要有几个测试用例。比如金额格式化、日期转换、状态判断这类函数写几个断言能让你在重构时心里有底。手动验证三次不如一个自动测试永远守护着它。最后分享一个我最近用得很顺的小技巧写新函数前先在浏览器控制台或代码里用几组模拟数据跑一遍看看输入输出是否符合预期。这个习惯花不了几分钟却能避开大量低级 bug。函数本身不复杂复杂的是调用环境。把函数当作一个可被验证的单元来对待你的代码会一步步变得更可靠。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑