递归return后持续调用原理及归并排序执行逻辑疑问
先看你分析的这段归并排序代码:
function mergeSort(a) { if (a.length === 1) return a; const mid = Math.trunc(a.length / 2); const left = mergeSort(a.slice(0, mid)); const right = mergeSort(a.slice(mid)); const result = []; ((l, r) => { let i = 0, j = 0; while (i < l.length && j < r.length) { l[i] < r[j] ? result.push(l[i++]) : result.push(r[j++]); } while (i < l.length) result.push(l[i++]); while (j < r.length) result.push(r[j++]); })(left, right); return result; } const random = [10, 5, 2, 7, 3, 4];
问题1:递归在执行return后为何仍能持续调用?
递归的本质是多层函数调用栈,每一次mergeSort调用都是一个独立的栈帧(执行上下文)。当某个子调用执行return时,只是当前这个栈帧执行完毕、弹出栈,回到它的父级调用栈帧继续执行剩余代码,不是整个递归流程终止。
举个实际执行流程的例子:
- 初始调用
mergeSort(random)(数组长度6),会先执行left = mergeSort([10,5,2]); - 进入
mergeSort([10,5,2])(长度3),又执行left = mergeSort([10]); - 进入
mergeSort([10])(长度1),触发return a,这个子调用结束,回到mergeSort([10,5,2])的栈帧,继续执行right = mergeSort([5,2]); - 进入
mergeSort([5,2])(长度2),先执行left = mergeSort([5]),这个子调用return后,再执行right = mergeSort([2]),等这两个子调用都return后,才会执行后续的IIFE合并逻辑。
每一次return只是结束当前这一层的函数调用,父级调用还在等待子调用的返回值,所以递归会继续推进,直到最外层的初始调用执行完毕。
问题2:归并排序中是什么在内存中保存左/右子数组;首次执行IIFE时接收参数[5,2](因[1]被返回)并完成排序,为何后续IIFE会再次运行以排序左数组[10]与右数组[2,5],是什么触发了该操作?
内存存储左/右子数组的载体
左、右子数组是保存在函数调用栈的局部变量里的。JavaScript引擎会为每一次mergeSort调用创建一个栈帧,栈帧里包含当前函数的所有局部变量(left、right、mid、result等)。只有当当前mergeSort调用执行完毕(return后),对应的栈帧才会被销毁,里面的局部变量才会被回收。
后续IIFE的触发逻辑
首次执行IIFE是在mergeSort([5,2])这个调用栈帧里:当它的left(mergeSort([5])返回的[5])和right(mergeSort([2])返回的[2])都拿到结果后,函数会继续执行后续代码——创建result,然后执行IIFE合并这两个数组,得到[2,5],再把这个结果return给上一层mergeSort([10,5,2])的right变量。
此时mergeSort([10,5,2])的left已经是mergeSort([10])返回的[10],现在right也拿到了[2,5],它就会继续执行自己的后续代码:创建result,触发自己的IIFE合并[10]和[2,5],得到[2,5,10],再return给最外层调用的left变量。
简单说:每一层mergeSort调用,都会在拿到left和right的递归返回值后,自动执行后续的IIFE合并逻辑——这是函数代码本身的流程决定的,子调用返回后,父调用会继续往下走,直到执行完所有代码再return结果。
内容的提问来源于stack exchange,提问作者Amie Ambriz

