JS 执行机制深度解析:大厂历年高频真题(带解题思路)+ 核心原理,面试直接用

74 阅读9分钟

前言:为什么 JS 执行机制是大厂必考点?​

在大厂前端面试中,JS 执行机制是「基础中的基础」—— 无论是字节、阿里还是腾讯,都会通过「代码输出题」「原理阐述题」考察你对单线程、事件循环、宏微任务的理解。比如:​

  • 「以下代码的输出顺序是什么?」(经典宏微任务题)​
  • 「JS 为什么是单线程?事件循环的工作原理是什么?」​
  • 「async/await 和 Promise 的执行顺序差异?」​

理解 JS 执行机制,不仅能搞定面试,更能解决实际开发中的异步问题(比如回调地狱、接口请求顺序错乱)。本文将从「底层原理→核心概念→真题拆解」层层递进,帮你彻底掌握。​

​​

一、核心前提:JS 为什么是单线程?​

  1. 单线程的定义​

JS 设计之初就是「单线程」—— 同一时间只能执行一个任务,所有任务排队等待执行。​

  1. 为什么不设计成多线程?​
  • 避免 DOM 操作冲突:如果多个线程同时操作 DOM,会导致页面渲染混乱(比如线程 A 要删除 DOM,线程 B 要修改 DOM)。​
  • 简化执行逻辑:单线程无需处理线程同步、锁机制等复杂问题,降低了语言复杂度。​
  1. 单线程的问题与解决方案​

单线程的痛点:如果有一个耗时任务(比如网络请求、定时器),会阻塞后续任务执行(页面卡死)。​

解决方案:「异步编程」+「事件循环」—— 耗时任务放入「任务队列」,主线程先执行同步任务,待同步任务完成后,再从队列中取出异步任务执行。​

​​

二、执行上下文:JS 代码的「运行环境」​

执行上下文是 JS 代码执行时的环境容器,包含「变量、函数、this 指向」等信息。每次执行代码,都会创建对应的执行上下文。​

  1. 执行上下文的类型​

类型​场景​示例​
全局执行上下文​页面加载时,JS 引擎首先创建​console.log(window)​
函数执行上下文​函数被调用时创建​function fn() {} fn()​
eval 执行上下文​eval() 函数执行代码时创建(慎用)​eval('var a=1')​

  1. 执行上下文栈(调用栈)​

JS 用「栈结构」管理执行上下文,遵循「先进后出」原则:​

  • 页面加载时,全局执行上下文入栈(栈底)。​
  • 函数调用时,函数执行上下文入栈。​
  • 函数执行完毕,其执行上下文出栈。​
  • 所有代码执行完毕,全局执行上下文出栈。​

实例:执行上下文栈的变化​

function a() {​

b();​

console.log('a 执行完毕');​

}​

function b() {​

console.log('b 执行完毕');​

}​

a();​

栈变化过程:​

  1. 全局执行上下文入栈 → 栈:[全局]​
  1. 调用 a() → a 执行上下文入栈 → 栈:[全局,a]​
  1. a 中调用 b() → b 执行上下文入栈 → 栈:[全局,a, b]​
  1. b 执行完毕 → b 出栈 → 栈:[全局,a]​
  1. a 执行完毕 → a 出栈 → 栈:[全局]​
  1. 所有代码执行完毕 → 全局出栈 → 栈空​

  2. 执行上下文的创建过程​

每个执行上下文创建时,会经历 2 个阶段:​

  1. 初始化阶段:​
  • 确定 this 指向(全局上下文 this 是 window,函数上下文 this 取决于调用方式)。​
  • 创建「词法环境」(存储变量、函数声明)。​
  • 创建「变量环境」(存储 var 声明的变量,初始值为 undefined,存在变量提升)。​
  1. 执行阶段:​
  • 赋值变量、执行函数调用、解析代码逻辑。​

关键考点:变量提升 vs 函数提升​

  • 函数提升:函数声明会被提升到当前作用域顶部,可提前调用。​
  • 变量提升:var 声明的变量会被提升,初始值为 undefined;let/const 不会提升,存在「暂时性死区」。​

// 函数提升:可提前调用​

fn(); // 输出 "fn 执行"​

function fn() {​

console.log('fn 执行');​

}​

// 变量提升:var 初始值 undefined​

console.log(a); // undefined​

var a = 10;​

// let/const 无提升:暂时性死区​

console.log(b); // 报错:Cannot access 'b' before initialization​

let b = 20;​

​​

三、作用域与作用域链:变量的「访问规则」​

  1. 作用域的定义​

作用域是变量、函数的可访问范围,决定了代码中变量的可见性。​

  1. 作用域的类型​

类型​定义​作用域链查找顺序​
全局作用域​页面级作用域,所有代码可访问​自身 → 无​
函数作用域​函数内部的作用域,仅函数内可访问​自身 → 外层函数 → 全局​
块级作用域​let/const 声明的变量所在的 {} 块(if/for/switch)​自身 → 外层块 → 全局​

  1. 作用域链​

当访问一个变量时,JS 会从当前作用域开始查找,若找不到则向上一层作用域查找,直到全局作用域 —— 这个查找链条就是「作用域链」。​

实例:作用域链查找​

var globalVar = '全局变量';​

function outer() {​

var outerVar = '外层变量';​

function inner() {​

var innerVar = '内层变量';​

console.log(innerVar); // 内层变量(当前作用域)​

console.log(outerVar); // 外层变量(外层作用域)​

console.log(globalVar); // 全局变量(全局作用域)​

console.log(notExistVar); // 报错:not defined(全局也找不到)​

}​

inner();​

}​

outer();​

  1. 闭包:作用域链的延伸(大厂高频考点)​

闭包的定义​

函数嵌套时,内层函数引用外层函数的变量 / 函数,且内层函数被外层函数外部访问 —— 此时内层函数就是闭包,会保留外层函数的作用域。​

闭包的形成条件​

  1. 函数嵌套(内层函数嵌套在外层函数中)。​
  1. 内层函数引用外层函数的变量 / 函数。​
  1. 内层函数被外层函数外部调用。​

实例:闭包的应用与考点​

function createCounter() {​

let count = 0; // 外层函数变量,被内层函数引用​

return function() {​

count++; // 引用外层变量​

return count;​

};​

}​

const counter = createCounter(); // 内层函数被外部访问​

console.log(counter()); // 1​

console.log(counter()); // 2​

console.log(counter()); // 3​

闭包的考点​

  • 作用:保存变量状态(如计数器)、模块化(隐藏私有变量)。​
  • 问题:可能导致内存泄漏(闭包引用的外层变量不会被垃圾回收)。​
  • 解决方案:不再使用时,手动解除引用(counter = null)。​

​​

四、事件循环:异步编程的核心(大厂必问)​

事件循环(Event Loop)是 JS 处理异步任务的核心机制,解决了单线程阻塞的问题。​

  1. 核心概念:同步任务 vs 异步任务​
  • 同步任务:立即执行的任务,阻塞后续任务(如变量声明、函数调用、console.log)。​
  • 异步任务:不立即执行的任务,不会阻塞后续任务(如定时器、网络请求、DOM 事件、Promise)。​

异步任务又分为「宏任务」和「微任务」,优先级不同:​

类型​常见任务​
宏任务​setTimeout、setInterval、DOM事件、fetch、script(整体代码)​
微任务​Promise.then/catch/finally、process.nextTick(Node 独有)、queueMicrotask​

  1. 事件循环的执行流程(浏览器环境)​

  2. 执行全局同步任务(全局执行上下文入栈,执行完毕出栈)。​

  1. 执行当前微任务队列中的所有微任务(按顺序执行,直到队列空)。​
  1. 执行一次宏任务队列中的第一个宏任务(执行完毕后,出队)。​
  1. 再次执行所有微任务(宏任务执行后,会清空微任务队列)。​
  1. 重复步骤 3-4,形成循环。​

流程图​

同步任务 → 微任务队列(全部执行)→ 宏任务队列(执行第一个)→ 微任务队列(全部执行)→ ...​

  1. 经典真题:宏微任务执行顺序​

console.log('1'); // 同步任务​

setTimeout(() => {​

console.log('2'); // 宏任务​

}, 0);​

new Promise((resolve) => {​

console.log('3'); // 同步任务(Promise 构造函数是同步的)​

resolve();​

}).then(() => {​

console.log('4'); // 微任务​

});​

console.log('5'); // 同步任务​

解题步骤​

  1. 执行同步任务:console.log('1') → 输出 1;new Promise 构造函数同步执行 → 输出 3;console.log('5') → 输出 5。​
  1. 同步任务执行完毕,执行微任务队列:Promise.then → 输出 4。​
  1. 微任务队列空,执行宏任务队列第一个:setTimeout → 输出 2。​
  1. 最终输出:1 → 3 → 5 → 4 → 2。​

  2. 进阶考点:async/await 与事件循环​

async/await 是 Promise 的语法糖,本质还是微任务。核心规则:​

  • async 函数执行时,遇到 await 会暂停,先执行 await 后面的表达式(同步)。​
  • await 后面的代码(即 await 所在行之后的代码)会被放入「微任务队列」,等待当前同步任务和已有微任务执行完毕后执行。​

真题:async/await 执行顺序​

async function async1() {​

console.log('async1 start'); // 同步​

await async2(); // 执行 async2(同步),暂停 async1,后续代码放入微任务​

console.log('async1 end'); // 微任务​

}​

async function async2() {​

console.log('async2'); // 同步​

}​

console.log('script start'); // 同步​

setTimeout(() => {​

console.log('setTimeout'); // 宏任务​

}, 0);​

async1();​

new Promise((resolve) => {​

console.log('promise1'); // 同步​

resolve();​

}).then(() => {​

console.log('promise2'); // 微任务​

});​

console.log('script end'); // 同步​

解题步骤​

  1. 执行同步任务:​
  • console.log('script start') → 输出 script start。​
  • 调用 async1() → 输出 async1 start → 调用 async2() → 输出 async2 → await 暂停 async1,async1 end 放入微任务队列。​
  • new Promise 构造函数同步执行 → 输出 promise1 → then 放入微任务队列。​
  • console.log('script end') → 输出 script end。​
  1. 同步任务执行完毕,执行微任务队列:​
  • 先执行 async1 end → 输出 async1 end。​
  • 再执行 promise2 → 输出 promise2。​
  1. 微任务队列空,执行宏任务:setTimeout → 输出 setTimeout。​
  1. 最终输出:script start → async1 start → async2 → promise1 → script end → async1 end → promise2 → setTimeout。​

  2. 浏览器 vs Node.js 事件循环差异(大厂高频考点)​

维度​浏览器环境​Node.js 环境(11+ 版本)​
宏任务队列​1 个宏任务队列​6 个宏任务队列(按优先级执行,如 timers 队列优先级高于 poll 队列)​
微任务执行时机​每个宏任务执行后,清空微任务队列​每个宏任务执行后,清空微任务队列(与浏览器一致)​
微任务类型​Promise.then、queueMicrotask​新增 process.nextTick(优先级高于普通微任务)​

Node.js 特有考点:process.nextTick​

console.log('1');​

process.nextTick(() => {​

console.log('2'); // 微任务(优先级最高)​

});​

new Promise((resolve) => {​

console.log('3');​

resolve();​

}).then(() => {​

console.log('4'); // 普通微任务​

});​

console.log('5');​

// 输出:1 → 3 → 5 → 2 → 4​

​​

五、大厂面试真题汇总(含解析)​

真题 1:宏微任务混合执行​

console.log('a');​

setTimeout(() => {​

console.log('b');​

new Promise((resolve) => {​

console.log('c');​

resolve();​

}).then(() => {​

console.log('d');​

});​

}, 0);​

new Promise((resolve) => {​

console.log('e');​

resolve();​

}).then(() => {​

console.log('f');​

setTimeout(() => {​

console.log('g');​

}, 0);​

});​

console.log('h');​

解析:​

  1. 同步任务:a → e → h。​
  1. 微任务:f → 微任务队列空。​
  1. 宏任务 1(setTimeout):b → c → 微任务 d → 微任务队列空。​
  1. 宏任务 2(setTimeout):g。​
  1. 输出:a → e → h → f → b​