主线程与调用栈严格绑定,主线程唯一职责是压入和弹出函数执行上下文;同步代码必经调用栈,异步回调需等栈空后由事件循环推入执行。

主线程和调用栈是 JavaScript 同步执行的“一体两面”:主线程是执行环境,调用栈是它的运行记录器。两者严格绑定——主线程只做一件事:维护并操作调用栈。
主线程只负责推入和弹出函数
JavaScript 引擎(如 V8)在主线程上运行时,不会并发执行多个函数。每当遇到一个函数调用,主线程就立即将其执行上下文压入调用栈顶部;函数返回或执行完毕,主线程立刻将其从栈顶弹出。这个过程不可中断、不可跳过。
- 压栈 = 开始执行该函数,分配内存,初始化变量和作用域
- 弹出 = 清理该函数的执行上下文,释放局部变量,恢复上层函数状态
- 如果栈溢出(比如无限递归),主线程直接抛出
RangeError: Maximum call stack size exceeded
调用栈是主线程的唯一执行视图
你无法绕过调用栈让主线程“执行别的东西”。所有同步代码(console.log、for 循环、普通函数调用)都必须先进栈、再执行、最后出栈。它就像主线程的“工作日志”,实时反映当前正在做什么、谁调用了谁、哪一层还没结束。
- 栈底通常是
Global Execution Context(全局上下文) - 每层栈帧包含该函数的参数、局部变量、
this、词法作用域链等信息 - 开发者工具里的“Call Stack”面板,就是这个结构的可视化呈现
异步操作不进栈,但最终靠主线程+调用栈收尾
像 setTimeout 或 fetch 这类异步操作,本身不占用主线程,也不进调用栈。它们由浏览器 Web API 处理,完成后把回调放入任务队列。但回调要真正执行,必须等主线程空闲、调用栈清空后,由事件循环推入栈中——此时主线程才重新开始压栈操作。
- 也就是说:异步 ≠ 并行执行,只是“延迟进栈”
- 微任务(如
Promise.then)会在当前宏任务结束后、下一个宏任务开始前,被优先推入调用栈执行 - 没有调用栈,回调函数就只是队列里的一段代码,永远不会运行
单线程本质就体现在这个协同关系里
主线程没有“多任务切换”,也没有“后台线程协作”。它永远忠实地按调用栈指示干活:栈不空,就不取新任务;栈一空,就查队列;队列有活,就压一个进去继续干。这种确定性,正是 JS 能避免竞态条件、保持行为可预测的根本原因。











