JS函数返回值技巧:怎么返回多个值或复杂对象

作者:青柠

时间:2026-10-30

浏览:658

深入讲解JavaScript函数返回多个值与复杂对象的核心技巧与设计模式,详解对象解构、数组元组解构、Result模式及类型安全保障。

文章目录

在编写 JavaScript 业务逻辑时,我们经常遇到需要让一个函数输出多项结果的情况。例如在解析表单或处理网络请求时,调用方既需要拿到解析后的结构化数据,又需要同步获知验证状态、错误消息集合或分页计数。然而,JavaScript 规范中函数的 return 语句只能接收单个表达式并返回单一值或引用。要优雅地传递多维数据,开发者需要将多项结果组织成合适的数据载体,再配合现代 JavaScript 的语法特性高效提取。

解决这个问题的核心思路,并不是强行改变运行时的语法规则,而是通过“对象打包”、“数组元组封装”或“自定义结构体”来承载多项数据,并结合 ES6 解构赋值、默认值与 Result 模式,让调用端像获取原生多个返回值一样清晰自如。

一、JavaScript 多返回值的底层原理:封装与解构

JavaScript 引擎在执行 return value; 时,只在栈帧顶部写入一个指向返回值的内存槽。所谓“返回多个值”,本质上是把多个逻辑变量收拢为一个复合对象或数组引用,返回后立即在调用处解构分发。理解这一点有助于我们在设计函数签名时建立清晰的数据流意识。

JS函数返回值技巧:怎么返回多个值或复杂对象
图 1:JavaScript 函数实现多返回值的数据流转全过程

二、技巧一:返回对象——最健壮、可扩展的多值返回方式

返回普通字面量对象(Plain Object)是绝大多数业务场景下的第一选择。它的最大优势是语义清晰且与调用顺序无关。当函数日后扩展新增返回值字段时,既有的调用端完全不受破坏。

1. 基础用法与解构重命名

借助 ES6 的对象属性缩写和解构赋值,定义与消费都非常简洁:

// 统计用户分数及排名的函数
function analyzeScore(scores) {
  if (!Array.isArray(scores) || scores.length === 0) {
    return { average: 0, highest: 0, lowest: 0, count: 0 };
  }

  const count = scores.length;
  const highest = Math.max(...scores);
  const lowest = Math.min(...scores);
  const average = Number((scores.reduce((sum, s) => sum + s, 0) / count).toFixed(1));

  // 通过对象字面量直接打包返回
  return {
    average,
    highest,
    lowest,
    count
  };
}

// 调用端:选择性解构,并支持为变量重命名与设置默认值
const rawList = [88, 92, 79, 95, 60];
const { average: classAvg, highest, missing = false } = analyzeScore(rawList);

console.log(classAvg); // 82.8
console.log(highest);  // 95
console.log(missing);  // false (触发了默认值)

2. 对象返回的四大优势

  • 无序解构:调用者不需要按定义顺序接收参数,只需指定关心的键名。
  • 默认值回退:即便某个属性返回 undefined,解构语法允许声明 fallback。
  • 无损扩展:如果未来函数需要额外返回 median(中位数),现有只解构 average 的代码不需要任何改动。
  • 代码自解释:相比读取数组索引 res[1],res.highest 具有强烈的自解释性。

三、技巧二:返回数组(元组)——精简、重命名自由的高频组合

如果返回的值只有 2 到 3 个,且各项在业务上高度关联(最典型的如 React 的 useState),返回数组(即仿 Tuple 结构)是非常好的方案。

// 模拟状态订阅器:返回当前值与更新函数
function createToggle(initialState = false) {
  let state = Boolean(initialState);
  
  const getState = () => state;
  const toggle = () => {
    state = !state;
    return state;
  };

  // 返回长度为 2 的数组元组
  return [getState, toggle];
}

// 调用端:调用者可以任意自由命名,无需冒号别名
const [isVisible, toggleVisible] = createToggle(false);
const [isSidebarOpen, toggleSidebar] = createToggle(true);

toggleVisible();
console.log(isVisible()); // true

注意边界:当返回项超过 3 个时,请尽量避免使用数组返回。因为位置必须严格对应,调用方如果只要第四项,不得不写成 const [, , , fourth] = fn(),极易引发偏移错误且难以维护。

四、对象返回 vs 数组返回:如何权衡选择

在工程实践中,选用对象还是数组并非个人随意偏好,而是取决于扩展性要求与变量命名权归属:

JS函数返回值技巧:怎么返回多个值或复杂对象
图 2:对象打包与数组元组选型维度对比

五、技巧三:返回复杂业务对象与领域模型

当返回值不仅包含纯数据,还携带关联操作(行为)或需要维持内部一致性时,直接返回包含闭包方法或类实例(Class Instance)的复杂对象是最地道的做法。

// 复杂对象模式:工厂函数返回包含数据与操作的对象
function createCartSession(userId) {
  const items = new Map();
  let createdAt = Date.now();

  return {
    userId,
    // 行为方法:外部无法直接修改内部 items 结构,必须通过受控接口
    addItem(productId, quantity = 1, price = 0) {
      const current = items.get(productId) || { quantity: 0, price };
      current.quantity += quantity;
      current.price = price;
      items.set(productId, current);
    },
    removeItem(productId) {
      items.delete(productId);
    },
    // 计算属性通过 getter 提供只读访问
    get summary() {
      let totalAmount = 0;
      let totalCount = 0;
      for (const item of items.values()) {
        totalAmount += item.quantity * item.price;
        totalCount += item.quantity;
      }
      return { totalCount, totalAmount };
    },
    // 导出快照数据
    toJSON() {
      return {
        userId,
        createdAt,
        itemList: Array.from(items.entries()).map(([id, info]) => ({ id, ...info }))
      };
    }
  };
}

// 消费端
const cart = createCartSession("user_1024");
cart.addItem("book_99", 2, 45);
console.log(cart.summary); // { totalCount: 2, totalAmount: 90 }

六、技巧四:Result 模式与优雅异步错误处理

在异步请求或可能抛出异常的复杂逻辑中,传统的 try-catch 会使调用端嵌套层级过深。借鉴现代函数式编程与 Go 语言思想,我们可以让函数固定返回 { data, error } 或 [err, result],即 Result 模式。

JS函数返回值技巧:怎么返回多个值或复杂对象
图 3:基于复合对象返回的 Result 错误处理架构
// 封装通用的安全执行助手
async function safeExecute(asyncFn, ...args) {
  try {
    const data = await asyncFn(...args);
    return { data, error: null };
  } catch (err) {
    return { data: null, error: err instanceof Error ? err : new Error(String(err)) };
  }
}

// 模拟API调用
async function fetchUserDetail(id) {
  if (id <= 0) throw new Error("无效的用户ID");
  return { id, name: "Alice", role: "Developer" };
}

// 调用示例:扁平、直观、免去深层 try-catch
async function run() {
  const { data: user, error } = await safeExecute(fetchUserDetail, -1);
  
  if (error) {
    console.warn("读取用户失败:", error.message);
    return;
  }
  
  console.log("用户信息:", user.name);
}

七、工程防坑指南:保护返回的复杂对象

在返回复杂对象时,初学者极易踩入“引用泄漏”的陷阱:

  1. 不要直接返回模块内部可变状态的直接引用:

    如果函数直接 return internalConfig;,外部调用方直接修改 res.port = 8080 会污染内部状态。应优先返回浅拷贝 { ...internalConfig } 或结构化深克隆。

  2. 关键配置使用 Object.freeze() 封印:

    对于不允许调用方后续篡改的基础常量对象,在返回前执行浅冻结:

    function getSystemConfig() {
      return Object.freeze({
        apiEndpoint: "https://api.domain.com/v1",
        timeoutMs: 5000,
        retryCount: 3
      });
    }
  3. 配合 JSDoc 或 TypeScript 固化类型提示:

    返回多字段或复杂对象时,IDE 的代码补全依赖准确的类型注解。即使在普通 JS 项目中,通过规范的 JSDoc 注解也能让调用方直接享受敲代码时的智能提示:

    /**
     * @typedef {Object} ParseResult
     * @property {boolean} success - 是否成功
     * @property {string[]} warnings - 警告列表
     * @property {Record<string, any>} payload - 解析内容
     */
    
    /**
     * @param {string} raw
     * @returns {ParseResult}
     */
    function parseCustomPayload(raw) {
      return {
        success: true,
        warnings: [],
        payload: JSON.parse(raw)
      };
    }

八、全篇总结与实践选型全景

JS函数返回值技巧:怎么返回多个值或复杂对象
图 4:函数返回值技巧架构与工程保障总览

综上所述,JavaScript 虽在语法上保持“单返回值”约束,但现代语言特性的融合为我们提供了丰富的表达手段。掌握“3项以上优选对象、高频双元选元组、不确定异常用Result模式、复杂行为用工厂领域模型”的基本准则,就能在保证灵活性的同时,写出层次清晰、健壮且易于维护的代码。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章