You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript中Spread语法与生成器实现树转数组的2倍性能差原因探究

二叉树前序遍历两种实现的性能差异原因分析

我在练习JavaScript函数式编程时,测试了两种二叉树前序遍历转数组的实现:基于spread语法的seql方法,以及基于yield关键字的生成器preOrderTraversal方法。测试结果显示前者性能约为后者的2倍,前者每秒操作数在700-1500间波动,后者稳定在400左右。以下是测试代码:

class BinaryTreeNode {
  constructor(key, value = key, parent = null) {
    this.key = key;
    this.value = value;
    this.parent = parent;
    this.left = null;
    this.right = null;
  }

  get isLeaf() {
    return this.left === null && this.right === null;
  }

  get hasChildren() {
    return !this.isLeaf;
  }
}

class BinaryTree {
  constructor(key, value = key) {
    this.root = new BinaryTreeNode(key, value);
  }

  seql(node = this.root) {
    return [
      node,
      ...(node.left ? this.seql(node.left) : []),
      ...(node.right ? this.seql(node.right) : []),
    ];
  }

  *preOrderTraversal(node = this.root) {
    yield node;
    if (node.left) yield* this.preOrderTraversal(node.left);
    if (node.right) yield* this.preOrderTraversal(node.right);
  }

  fill() {
    for (let node of this.seql()) {
      if (node.left === null) {
        node.left = new BinaryTreeNode(Math.random(), Math.random(), node);
      }
      if (node.right === null) {
        node.right = new BinaryTreeNode(Math.random(), Math.random(), node);
      }
    }
  }
}

let i = 1;
const tree = new BinaryTree(Math.random, Math.random());
while (i < 13) {
  tree.fill();
  i++;
}

const test = () => {
  [...tree.preOrderTraversal()];
  //[...tree.seql()];
};

const iterationsC = 50;
const main = async () => {
  const runBaseline = (iterations = iterationsC) => {
    let startTime = new Date().getTime();
    for (let i = 0; i < iterations; i++) {
      // no operation
    }
    let endTime = new Date().getTime();
    return endTime - startTime;
  };

  const runTest = (iterations = iterationsC) => {
    let startTime = new Date().getTime();

    for (let i = 0; i < iterations; i++) test();

    let endTime = new Date().getTime();
    let baselineTime = runBaseline(iterations);
    let totalTime = endTime - startTime - baselineTime;
    let fnTime = (totalTime / iterations).toFixed(10);
    let operations = Math.floor((1000 / totalTime) * iterations);

    console.log(`total: ${totalTime}, time: ${fnTime}, op/sec: ${operations}`);
  };

  while (true) {
    runTest();
    await sleep(50);
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
};

main();

性能差异的核心原因:

  • 生成器的状态管理开销:生成器函数每次调用都会创建独立的迭代器对象,yield和yield*操作需要频繁暂停、保存函数执行上下文,恢复时又要重新加载状态,这一系列操作的开销远高于普通函数的直接执行。

  • Spread语法的引擎优化:seql方法通过递归+spread构建数组,现代JavaScript引擎对数组字面量和spread操作有深度优化,会将多次数组拼接合并为高效的内存分配与拷贝,即便产生中间数组,引擎优化后的开销也远低于生成器的迭代流程。

  • 迭代器转数组的额外步骤:用[...generator]转换数组时,需要逐个调用迭代器的next()方法,每次调用都会触发生成器的状态恢复、执行到下一个yield,这层迭代协议的调用链比直接递归构建数组多了大量额外操作。

  • 递归模式的效率差异:seql是普通递归函数,每次调用直接返回数组,引擎对普通递归的栈处理更直接高效;而preOrderTraversal的yield*本质是迭代器委托,涉及嵌套迭代器调用,内部逻辑复杂度更高,开销自然更大。

内容的提问来源于stack exchange,提问作者Likepineapple

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 14:35:11