
函数的定义与调用:从基础语法到实际用法
函数是把一段操作组织成可重复调用的代码单元。遇到需要复用的逻辑、希望把复杂任务拆成步骤,或需要把处理过程交给回调执行时,函数都很有用。理解函数,不只是记住一对括号:还要知道参数如何传入、返回值如何产生、变量在哪些范围内可见,以及输入不符合预期时如何处理。
本文以 JavaScript 为例。示例可以在现代浏览器的开发者工具控制台或 Node.js 中运行。函数语法在不同语言间有所差异,因此不要把本文中的 JavaScript 规则直接套用于其他语言。
一、定义函数与调用函数
定义函数,是声明或创建一段可执行的逻辑;调用函数,则是通过函数名和圆括号让这段逻辑运行。下面用函数声明定义一个问候函数:
function greet(name) {
return `你好,${name}!`;
}
const message = greet("小林");
console.log(message);
这里,function 是关键字,greet 是函数名,name 是形参,函数体位于花括号中。调用时传入的 "小林" 是实参。函数执行到 return 时结束,并把后面的值交还给调用方;调用结果被保存在 message 中。
如果没有写 return,或者执行到函数末尾仍未返回,函数的返回值就是 undefined:
function logStatus() {
console.log("正在处理");
}
const result = logStatus();
console.log(result); // undefined
console.log 负责输出信息,return 负责把结果交还给调用方,两者不能混为一谈。需要计算结果供后续代码使用时,应明确返回值。
二、常见的函数定义方式
1. 函数声明
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
函数声明有一个实用特性:在同一作用域内,可以在声明语句之前调用它。这是因为 JavaScript 会在执行代码前处理函数声明。不过,为了便于阅读,通常仍建议先定义再调用。函数声明适合表示一个有名称、会在当前作用域中复用的操作。
2. 函数表达式
函数也可以作为值赋给变量:
const multiply = function (a, b) {
return a * b;
};
console.log(multiply(4, 5)); // 20
这里的函数表达式赋给了常量 multiply。调用方式仍然是变量名加圆括号。和函数声明不同,变量必须先完成初始化,才能安全调用;在初始化语句之前使用 multiply 会出错。
3. 箭头函数
箭头函数提供了更简洁的写法,常用于短函数、回调和数组操作:
const square = (number) => {
return number * number;
};
console.log(square(6)); // 36
只有一个参数时可以省略参数括号;函数体只有一个表达式时,可以省略花括号和 return,形成隐式返回:
const double = number => number * 2;
const add = (a, b) => a + b;
console.log(double(7)); // 14
console.log(add(3, 8)); // 11
省略写法简洁,但复杂逻辑最好使用花括号和显式 return,以免读者误判返回结果。箭头函数也不是普通函数的完全替代品:它没有自己的 this 和 arguments,不能用作构造函数。需要通过 new 创建实例时,应使用普通函数或类。
三、参数、默认值与实参数量
形参是函数定义中列出的参数名称,实参是调用时实际提供的值。JavaScript 调用函数时,不会因为实参数量与形参数量不一致就自动报错。
function describe(name, city) {
return `${name}来自${city}`;
}
console.log(describe("阿青")); // 阿青来自undefined
console.log(describe("阿青", "杭州", "多余的值")); // 阿青来自杭州
调用方少传的参数会得到 undefined;多传的实参通常不会报错,但若函数体没有读取它们,它们也不会产生预期效果。依赖固定输入时,应检查参数或提供合理默认值。
默认参数
可以在形参位置为参数设置默认值。当对应实参省略或传入 undefined 时,默认值会生效:
function createLabel(text, prefix = "提示") {
return `${prefix}:${text}`;
}
console.log(createLabel("操作成功")); // 提示:操作成功
console.log(createLabel("操作成功", "结果")); // 结果:操作成功
传入 null 不会触发默认值,因为 null 是一个明确提供的值:
function show(value = "默认值") {
return value;
}
console.log(show(undefined)); // 默认值
console.log(show(null)); // null
因此,如果业务上也要把 null 视为“未提供”,需要自行判断。默认参数适合表达可选配置;对于字段较多、含义复杂的输入,使用对象参数通常更清晰。
使用对象参数表达多个选项
function formatPrice({ amount, currency = "CNY" } = {}) {
if (typeof amount !== "number" || !Number.isFinite(amount)) {
throw new TypeError("amount 必须是有限数字");
}
return `${currency} ${amount.toFixed(2)}`;
}
console.log(formatPrice({ amount: 19.9 })); // CNY 19.90
console.log(formatPrice({ amount: 19.9, currency: "USD" })); // USD 19.90
参数解构能让调用方用属性名表达意图,也方便新增可选项。上例中 = {} 用于处理整个参数被省略的情况,否则对 undefined 做对象解构会抛出异常。字段的类型和取值仍需要按业务要求校验。
剩余参数
当函数需要接收数量不固定的实参时,可以使用剩余参数 ...values。它会把剩余实参收集为数组:
function sum(...values) {
return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
剩余参数必须位于参数列表末尾,而且每个函数只能有一个剩余参数。它不同于已经废弃、语义也不同的旧式 arguments 用法。箭头函数没有自己的 arguments,需要接收可变数量的参数时,应使用剩余参数。
四、参数数量的边界
JavaScript 语法没有规定一个适用于所有运行环境的、小而固定的最大实参数量。函数属性 function.length 也不是调用上限,它主要反映第一个带默认值参数之前的形参数量;剩余参数不计入其中。
function example(a, b = 1, ...rest) {}
console.log(example.length); // 1
但“语言没有统一的小上限”不代表可以放心传入任意数量的参数。具体 JavaScript 引擎对调用参数数量、栈空间等资源存在实现限制,过大的调用可能失败;把超大数组展开成实参也可能触及这些限制。限制取决于运行环境,不应假设某个固定数字在所有浏览器和 Node.js 版本中都适用。
对于大量数据,通常直接把数组作为一个参数传入,而不是把每个元素展开成独立实参:
function average(numbers) {
if (!Array.isArray(numbers) || numbers.length === 0) {
throw new TypeError("numbers 必须是非空数组");
}
const total = numbers.reduce((sum, value) => sum + value, 0);
return total / numbers.length;
}
console.log(average([2, 4, 6])); // 4
要注意,这个例子为了简洁假设数组元素可参与加法;实际程序还应检查每个元素是否为有限数字。使用数组作为批量数据容器,通常比把大量值塞进函数参数列表更易读、更易维护。
五、返回值与执行流程
函数可以只执行操作,也可以计算并返回结果。一个函数可以有多个 return,但一次调用只会执行其中一条实际到达的返回语句:
function classifyScore(score) {
if (score < 0 || score > 100) {
return "分数超出范围";
}
if (score >= 60) {
return "及格";
}
return "未及格";
}
console.log(classifyScore(85)); // 及格
return 会立即结束当前函数。把它写在条件判断之后且没有花括号,可能造成与预期不同的控制流。不要在同一行换行后再写返回表达式,例如 return 与返回值之间的换行可能触发自动分号插入,导致函数实际返回 undefined:
function broken() {
return
10;
}
console.log(broken()); // undefined
应把返回值放在 return 之后的同一行,或用括号明确表达式。
JavaScript 函数只能直接返回一个值。若需要返回多个相关结果,可以返回数组或对象:
function getRange(numbers) {
return {
min: Math.min(...numbers),
max: Math.max(...numbers)
};
}
const range = getRange([3, 8, 2]);
console.log(range.min, range.max); // 2 8
上例中的展开参数适用于较小数组;若数组可能非常大,Math.min(...numbers) 可能因为调用参数数量限制而失败。可以改为循环遍历,避免将所有元素展开为实参。
六、作用域、局部变量与闭包
函数体可以访问所在作用域中可见的变量,也可以声明自己的局部变量。使用 let 和 const 声明的变量遵循块级作用域,函数内部声明的变量通常不能在函数外直接访问:
function calculate() {
const taxRate = 0.1;
return 100 * (1 + taxRate);
}
console.log(calculate()); // 110
// console.log(taxRate); // ReferenceError
尽量使用 const 声明不会重新赋值的变量,需要重新赋值时使用 let。避免无意间创建全局变量:在严格模式下,给未声明的变量赋值会报错;模块代码也处于严格模式。依赖全局状态会让函数难以测试,也容易引发不同代码之间的命名冲突。
函数可以“记住”定义时可访问的变量,这种行为称为闭包。闭包常用于保存私有状态:
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
外部代码无法直接读取 count,但返回的内部函数仍可访问并修改它。闭包适合封装少量状态;如果状态共享关系复杂,应考虑更明确的数据结构或模块设计,避免难以追踪的隐式依赖。
七、函数作为值:回调与数组方法
JavaScript 中函数可以赋给变量、作为参数传递,也可以作为其他函数的返回值。传给另一个函数、由它在适当时机调用的函数,通常称为回调函数。
function runTwice(action) {
action();
action();
}
runTwice(() => {
console.log("执行一次");
});
数组方法也广泛使用回调:
const prices = [12, 25, 40];
const withTax = prices.map(price => price * 1.1);
const affordable = prices.filter(price => price < 30);
console.log(withTax); // [13.2, 27.500000000000004, 44]
console.log(affordable); // [12, 25]
map 通常用于把每一项转换为新值,filter 用于按条件筛选。它们会返回新数组,不会因调用本身而修改原数组。不过,回调若修改了对象内部属性,仍可能影响共享对象;“返回新数组”不等于深拷贝。
常见错误是把函数的执行结果当成函数本身传入。例如,setTimeout(task, 1000) 是把函数交给定时器稍后调用;setTimeout(task(), 1000) 则会立刻执行 task,再把它的返回值传给定时器。
八、异步函数与调用结果
函数也可以执行异步操作。async 函数总是返回一个 Promise:显式返回的普通值会成为已兑现的 Promise 结果;函数中抛出的异常会成为 Promise 的拒绝结果。
async function getMessage() {
return "任务完成";
}
getMessage().then(message => {
console.log(message); // 任务完成
});
在另一个 async 函数中,可以使用 await 等待 Promise 结果:
async function loadExample() {
const response = await fetch("https://example.com/data.json");
if (!response.ok) {
throw new Error(`请求失败:${response.status}`);
}
return response.json();
}
loadExample()
.then(data => console.log(data))
.catch(error => console.error(error));
此示例需要网络可用,且目标服务器提供相应资源;它不是离线即可保证成功的示例。实际代码应处理网络失败、HTTP 错误和数据格式错误。fetch 遇到 HTTP 404 等状态时通常仍会兑现 Promise,因此需要检查 response.ok。未处理的 Promise 拒绝可能导致错误难以定位。
九、输入校验与安全边界
函数参数只是调用方提供的输入,并不会因为写在形参列表中就自动变得可信。参数可能来自用户表单、网络请求、文件或其他不受信任的来源。需要根据函数职责验证类型、范围、格式和业务约束,并对无效输入选择明确行为:返回错误结果、抛出异常,或按接口约定进行处理。
function divide(dividend, divisor) {
if (
typeof dividend !== "number" ||
typeof divisor !== "number" ||
!Number.isFinite(dividend) ||
!Number.isFinite(divisor)
) {
throw new TypeError("参数必须是有限数字");
}
if (divisor === 0) {
throw new RangeError("除数不能为零");
}
return dividend / divisor;
}
console.log(divide(12, 3)); // 4
这类校验能让函数的输入契约更清楚,也能减少错误数据进入后续计算的机会。但参数校验不是通用的安全证明:应按具体场景处理权限、长度、编码、格式和业务规则,也不能只依赖前端校验。服务端仍要对来自客户端的请求进行验证。
不要把字符串当作代码执行。例如,不要使用 eval 或 new Function 来运行用户提交的内容。这样做可能导致不可信输入在当前环境中执行,造成代码注入风险。需要解析数据时应使用合适的数据格式解析器(例如 JSON),而不是将数据拼接成可执行代码。
同样,函数接收参数并不意味着它自动抵御 SQL 注入、跨站脚本或命令注入。构造数据库查询时应使用参数化查询;把内容插入 HTML 时应依据输出上下文进行安全编码或采用安全的 DOM API;执行系统命令时应尽量使用安全的参数接口,避免拼接不可信字符串。安全措施要落实在对应的边界和 API 上,不能指望一个通用函数或一条类型检查解决所有问题。
十、常见错误与排查方法
- 把定义和调用混淆:
function greet() {}是定义,greet()才是调用。只有定义而从未调用,函数体不会运行。 - 忘写括号:把
greet传给回调是传函数;把greet()传过去通常是先调用函数,再传它的返回值。 - 忘记返回:函数中打印了结果,不代表调用方能获得结果。需要供后续计算使用时,写出
return。 - 假设参数会自动校验:JavaScript 不会因为参数名写成
age就保证传入值是合理年龄。检查类型、范围和业务规则。 - 在箭头函数中使用自己的
this:箭头函数不绑定自己的this。对象方法若需要根据调用对象访问属性,通常使用方法简写或普通函数,并留意调用方式。 - 对参数数量作不可靠假设:不要认为函数的形参数量就是运行时的硬性限制,也不要把超大数组展开成调用参数。大量数据应使用数组或对象传递。
- 忽略异步返回值:
async函数返回 Promise,直接把它当作最终数据使用会得到 Promise 对象。应使用await或.then()取得结果,并处理拒绝情况。
十一、实际设计函数时的检查清单
- 给函数起能表达动作或结果的名称,避免只用
doThing一类含糊名字。 - 明确输入和输出:参数代表什么,哪些可选,返回值是什么,错误如何表现。
- 保持职责集中。一个函数如果同时校验数据、读写文件、修改页面并发送请求,通常难以测试和复用;可以按职责拆分。
- 对外部输入进行适当校验,但避免重复、无意义的检查。验证应与函数的信任边界和业务契约相符。
- 避免不必要的全局状态和参数副作用。若函数会修改传入对象或数组,应让调用方容易看出来。
- 给异常和异步失败留出处理路径。调用方需要知道失败时是收到错误值、异常还是拒绝的 Promise。
函数的核心用法可以归纳为:定义一段有明确职责的逻辑,通过实参提供输入,以返回值或约定的副作用交付结果。熟悉声明、表达式、箭头函数、默认参数、剩余参数和返回值之后,再把作用域、回调、异步行为与输入校验纳入考虑,就能写出更容易复用、测试和维护的代码。









