{T}

参数与返回值

函数的参数和返回值是函数与外部交互的重要接口,理解它们的特性对于编写高质量函数至关重要。本文档将详细介绍 JavaScript 中参数传递机制、参数特性、返回值处理以及相关最佳实践。

概述

参数与返回值的角色

在 JavaScript 函数体系中,参数和返回值承担以下核心职责:

  • 参数:函数的输入接口,用于接收外部数据
  • 返回值:函数的输出接口,用于向调用者传递处理结果
  • 接口契约:参数和返回值共同定义了函数的 API 契约

核心架构图

code
┌─────────────────────────────────────────────────────────┐
│                      函数调用过程                         │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  1. 参数传递阶段                                          │
│     • 实参求值(从左到右)                                 │
│     • 参数绑定(形参 = 实参值)                            │
│     • 默认参数处理                                         │
│     • arguments 对象创建                                   │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  2. 函数执行阶段                                          │
│     • 函数体代码执行                                       │
│     • return 语句处理                                     │
│     • 异常处理                                             │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  3. 返回值处理阶段                                        │
│     • 返回值计算                                           │
│     • 控制权交还给调用者                                   │
│     • 返回值传递                                           │
└─────────────────────────────────────────────────────────┘

函数参数

基本概念

javascript
// 函数声明时指定的参数称为形参(形式参数)
function add(a, b) {
  return a + b
}

// 函数调用时传入的参数称为实参(实际参数)
add(1, 2)  // 1 和 2 是实参

术语说明:

  • 形参(Parameter):函数定义时声明的变量名
  • 实参(Argument):函数调用时传入的具体值

参数传递方式 ⚠️

JavaScript 中所有参数都是按值传递的,这是理解参数行为的核心概念。

基本类型传参

传递的是值的副本,函数内部修改不影响外部变量:

javascript
function changeValue(x) {
  x = 10
  console.log('函数内部:', x)  // 10
}

let a = 5
changeValue(a)
console.log('函数外部:', a)  // 5,未改变

引用类型传参

传递的是引用的副本(内存地址),函数内部可以通过引用修改对象属性:

javascript
function changeObject(obj) {
  obj.name = 'changed'  // 修改引用指向的对象
}

let person = { name: 'original' }
changeObject(person)
console.log(person.name)  // 'changed'

// 但重新赋值不会影响原对象
function reassignObject(obj) {
  obj = { name: 'new' }  // 重新赋值,改变引用本身
}

reassignObject(person)
console.log(person.name)  // 'changed',未被重新赋值

参数传递机制详解

code
基本类型传参:
┌──────────┐          ┌──────────┐
│ 外部变量 │          │ 函数参数 │
│    a=5   │ ───────> │    x=5   │
└──────────┘  值复制   └──────────┘
   修改 x 不影响 a

引用类型传参:
┌──────────┐          ┌──────────┐
│ 外部变量 │          │ 函数参数 │
│  person  │          │   obj    │
│  ┌─────┐ │          │  ┌─────┐ │
│  │引用 │─┼──────────┼─>│引用 │ │
│  └─────┘ │  引用复制 │  └─────┘ │
└──────────┘          └──────────┘
      │                     │
      └──────────┬──────────┘
                 ▼
          ┌─────────────┐
          │ 共享的对象   │
          │ {name:'xx'} │
          └─────────────┘
    修改属性会影响原对象
    重新赋值不影响原引用

参数特性

1. 参数个数不匹配

JavaScript 对参数个数非常宽容,不会因为参数个数不匹配而报错:

javascript
function foo(a, b, c) {
  console.log(a, b, c)
}

foo(1)           // 1 undefined undefined
foo(1, 2)        // 1 2 undefined
foo(1, 2, 3)     // 1 2 3
foo(1, 2, 3, 4)  // 1 2 3(多余的参数被忽略)

注意: 未传入的参数值为 undefined,而不是 null 或其他值。

2. 默认参数值(ES6)

基本用法

javascript
// ES5 方式 - 使用逻辑或运算符
function greet(name) {
  name = name || 'Guest'
  console.log('Hello, ' + name)
}

// ES6 方式 - 使用默认参数
function greet(name = 'Guest') {
  console.log(`Hello, ${name}`)
}

greet()        // Hello, Guest
greet('John')  // Hello, John
greet('')      // Hello, (空字符串也会使用默认值,ES5 方式会输出 Guest)

默认参数的触发条件

javascript
function test(a = 'default') {
  console.log(a)
}

test()          // 'default'
test(undefined) // 'default'
test(null)      // null(null 不触发默认值)
test(0)         // 0
test('')        // ''(空字符串不触发默认值)
test(false)     // false

重要: 只有参数值为 undefined 或未传入时,才会使用默认值。

3. 默认参数表达式

默认参数可以是任意表达式,且只在需要时求值:

javascript
function getValue() {
  console.log('计算默认值')
  return 10
}

function foo(a = getValue()) {
  console.log(a)
}

foo()      // 输出: '计算默认值' 然后 10
foo(5)     // 输出: 5(不执行 getValue)

实际应用:必需参数检查

javascript
function required(paramName) {
  throw new Error(`参数 ${paramName} 是必需的`)
}

function foo(a = required('a'), b = required('b')) {
  return a + b
}

foo(1, 2)  // 3
foo(1)     // Error: 参数 b 是必需的

4. 默认参数作用域 ⚠️

默认参数有自己独立的作用域,这是一个重要的特性:

javascript
let x = 1

function foo(a = x) {
  let x = 2  // 局部变量 x
  console.log(a)
}

foo()  // 1,默认参数使用外部的 x

// 参数之间的依赖
function bar(a = b, b = 2) {
  console.log(a, b)
}

bar()  // ReferenceError: Cannot access 'b' before initialization

// 正确的顺序
function baz(a = 1, b = a) {
  console.log(a, b)
}

baz()     // 1 1
baz(2)    // 2 2
baz(2, 3) // 2 3

作用域规则:

  • 默认参数在函数参数列表中从左到右求值
  • 右侧参数可以引用左侧参数
  • 默认参数表达式在函数自己的作用域中求值

剩余参数(Rest Parameters)⚠️

ES6 引入剩余参数,用于获取函数的多余参数,是替代 arguments 对象的现代方式。

基本用法

javascript
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0)
}

sum(1, 2, 3, 4, 5)  // 15

// 剩余参数必须是最后一个参数
function foo(a, b, ...rest) {
  console.log(a)      // 1
  console.log(b)      // 2
  console.log(rest)   // [3, 4, 5]
}

foo(1, 2, 3, 4, 5)

剩余参数与 arguments 的区别

javascript
// arguments 是类数组对象
function oldWay() {
  console.log(arguments)        // Arguments { 0: 1, 1: 2, 2: 3 }
  console.log(Array.isArray(arguments))  // false
  // 需要转换为数组才能使用数组方法
  const args = Array.from(arguments)
}

// 剩余参数是真数组
function newWay(...args) {
  console.log(args)        // [1, 2, 3]
  console.log(Array.isArray(args))  // true
  // 可以直接使用数组方法
  args.forEach(arg => console.log(arg))
}

oldWay(1, 2, 3)
newWay(1, 2, 3)

剩余参数的优势

特性arguments剩余参数
类型类数组对象真数组
箭头函数支持❌ 不支持✅ 支持
数组方法需要转换直接使用
可读性较差更好
性能稍慢更快

实际应用场景

javascript
// 1. 数组合并
function mergeArrays(...arrays) {
  return arrays.flat()
}

mergeArrays([1, 2], [3, 4], [5, 6])  // [1, 2, 3, 4, 5, 6]

// 2. 动态函数调用
function callMethod(obj, method, ...args) {
  return obj[method](...args)
}

// 3. 构造器模式
class User {
  constructor(name, ...roles) {
    this.name = name
    this.roles = roles
  }
}

参数解构

对象解构

javascript
function printUser({ name, age, city = 'Unknown' }) {
  console.log(`Name: ${name}, Age: ${age}, City: ${city}`)
}

printUser({ name: 'John', age: 30 })
// Name: John, Age: 30, City: Unknown

// 配合默认参数使用
function createConfig({ 
  host = 'localhost', 
  port = 3000, 
  debug = false 
} = {}) {
  return { host, port, debug }
}

createConfig()  // { host: 'localhost', port: 3000, debug: false }
createConfig({ port: 8080 })  // { host: 'localhost', port: 8080, debug: false }

数组解构

javascript
function printCoordinates([x, y, z = 0]) {
  console.log(`X: ${x}, Y: ${y}, Z: ${z}`)
}

printCoordinates([1, 2])
// X: 1, Y: 2, Z: 0

解构的实际应用

javascript
// React 组件 props 解构
function UserProfile({ name, age, email, address = {} }) {
  const { city, country } = address
  // ...
}

// 配置对象模式
function setupApp({
  api: {
    baseUrl = '/api',
    timeout = 5000
  } = {},
  ui: {
    theme = 'light',
    language = 'zh-CN'
  } = {}
} = {}) {
  return { baseUrl, timeout, theme, language }
}

// 函数选项模式
function fetchData(url, {
  method = 'GET',
  headers = {},
  body = null,
  credentials = 'same-origin'
} = {}) {
  // 实现请求逻辑
}

函数返回值

基本概念

javascript
// 有返回值
function add(a, b) {
  return a + b
}

// 无返回值(返回 undefined)
function greet(name) {
  console.log('Hello, ' + name)
}

console.log(add(1, 2))    // 3
console.log(greet('John')) // Hello, John \n undefined

提前返回(Guard Clauses)

javascript
function divide(a, b) {
  if (b === 0) {
    return 'Cannot divide by zero'
  }
  return a / b
}

// 多重 guard clauses
function processUser(user) {
  if (!user) return 'User is required'
  if (!user.name) return 'User name is required'
  if (!user.email) return 'User email is required'
  
  // 处理用户数据
  return { success: true, user }
}

返回多个值

javascript
// 使用数组返回多个值
function getMinMax(numbers) {
  return [Math.min(...numbers), Math.max(...numbers)]
}

const [min, max] = getMinMax([1, 2, 3, 4, 5])
console.log(min, max)  // 1 5

// 使用对象返回多个值(推荐)
function getUser() {
  return { name: 'John', age: 30, city: 'New York' }
}

const { name, age, city } = getUser()
console.log(name, age, city)  // John 30 New York

返回函数(高阶函数)

javascript
// 返回函数 - 函数工厂
function multiplier(factor) {
  return function (number) {
    return number * factor
  }
}

const double = multiplier(2)
const triple = multiplier(3)

console.log(double(5))  // 10
console.log(triple(5))  // 15

// 实际应用:中间件模式
function logger(prefix) {
  return function(message) {
    console.log(`[${prefix}] ${message}`)
  }
}

const infoLogger = logger('INFO')
const errorLogger = logger('ERROR')

infoLogger('Operation succeeded')  // [INFO] Operation succeeded
errorLogger('Operation failed')    // [ERROR] Operation failed

链式调用

javascript
class Calculator {
  constructor(value = 0) {
    this.value = value
  }
  
  add(num) {
    this.value += num
    return this  // 返回 this 实现链式调用
  }
  
  multiply(num) {
    this.value *= num
    return this
  }
  
  getResult() {
    return this.value
  }
}

const result = new Calculator(10)
  .add(5)
  .multiply(2)
  .getResult()

console.log(result)  // 30

参数校验

类型检查

javascript
function processNumber(num) {
  if (typeof num !== 'number') {
    throw new TypeError(`Expected a number, got ${typeof num}`)
  }
  if (isNaN(num)) {
    throw new Error('Expected a valid number, got NaN')
  }
  return num * 2
}

// 使用默认参数和类型检查
function processArray(arr = []) {
  if (!Array.isArray(arr)) {
    throw new TypeError('Expected an array')
  }
  return arr.map(item => item * 2)
}

参数范围检查

javascript
function setAge(age) {
  if (typeof age !== 'number') {
    throw new TypeError('Age must be a number')
  }
  if (age < 0 || age > 150) {
    throw new RangeError('Age must be between 0 and 150')
  }
  this.age = age
}

函数重载模拟

JavaScript 不支持传统函数重载,但可以通过参数特性模拟。

通过参数个数

javascript
function greet() {
  switch (arguments.length) {
    case 0:
      console.log('Hello!')
      break
    case 1:
      console.log('Hello, ' + arguments[0])
      break
    case 2:
      console.log(`Hello, ${arguments[0]} ${arguments[1]}`)
      break
    default:
      console.log('Hello, everyone!')
  }
}

greet()            // Hello!
greet('John')      // Hello, John
greet('John', 'Doe') // Hello, John Doe

通过参数类型

javascript
function process(value) {
  if (typeof value === 'number') {
    console.log('Number:', value * 2)
  } else if (typeof value === 'string') {
    console.log('String:', value.toUpperCase())
  } else if (Array.isArray(value)) {
    console.log('Array:', value.join(', '))
  } else if (value instanceof Date) {
    console.log('Date:', value.toISOString())
  } else if (typeof value === 'object' && value !== null) {
    console.log('Object:', JSON.stringify(value))
  }
}

process(10)        // Number: 20
process('hello')   // String: HELLO
process([1, 2, 3]) // Array: 1, 2, 3

通过对象参数(推荐)

javascript
function createUser(options) {
  const {
    name = 'Guest',
    age = 0,
    email = '',
    role = 'user',
    permissions = []
  } = options
  
  if (!name) {
    throw new Error('Name is required')
  }
  
  return { 
    name, age, email, role,
    permissions, createdAt: new Date()
  }
}

const user = createUser({
  name: 'John',
  age: 30,
  role: 'admin',
  permissions: ['read', 'write', 'delete']
})

常见陷阱与避坑指南

陷阱1:修改参数对象

javascript
// ❌ 陷阱:意外修改传入的对象
function updateUser(user) {
  user.name = 'modified'  // 修改了原对象
  return user
}

const original = { name: 'original' }
updateUser(original)
console.log(original.name)  // 'modified'(原对象被修改)

// ✅ 正确:创建副本
function updateUserSafe(user) {
  return { ...user, name: 'modified' }
}

陷阱2:默认参数的意外行为

javascript
// ❌ 陷阱:引用类型的默认值在复杂场景
let defaultConfig = { debug: false }

function setup(config = defaultConfig) {
  config.debug = true  // 修改了默认对象
  return config
}

setup()  // { debug: true }
setup()  // { debug: true }(defaultConfig 已被修改)

// ✅ 正确:使用不可变默认值
function setup(config = { debug: false }) {
  return config
}

陷阱3:参数顺序错误

javascript
// ❌ 陷阱:参数顺序容易出错
function createElement(tag, content, className, id, style) {
  // 参数很多,调用时容易搞错顺序
}

// ✅ 正确:使用对象参数
function createElement(options) {
  const { tag, content, className = '', id = '', style = '' } = options
  // ...
}

createElement({
  tag: 'div',
  content: 'Hello',
  className: 'container',
  id: 'main',
  style: 'color: red'
})

最佳实践

1. 参数数量控制

javascript
// ❌ 不推荐:参数过多(超过 3-4 个)
function createUser(name, age, email, phone, address, city, country) {
  // ...
}

// ✅ 推荐:使用对象参数
function createUser(userInfo) {
  const { name, age, email, phone, address, city, country } = userInfo
  // ...
}

// ✅ 推荐:使用配置对象 + 默认值
function createUser({
  name = 'Guest',
  age = 0,
  email = '',
  phone = '',
  address = ''
} = {}) {
  // ...
}

2. 参数默认值

javascript
// ❌ 不推荐:使用 || 运算符
function foo(a, b, c) {
  a = a || 1  // 0, '', false 都会使用默认值
}

// ✅ 推荐:使用 ES6 默认参数
function foo(a = 1, b = 2, c = 3) {
  // 只有 undefined 才使用默认值
}

3. 剩余参数替代 arguments

javascript
// ❌ 不推荐:使用 arguments
function sum() {
  return Array.from(arguments).reduce((a, b) => a + b, 0)
}

// ✅ 推荐:使用剩余参数
function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0)
}

4. 纯函数与副作用

javascript
// ❌ 避免:修改参数(副作用)
function addItem(item, list = []) {
  list.push(item)
  return list
}

// ✅ 推荐:返回新数组(纯函数)
function addItem(item, list = []) {
  return [...list, item]
}

总结

核心要点

  1. 参数传递:JavaScript 中所有参数都是按值传递,引用类型传递的是引用的副本
  2. 默认参数:只有参数为 undefined 或未传入时才使用默认值
  3. 剩余参数:使用 ... 语法,是真数组,优于 arguments
  4. 参数解构:可以简化对象参数的处理,提高代码可读性
  5. 返回值:合理使用返回值,避免副作用
  6. 参数校验:对关键参数进行类型和范围检查

最佳实践清单

  • ✅ 使用默认参数代替 || 运算符
  • ✅ 参数数量超过 3 个时使用对象参数
  • ✅ 使用剩余参数替代 arguments
  • ✅ 参数解构提高可读性
  • ✅ 不修改参数对象,保持纯函数
  • ✅ 使用对象参数模拟函数重载
  • ✅ 为函数添加清晰的 JSDoc 注释
  • ✅ 关键参数进行类型和范围校验

API 设计原则

  1. 一致性:相似功能的函数使用相似的参数接口
  2. 可读性:参数名和顺序应该清晰表达意图
  3. 容错性:提供合理的默认值
  4. 可扩展性:使用对象参数便于未来扩展
  5. 文档化:使用 JSDoc 或 TypeScript 类型注释

参考资料