函数的定义与调用:从基础语法到实际用法

百科常识3小时前发布 jdkjadmin
0 00
函数的定义与调用:从基础语法到实际用法

函数的定义与调用:从基础语法到实际用法

函数是把一段操作组织成可重复调用的代码单元。遇到需要复用的逻辑、希望把复杂任务拆成步骤,或需要把处理过程交给回调执行时,函数都很有用。理解函数,不只是记住一对括号:还要知道参数如何传入、返回值如何产生、变量在哪些范围内可见,以及输入不符合预期时如何处理。

本文以 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() 取得结果,并处理拒绝情况。

十一、实际设计函数时的检查清单

  1. 给函数起能表达动作或结果的名称,避免只用 doThing 一类含糊名字。
  2. 明确输入和输出:参数代表什么,哪些可选,返回值是什么,错误如何表现。
  3. 保持职责集中。一个函数如果同时校验数据、读写文件、修改页面并发送请求,通常难以测试和复用;可以按职责拆分。
  4. 对外部输入进行适当校验,但避免重复、无意义的检查。验证应与函数的信任边界和业务契约相符。
  5. 避免不必要的全局状态和参数副作用。若函数会修改传入对象或数组,应让调用方容易看出来。
  6. 给异常和异步失败留出处理路径。调用方需要知道失败时是收到错误值、异常还是拒绝的 Promise。

函数的核心用法可以归纳为:定义一段有明确职责的逻辑,通过实参提供输入,以返回值或约定的副作用交付结果。熟悉声明、表达式、箭头函数、默认参数、剩余参数和返回值之后,再把作用域、回调、异步行为与输入校验纳入考虑,就能写出更容易复用、测试和维护的代码。

© 版权声明

相关文章