调用函数
函数是一段可以按名称重复执行的代码。定义函数是在描述“要做什么”,调用函数才是真正让这段代码运行。
本文使用 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())
- 先计算函数位置(这里是
getName或greet); - 从左到右计算每个实参表达式;
- 创建本次调用的参数和局部变量;
- 执行函数体,遇到
return时结束; - 用返回值替换整个调用表达式。
实参表达式通常按从左到右的顺序求值:
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
遇到嵌套过深的表达式,先保存中间结果能显著提高可读性和调试效率。
不同语言的调用写法
“先得到函数,再传入实参并取得返回值”是跨语言的共同模型,括号和参数语法会略有不同:
| 语言 | 定义示例 | 调用示例 |
|---|---|---|
| JavaScript | function add(a, b) { return a + b; } | add(1, 2) |
| Python | def add(a, b): return a + b | add(1, 2) |
| Go | func add(a, b int) int { return a + b } | add(1, 2) |
Go 还可以一次接收多个返回值,Python 和 JavaScript 通常使用元组、数组或对象表达类似结果。具体规则应以所用语言的函数章节为准。
调用函数的检查清单
遇到一次函数调用,可以按下面顺序快速检查:
- 调用的是函数本身吗? 需要立即执行才写括号;传回调时通常只写函数名。
- 参数顺序和数量正确吗? 缺少参数会得到
undefined,多余参数未必会报错。 - 参数表达式有副作用吗? 它们通常从左到右求值,复杂逻辑建议拆开。
- 需要接收返回值吗?
console.log只是输出,未必提供可复用的结果。 - 调用上下文正确吗? 方法脱离对象后可能丢失
this,必要时使用bind。 - 这是异步调用吗?
async函数返回 Promise,需要await或.then(),并处理错误。 - 输入边界处理了吗? 检查空值、类型、除零、递归终止条件等情况。
小结
函数名(...)会执行函数;函数名表示函数值本身。- 实参按位置匹配形参,默认参数、剩余参数和展开语法可以改善调用的灵活性。
return把结果交给调用方;没有return时结果是undefined。对象.方法()会影响this;new、call、apply和bind表示不同的调用语义。- 回调由其他函数在之后调用;
async函数调用后得到 Promise。 - JavaScript 的参数按值传递,传入对象时修改对象内容会影响调用方,应根据职责决定是否复制数据。