跳到主要内容

调用函数

函数是一段可以按名称重复执行的代码。定义函数是在描述“要做什么”,调用函数才是真正让这段代码运行。

本文使用 JavaScript(ES2022)示例。读完后,你应该能够:

  • 区分函数本身和函数调用结果;
  • 使用位置参数、默认参数、剩余参数和展开语法;
  • 正确处理返回值、默认值、剩余参数和异常;
  • 理解普通调用、方法调用、构造调用、回调调用和异步调用的差异。

先看一个最小例子​

function greet(name) {
return `你好,${name}!`
}

const message = greet('小林') // 调用函数
console.log(message) // 你好,小林!

greet 是函数值,greet("小林") 才是一次调用。调用时,字符串 "小林" 会传给参数 name,函数执行后返回一个字符串。

可以把调用过程记成:

函数名(实参) -> 函数执行 -> 返回结果

函数名后面的括号意味着什么​

有括号:立即调用​

greet('小林')

括号会触发调用。即使不需要参数,也要写空括号:

function showVersion() {
return '1.0.0'
}

showVersion()

没有括号:取得函数本身​

const action = greet
console.log(action('小周')) // 你好,小周!

action = greet 只是让两个变量引用同一个函数,不会执行它。这个区别在传递回调时尤其重要:

setTimeout(greet, 1000) // 传入函数,稍后由定时器调用
setTimeout(greet('小林'), 1000) // 先立即调用,再把返回值传给定时器

第二行通常不是想要的写法,因为 greet("小林") 的结果是字符串,而不是函数。

调用表达式如何执行​

一次调用可以拆成几个步骤:

const name = getName()
const message = greet(name.trim())
  1. 先计算函数位置(这里是 getName 或 greet);
  2. 从左到右计算每个实参表达式;
  3. 创建本次调用的参数和局部变量;
  4. 执行函数体,遇到 return 时结束;
  5. 用返回值替换整个调用表达式。

实参表达式通常按从左到右的顺序求值:

function mark(label) {
console.log(label)
return label
}

function combine(first, second) {
return `${first}-${second}`
}

console.log(combine(mark('first'), mark('second')))
// first
// second
// first-second

不要让实参依赖难以察觉的副作用;把复杂计算拆成独立语句通常更容易阅读和测试。

传递参数​

按位置传递​

参数按照出现顺序匹配:

function createUser(name, age) {
return { name, age }
}

createUser('小林', 18) // name = "小林", age = 18

调用处的值叫实参,函数定义中的占位名称叫形参。位置参数较少、顺序明确时最直观;参数很多时容易传错顺序。

参数不足或过多​

JavaScript 不会因为普通函数的参数数量不匹配就自动报错:

function add(left, right) {
return left + right
}

add(3) // 3 + undefined -> NaN
add(1, 2, 3) // 多出的 3 被忽略

缺少的参数值为 undefined。应使用默认参数或显式校验,避免错误悄悄传播。

默认参数​

参数没有传入,或传入 undefined 时,会使用默认值:

function greet(name = '朋友') {
return `你好,${name}!`
}

greet() // 你好,朋友!
greet(undefined) // 你好,朋友!
greet(null) // 你好,null!不会使用默认值

默认值只在对应值是 undefined 时生效;null 是一个明确传入的值。

剩余参数​

...numbers 会把剩余实参收集为数组,必须放在参数列表末尾:

function sum(first, ...numbers) {
return numbers.reduce((total, number) => total + number, first)
}

sum(1, 2, 3, 4) // 10

它比旧式的 arguments 更直观,并且是真正的数组,可以直接使用数组方法。

展开传参​

数组或可迭代对象前加 ...,可以把其中的元素展开为多个位置参数:

function addThree(a, b, c) {
return a + b + c
}

const values = [10, 20, 30]
addThree(...values) // 60

展开传参和剩余参数方向相反:

语法出现位置作用
function f(...args) {}函数定义收集多个实参
f(...values)函数调用展开多个实参

返回值​

return 把结果交给调用方​

function divide(total, count) {
if (count === 0) {
return null
}
return total / count
}

const result = divide(10, 2) // 5

调用结果可以继续参与运算、赋值、判断或再次传给函数:

console.log(Math.round(divide(7, 2))) // 4

没有 return 时返回 undefined​

function logMessage(message) {
console.log(message)
}

const result = logMessage('完成') // 控制台先输出“完成”
console.log(result) // undefined

不要把“打印结果”和“返回结果”混为一谈。需要让调用方继续使用数据时,应返回数据,而不是只调用 console.log()。

return 会立即结束当前函数​

function getLabel(score) {
if (score >= 60) {
return '及格'
}
return '不及格'
}

return 后面的代码不会执行。函数只返回一个值;若需要返回多个相关值,可以返回数组或对象:

function parsePoint(text) {
const [x, y] = text.split(',').map(Number)
return { x, y }
}

常见的调用形式​

普通函数调用​

function double(value) {
return value * 2
}

double(21)

使用 function 声明或箭头函数创建的函数,都可以用这种形式调用。

方法调用​

当函数作为对象属性时,通过 对象.方法() 调用:

const cart = {
items: [20, 30],
total() {
return this.items.reduce((sum, price) => sum + price, 0)
},
}

cart.total() // 50

这种形式会把调用点左侧的对象作为 this。把方法单独取出后,this 可能改变或丢失:

const total = cart.total
// total(); // 严格模式下 this 为 undefined,访问 this.items 会报错

需要保留对象上下文时,可以绑定 this:

const boundTotal = cart.total.bind(cart)
boundTotal() // 50

构造调用:new​

new 表示按照构造函数创建新对象:

function User(name) {
this.name = name
}

const user = new User('小林')
console.log(user.name) // 小林

new User(...) 会创建对象、把它设为构造函数中的 this,并返回这个对象。现代代码通常使用 class 或工厂函数,但调用时仍要区分 new 和普通括号调用。

显式指定调用上下文​

call 和 apply 都会立即调用函数,并显式指定 this:

function introduce(role) {
return `${this.name} 是${role}`
}

const person = { name: '小林' }

introduce.call(person, '开发者') // 小林是开发者
introduce.apply(person, ['开发者']) // 小林是开发者

两者的区别只有参数形式:call 接收一串参数,apply 接收一个参数数组。需要反复调用同一上下文时使用 bind,它返回一个尚未执行的新函数:

const sayRole = introduce.bind(person, '开发者')
sayRole() // 小林是开发者

立即执行函数​

函数表达式外加括号,可以在创建后立即调用:

;(function () {
const temporary = '只在这里可见'
console.log(temporary)
})()

现在更常用模块、块级作用域和普通函数来组织代码,因此不必为了“看起来封装”而滥用立即执行函数。

回调:把函数交给别人调用​

回调函数是作为参数传入、由其他代码在合适时机调用的函数:

const numbers = [1, 2, 3]
const doubled = numbers.map((number) => number * 2)

console.log(doubled) // [2, 4, 6]

map 负责遍历数组,箭头函数负责处理每个元素。已有函数签名匹配时,可以直接传函数名:

function isLongWord(word) {
return word.length >= 5
}

const words = ['Go', 'Dart', 'Python']
console.log(words.filter(isLongWord)) // ["Python"]

需要附加参数时,用箭头函数包一层,避免提前调用:

setTimeout(() => greet('小林'), 1000)

异步函数的调用​

声明为 async 的函数调用后总是返回 Promise,不能直接当作最终数据使用:

async function fetchName() {
return '小林'
}

const pending = fetchName()
console.log(pending instanceof Promise) // true

使用 await 等待结果,await 只能写在 async 函数中或支持顶层 await 的模块里:

async function main() {
const name = await fetchName()
console.log(name) // 小林
}

main().catch((error) => {
console.error('调用失败:', error)
})

多个互不依赖的异步调用可以并发发起,再一起等待:

const [first, second] = await Promise.all([fetchName(), fetchName()])

不要在循环中不加判断地顺序 await,也不要忘记处理 Promise 拒绝;具体并发方式应根据业务是否允许同时发起请求来决定。

参数是值,还是引用​

JavaScript 按值传递参数。对象参数传递的是“对象引用的副本”,所以函数可以修改对象内容,但不能通过重新绑定参数改变调用方变量的指向:

function updateProfile(profile) {
profile.level = 2 // 修改对象内容,调用方可看到
profile = { level: 3 } // 只改变局部参数的指向
}

const profile = { level: 1 }
updateProfile(profile)
console.log(profile) // { level: 2 }

如果函数不应修改传入对象,可以复制后再处理:

function withLevel(profile, level) {
return { ...profile, level }
}

错误与边界情况​

调用不存在的值​

只有函数值才能加括号调用:

const value = 42
// value(); // TypeError: value is not a function

调用前要确认变量确实保存了函数,或在 API 设计中用明确的类型约束减少这种错误。

函数名未定义​

// missingFunction(); // ReferenceError

拼写错误、导入路径错误或作用域不正确,都可能造成 ReferenceError。

递归调用要有结束条件​

函数可以调用自己,但必须在某个条件下停止:

function countdown(number) {
if (number <= 0) {
return
}
console.log(number)
countdown(number - 1)
}

countdown(3) // 3、2、1

没有结束条件会不断创建调用层级,最终触发栈溢出。

用括号包住复杂调用结果​

调用可以参与链式操作和运算,但要注意运算符优先级:

const label = getLabel(72).toUpperCase()
const total = getPrice() * quantity

遇到嵌套过深的表达式,先保存中间结果能显著提高可读性和调试效率。

不同语言的调用写法​

“先得到函数,再传入实参并取得返回值”是跨语言的共同模型,括号和参数语法会略有不同:

语言定义示例调用示例
JavaScriptfunction add(a, b) { return a + b; }add(1, 2)
Pythondef add(a, b): return a + badd(1, 2)
Gofunc add(a, b int) int { return a + b }add(1, 2)

Go 还可以一次接收多个返回值,Python 和 JavaScript 通常使用元组、数组或对象表达类似结果。具体规则应以所用语言的函数章节为准。

调用函数的检查清单​

遇到一次函数调用,可以按下面顺序快速检查:

  1. 调用的是函数本身吗? 需要立即执行才写括号;传回调时通常只写函数名。
  2. 参数顺序和数量正确吗? 缺少参数会得到 undefined,多余参数未必会报错。
  3. 参数表达式有副作用吗? 它们通常从左到右求值,复杂逻辑建议拆开。
  4. 需要接收返回值吗? console.log 只是输出,未必提供可复用的结果。
  5. 调用上下文正确吗? 方法脱离对象后可能丢失 this,必要时使用 bind。
  6. 这是异步调用吗? async 函数返回 Promise,需要 await 或 .then(),并处理错误。
  7. 输入边界处理了吗? 检查空值、类型、除零、递归终止条件等情况。

小结​

  • 函数名(...) 会执行函数;函数名 表示函数值本身。
  • 实参按位置匹配形参,默认参数、剩余参数和展开语法可以改善调用的灵活性。
  • return 把结果交给调用方;没有 return 时结果是 undefined。
  • 对象.方法() 会影响 this;new、call、apply 和 bind 表示不同的调用语义。
  • 回调由其他函数在之后调用;async 函数调用后得到 Promise。
  • JavaScript 的参数按值传递,传入对象时修改对象内容会影响调用方,应根据职责决定是否复制数据。