news 2026/9/7 23:34:07

JavaScript面试核心考点精讲:变量提升、闭包、事件循环与手写题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript面试核心考点精讲:变量提升、闭包、事件循环与手写题

1. 变量提升与暂时性死区:var、let、const 到底怎么考

1.1 var 的变量提升到底提的是什么

前端面试问到 JS,十次里有八次会从变量提升开场。我当年第一次被问“var 和 let 的区别”时,只背了句“var 有变量提升,let 没有”,结果面试官追问了一句“那函数声明有变量提升吗?函数表达式呢?”我就卡住了。所以这一节咱们把变量提升这件事彻底讲透。

先看最经典的代码:

console.log(a); // undefined var a = 10;

很多新手以为这是“报错”,实际输出是undefined。原因在于:JS 引擎在执行代码之前,会先做一次完整的“收集”过程,把所有var声明的变量和function声明的函数,登记到当前执行上下文的变量环境里,并且把var声明的变量初始化为undefined。这个过程就是变量提升。

注意这里的两个关键点:

  • 提升的是声明,不是赋值,赋值仍然留在原来的位置。
  • var声明的变量会被初始化为undefined,所以提前访问不报错,但拿不到值。

如果换成 let:

console.log(a); // ReferenceError: Cannot access 'a' before initialization let a = 10;

let 也会被“提升”,但它不会初始化成undefined,而是进入一个叫“暂时性死区”(Temporal Dead Zone,简称 TDZ)的状态。在 TDZ 内访问变量,直接抛 ReferenceError。所以严格来说,let 也存在提升,只是行为表现和 var 完全不同。面试官想听的往往就是这一层:你不知道 let 其实也被提升了,说明你对执行上下文的理解还停留在背结论阶段

再看函数声明和函数表达式的区别:

foo(); // "函数声明" bar(); // TypeError: bar is not a function function foo() { console.log("函数声明"); } var bar = function () { console.log("函数表达式"); };

函数声明整体提升,所以调用没问题;函数表达式本质是var bar = xxx,只提升了 var 声明,bar当时是undefined,调用自然报错。这些细节面试官特别爱拆开问,答的时候别急,把“声明提升”和“初始化时机”分开说,思路会清晰很多。

1.2 let/const 的暂时性死区与经典循环输出题

暂时性死区最直观的触发场景有两个:块级作用域内的提前访问,以及 typeof 的使用。

typeof x; // ReferenceError,而不是 "undefined" let x;

注意这里有个反直觉的坑:typeof在没有声明过的变量上会返回"undefined",但一旦进入 TDZ,typeof也会抛错。这题我见过不少候选人答错,因为他们只记了“typeof 一个不存在的变量不会报错”这条结论,没意识到 TDZ 的优先级更高。

for 循环里的经典输出题,几乎每次面试都会出现:

for (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 0); } // 输出 5 5 5 5 5

换成 let 后输出 0 1 2 3 4。面试官会追问“为什么”,这里有两个层面的理解:

第一层(var 版本):var 声明的是全局变量,循环结束后 i 已经变成 5,五个定时器的回调函数读取的都是同一个 i。

第二层(let 版本):let 声明的是块级变量,for 循环的每次迭代会创建一个新的词法环境,闭包捕捉的是当次迭代的 i,所以每个回调拿到的 i 值不同。这其实涉及闭包的知识,放到下一节展开。

const 的特殊性也要单独提:const 声明时必须赋值,且不能重新赋值。但“不能重新赋值”不等于“不可变”——对象内部的属性仍然可以改。

const obj = { name: "张三" }; obj.name = "李四"; // 允许 obj = {}; // TypeError: Assignment to constant variable

这个点面试官经常用来引出深拷贝、不可变数据这些进阶话题。我建议你在准备时顺手把Object.freeze和 const 的区别也梳理清楚:Object.freeze是冻结对象,让属性不可修改,但它是浅冻结,嵌套对象仍然能改。

2. 闭包与 this 指向:面试官最爱的两个陷阱

2.1 闭包的本质与内存问题

如果说变量提升是开场菜,闭包就是前端面试的主菜。面试官可能会让你“说说什么是闭包”,也可能直接甩一道“用闭包实现计数器”的手写题。这道题本身不难,但很多人讲不明白闭包产生的机制。

闭包的本质就一句话:函数 + 它创建时所在的词法环境。当内部函数引用了外部函数的变量,且内部函数被返回或传递到外部时,外部函数的执行上下文虽然已经“销毁”,但它的变量对象仍然被内部函数引用着,不会从内存中消失,这就形成了闭包。

一个规范的计数器实现:

function createCounter() { let count = 0; return function () { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2

面试常见追问是“闭包有什么应用场景”。这个问题要想答得漂亮,得结合真实业务,只背“保存变量”太单薄了。常见的场景有:

  • 防抖与节流:通过闭包保存定时器 id 和上次执行时间。
  • 封装私有变量:在模块模式(IIFE + 闭包)里实现外部无法直接访问的内部状态。
  • 函数柯里化:通过闭包记住逐步传入的参数。

如果面试官继续追问“闭包会导致内存泄漏吗”,这里要小心,这不是一个简单的“会”或“不会”。闭包本身不必然导致内存泄漏,但如果你长期持有一个大对象的引用,而这个引用本可以释放,那它确实会造成内存占用过高。经典错误是:在全局环境中保存了一个闭包,闭包里引用了一个永远不会再用的 DOM 节点或大数据对象。

function handleClick() { const bigData = new Array(10000000).fill(1); document.getElementById("btn").onclick = function () { console.log("clicked"); }; }

这里的bigData明明没被回调函数用到,但因为闭包保留了整个词法环境,bigData仍然被隐式引用着。排查手段是打开 Chrome DevTools 的 Memory 面板,录制堆快照,搜索不期望存在的对象。实际代码里,可以在回调函数内部手动bigData = null,或者用 WeakMap 管理这类不需要强引用的数据。

2.2 this 指向的四种绑定规则与箭头函数陷阱

this 指向问题几乎是一道必考题,而且经常以“输出题”的形式出现。我见到的最典型的错误答案是:这个候选人背了“谁调用就指向谁”的口诀,但遇到let obj = { fn: fn }这种赋值引用场景,或者const test = obj.fn之后再调用,就完全乱了。

规范的做法是记四种绑定规则,按优先级从低到高:

  1. 默认绑定:独立调用函数时,非严格模式下 this 指向 window/globalThis,严格模式下是 undefined。
  2. 隐式绑定:函数作为对象的方法调用时,this 指向该对象。
  3. 显式绑定:call、apply、bind 手动指定 this。
  4. new 绑定:通过 new 调用构造函数时,this 指向新创建的对象。

优先级关系:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

最容易丢分的是隐式绑定丢失的场景:

const obj = { name: "obj", fn() { console.log(this.name); }, }; const test = obj.fn; test(); // undefined 或报错,this 指向 window/undefined

把方法单独取出来调用,隐式绑定就丢了,回到了默认绑定。setTimeout 传回调时也一样:

setTimeout(obj.fn, 100); // this 指向 window/undefined

修复方法是用箭头函数或者 bind:

setTimeout(() => obj.fn(), 100); // 或 setTimeout(obj.fn.bind(obj), 100);

这里自然过渡到箭头函数。箭头函数没有自己的 this,它的 this 由外层作用域决定,而且一经确定不可改变——所以 call/apply/bind 对箭头函数无效。这个特性让箭头函数在处理回调场景时非常省心,但也带来了一个面试高频题:箭头函数能不能当构造函数?答案是不能。因为它没有自己的 this,也没有 prototype 属性,new 一个箭头函数会直接报错。

3. 原型链与继承:JS 面向对象的核心考区

3.1 原型链的查找机制

原型链是 JS 里最抽象、最劝退的一类知识点,但也是面试官检验候选人“有没有系统学过 JS”的分水岭。很多人能说出“对象通过__proto__向上找”,但说不清楚prototype__proto__到底是什么关系。

首先要区分三个容易混的概念:

  • prototype:函数才有的属性,指向一个对象,这个对象会作为该函数构造出来的实例的原型。
  • __proto__:每个对象都有的属性(除了Object.create(null)创建的对象),指向它自己的原型。
  • constructor:原型对象上的一个属性,指回构造函数本身。

当访问一个对象的属性时,JS 引擎的查找顺序是:先查自身属性,查不到就顺着__proto__往上找,一直找到Object.prototype,再往上就是null,查找结束。

function Person(name) { this.name = name; } Person.prototype.sayHi = function () { console.log("Hi, " + this.name); }; const p = new Person("张三"); p.sayHi(); // Hi, 张三

为什么 p 能调用到 sayHi?因为p.__proto__就是Person.prototype,所以沿着原型链找到了 sayHi。而p.toString能调用,是因为继续向上找到了Object.prototype.toString

面试官此时喜欢问原型链的应用场景,最典型的就是属性共享方法共享。你在构造函数的 prototype 上定义的方法,所有实例共享同一个函数引用,不会每个实例都复制一份,这在内存上非常高效。这也是为什么“方法定义在 prototype 上”是常见的写法约定。

还会追问的一个点是:instanceof的工作原理。instanceof会沿着左侧对象的原型链查找,看右侧函数的 prototype 是否出现在这条链上。所以:

p instanceof Person; // true p instanceof Object; // true

这两个都是 true,因为 Person.prototype 和 Object.prototype 都在 p 的原型链上。这也顺带解释了为什么所有对象都能调用 Object 的方法——它们都能在原型链末端找到 Object.prototype。

3.2 五种继承方式对比与手写

继承的实现方式是 JS 面试的经典保留项目,从 ES5 到 ES6 演化出了一系列方案。我给候选人面试时,通常期望听到的不是“我会用 class extends”,而是能讲清楚各种继承方式的优缺点,以及为什么最终落到寄生组合继承。

先快速过一下五种继承方式:

继承方式核心代码思路主要缺点
原型链继承Child.prototype = new Parent()父类引用类型属性会被所有实例共享,且无法向父类构造函数传参
构造函数继承在 Child 中执行Parent.call(this)无法继承 Parent.prototype 上的方法
组合继承原型链 + 构造函数,Child 中 call 父类,Child.prototype 指向 new Parent()父类构造函数被调用了两次,子类原型上有冗余属性
寄生组合继承Child.prototype = Object.create(Parent.prototype),并修正 constructor最优解,ES6 class 的底层原理
ES6 class extendsclass Child extends Parent+ super本质上是寄生组合继承的语法糖

这里重点看寄生组合继承的标准实现,面试手写题如果考到,直接写这个版本:

function inheritPrototype(Child, Parent) { const prototype = Object.create(Parent.prototype); prototype.constructor = Child; Child.prototype = prototype; } function Parent(name) { this.name = name; } Parent.prototype.sayName = function () { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); this.age = age; } inheritPrototype(Child, Parent); Child.prototype.sayAge = function () { console.log(this.age); };

有人会问为什么不用Child.prototype = new Parent()这种更简单的写法,原因是:new Parent()会执行父类构造函数,在子类原型上留下一份冗余的实例属性;而Object.create(Parent.prototype)只是让子类原型继承父类原型的方法,不执行父类构造函数,干净又高效。

ES6 的 class 底层逻辑就是这个,面试官如果追问“class 和 ES5 继承有什么区别”,可以从两个层面答:一是语法层面,class 更接近传统面向对象语言的写法;二是底层行为,class 继承必须调用 super,而且 class 类内的代码自动处于严格模式,类声明不会提升,new 之前必须定义类。

4. 事件循环与 Promise:异步编程的高频考区

4.1 宏任务与微任务的执行顺序

异步编程是前端面试的重头戏,JavaScript 是单线程语言,但通过事件循环机制实现了“看起来像多线程”的能力。面试官常用的考法是给你一段混合 setTimeout、Promise、async/await 的代码,让你写出输出顺序。这种题看着复杂,其实只要掌握事件循环的调度规则就能稳拿分。

核心规则可以拆成四步:

  1. 每轮事件循环,先从宏任务队列里取出一个宏任务执行(首次执行整段脚本算作第一个宏任务)。
  2. 宏任务执行过程中,遇到微任务则放入微任务队列,遇到新的宏任务则放入宏任务队列。
  3. 当前宏任务执行完,清空整个微任务队列(微任务中产生的微任务也会在这一轮全部执行完)。
  4. 微任务清空后,进行渲染更新(时机是每帧渲染前),再取下一个宏任务。

先上经典例题:

console.log("script start"); setTimeout(() => { console.log("setTimeout 1"); }, 0); Promise.resolve() .then(() => { console.log("promise 1"); }) .then(() => { console.log("promise 2"); }); setTimeout(() => { console.log("setTimeout 2"); }, 0); console.log("script end");

输出顺序是:script start → script end → promise 1 → promise 2 → setTimeout 1 → setTimeout 2。

原理不复杂:script start 和 script end 是同步代码,先执行;两个 setTimeout 是宏任务,进入宏任务队列;Promise 的 then 是微任务,进入微任务队列。同步代码执行完后,清空微任务队列,所以 promise 1 和 promise 2 先于两个 setTimeout 输出。两个 setTimeout 之间按进入队列的顺序执行,所以 setTimeout 1 在前。

关于 Promise 和 setTimeout 谁先执行的问题,从上面例子来看是 Promise 先,但这只在“同步代码后的第一轮微任务清空”的情况下成立。如果你在 setTimeout 里再创建 Promise,那就要看定时器何时回调。这种嵌套场景特别容易考,建议练习时自己多组合几段代码。

微任务里还有一个细节:process.nextTick(Node 环境)优先级高于 Promise 的 then。浏览器环境没有 nextTick,只有queueMicrotask。如果你在简历里写了 Node 后端经验,面试官可能会顺带问 Node 的事件循环有哪几个阶段,比如 timers、poll、check 这几个阶段的大致分工,建议准备 Node 方向的同学也复习一遍。

4.2 Promise 的常见 API 与手写

Promise 相关的手写题在面试中出现的频率极高,常见的包括手写 Promise.all、手写 Promise.race、手写一个简易版 Promise,以及 async/await 的原理说明。

先看一组易混 API 的对比:

API行为适用场景
Promise.all全部成功才成功,返回结果数组;一个失败就立即失败多个请求之间无依赖但需要同时等待
Promise.allSettled等所有 Promise 都结束,返回每个成功/失败的结果不关心失败,只关心全部状态
Promise.race第一个结束的 Promise 决定结果,无论成功失败请求超时控制
Promise.any第一个成功的 Promise 决定结果;全部失败才失败多接口资源,取最快成功的

笔试常错题是 Promise.all 的“快速失败”行为。注意:它是“立即失败”,但其他 Promise 并不会被取消,它们仍然在后台继续执行,只是你不会再收到它们的结果。这个点需要跟面试官明确说出来,会显得你理解更深。

手写 Promise.all 是高频手写题,一个标准实现如下:

function promiseAll(promises) { return new Promise((resolve, reject) => { const results = []; let count = 0; promises.forEach((promise, index) => { Promise.resolve(promise).then( (value) => { results[index] = value; count++; if (count === promises.length) { resolve(results); } }, (reason) => { reject(reason); } ); }); }); }

这里有两个关键细节:一是用Promise.resolve(promise)包装,兼容传入非 Promise 的普通值;二是结果数组按下标存储,保证结果顺序与入参顺序一致,而不是完成顺序。这两点都是面试官会追问的“考点”。

async/await 的本质也要说清楚:它是 Generator + Promise 的语法糖。await 后面的表达式会被包装成 Promise,await 的下一行代码相当于 then 回调。所以 async 函数内部任何异常,都会被包装成 reject 的 Promise,你可以在外部用 try/catch 或 .catch 捕获。

5. 数组与字符串高频方法:笔试与手写的重灾区

5.1 数组方法全家桶:哪些会改变原数组,哪些不会

数组方法这块,面试官很少直接问你“map 和 forEach 有什么区别”,而是通过输出题、手写题来考察。比如给出[1,2,3].map(parseInt)这种经典题,问输出结果。这题考的是 map 回调函数的参数:map 会传入(当前值,当前索引,数组本身),而 parseInt 的第二个参数是 radix,所以 parseInt(1, 0)、parseInt(2, 1)、parseInt(3, 2) 的结果分别是 1、NaN、NaN。

所以先把“会修改原数组”和“不会修改原数组”的方法按阵营理清:

分类方法说明
修改原数组push、pop、shift、unshift、splice、reverse、sortsplice 用于增删改;sort 默认按字符串编码排序
不修改原数组map、filter、reduce、forEach、find、findIndex、some、every、includes、flat、slice返回新数组或布尔值/元素

forEach 和 map 的区别要能说清楚:forEach 只是为了遍历,没有返回值(或返回 undefined);map 会把每次回调的返回值收集成一个新数组。所以需要“把原数组转换成一个新数组”的时候就选 map,不需要结果时用 forEach。

reduce 是面试中的进阶考点,它可以在一次遍历中完成很多逻辑。最常见的用法是求和:

const sum = [1, 2, 3, 4].reduce((acc, cur) => acc + cur, 0); // 10

还有“按属性分组”这个高频场景:

const grouped = people.reduce((acc, person) => { const key = person.city; if (!acc[key]) { acc[key] = []; } acc[key].push(person); return acc; }, {});

这个方法如果你不熟悉,可以先在本地写几遍。reduce 能做很多事情,包括扁平化数组、管道函数组合、统计出现次数等。面试官如果问“数组扁平化有几种写法”,你可能用到 flat、reduce + concat、JSON 字符串替换,或者递归实现。这道题考察的其实是“对一个 API 的熟练度和边界情况考虑”。

字符串方法在热点词里也出现了“js replace 方法”“js 字符串长度”这类词,说明这是新手面试容易踩坑的地方。replace 最容易错的地方是“默认只替换第一个匹配”,必须用正则加全局标志 g 才能替换全部:

"hello world hello".replace("hello", "hi"); // "hi world hello" "hello world hello".replace(/hello/g, "hi"); // "hi world hi"

字符串长度这块也有个常见误解:"hello".length是 5 没争议,但"😀".length也是 2,因为 emoji 是双字节字符。遇到 UTF-16 代理对,想准确统计用户看到的字符数量,得用Array.from(str).length或者展开运算符[...str].length。这些小细节很容易被深挖。

5.2 手写题中的高频考点:防抖、节流与深拷贝

手写题是前端面试的“压轴题”,面试官想看的不是你能不能默写代码,而是你思考问题的过程。这里挑三个最高频的面试手写题详细讲。

第一个是防抖(debounce)和节流(throttle)。这两个概念的核心区别是:防抖是“在一段连续触发后,只执行最后一次”;节流是“在一段时间内只执行一次”。防抖适合搜索框输入,节流适合滚动事件。

防抖实现:

function debounce(fn, delay = 300) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }

这里有个关键细节是fn.apply(this, args),它保证回调里的 this 指向调用者,而不是 window。很多候选人写手写题时忽略 this 和参数的处理,面试官一眼就能看出来你是背的模板。

节流实现(时间戳版本):

function throttle(fn, interval = 300) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; fn.apply(this, args); } }; }

如果需要“第一次立即执行、停止后还能再执行一次”这种边缘行为,就要用定时器版本,或者时间戳 + 定时器组合。面试时,能主动说出这两种方式的差异,很加分。

第二个是深拷贝。手写深拷贝通常的要求是:能处理对象、数组、循环引用、函数、Date、RegExp 这些常见类型。一个比较标准的递归版本:

function deepClone(target, map = new WeakMap()) { if (target === null || typeof target !== "object") { return target; } if (map.has(target)) { return map.get(target); } const clone = Array.isArray(target) ? [] : {}; map.set(target, clone); Reflect.ownKeys(target).forEach((key) => { clone[key] = deepClone(target[key], map); }); return clone; }

用 WeakMap 是为了解决循环引用问题:const obj = {}; obj.self = obj;如果不用 map 记录已克隆对象,会无限递归直接爆栈。这个细节是手写深拷贝的经典考点。

面试里常见的浅拷贝方式也要能说出来:展开运算符{...obj}Object.assign、数组的sliceconcat。它们都只是拷贝了第一层属性,嵌套对象仍然是引用。还有一个容易忽略的:JSON.parse(JSON.stringify(obj))能实现深拷贝,但会丢失函数、undefined、Symbol 属性,并且 Date 会变成字符串、RegExp 会变成空对象,所以有特殊类型的对象不能用它。

第三个高频题是数组去重。解法有很多,从最早的双循环、indexOf,到 Set、filter + Map,再到要考虑对象去重、NaN 去重,层层递进。最简单的 Set 版如下:

const unique = [...new Set([1, 2, 2, 3, NaN, NaN])]; // [1, 2, 3, NaN]

这里有个冷知识:Set 对 NaN 的判断是“认为 NaN 等于 NaN”,所以 NaN 去重可以成功。但[NaN].indexOf(NaN)返回 -1,因为 indexOf 用的是严格相等,而 NaN !== NaN。这种细节很能体现候选人对 JS 语义的掌握程度,面试官一旦问到你就能顺手举出来。

字符串的“判断是否包含”、“扩展运算符合并数组”也是在热搜词里反复出现的 JS 基础操作,我顺便提一下答案:字符串判断包含首选includes,其次是indexOf;扩展运算符合并数组就是const merged = [...arr1, ...arr2],相比 concat 更直观,也经常用于浅拷贝数组。

6. 面试答题的组织思路与避坑经验

6.1 面试官视角:什么样的回答能拿高分

我以面试官视角分享一下真实感受:大部分候选人不是在“答错”,而是“不会组织答案”。比如问闭包,候选人直接说“函数套函数”,这种回答等于没答。真正高分回答是有一个清晰的框架:

  1. 讲本质:闭包是函数与其词法环境的组合;
  2. 讲机制:内部函数引用了外部函数的变量,外部执行上下文销毁后变量仍被引用;
  3. 讲应用:防抖、节流、私有变量、柯里化;
  4. 讲注意点:内存占用问题、排查方法。

这个框架同样适用于原型链、事件循环等所有知识点。先用一句话给结论,再展开机制,接着举例,最后说坑。面试官其实很怕候选人“背了结论但不会展开”,所以你的回答要尽量做到“由浅入深、有代码、有场景”。

另外一个加分项是主动交代边界条件和限制。比如讲深拷贝时,主动说“JSON.parse 方案会丢函数和 undefined,所以这类实现需要递归”;讲 Promise.all 时,主动说“一个失败立即 reject,但其他 Promise 不会被取消”。主动暴露“我知道这个方案的局限”恰恰说明你真的用过,而不是只背了文档。

6.2 常见的踩坑点和复习建议

准备 JS 面试时,除了背八股文,我强烈建议你动手手写一遍,并且用浏览器跑一遍输出题。有几个踩坑点值得重点练习:

  • parseIntmap组合的坑:["1", "2", "3"].map(parseInt)的结果是[1, NaN, NaN],原因之前说过,是 radix 参数被索引覆盖。想避免的话写map((item) => parseInt(item))或者直接用Number
  • sort默认按字符串排序:[10, 9, 100].sort()得到[10, 100, 9],因为比较的是字符串编码。要传比较函数(a, b) => a - b才能按数字大小排。
  • =====的隐式类型转换:null == undefined是 true,但null === undefined是 false。0 == false也是 true。实际开发中永远用===,严格相等不会做隐式转换。
  • mapfilter并不会跳过稀疏数组的坑:forEach会跳过空位,但map会保留空位,并用回调处理非空位。比如[1, , 3].map(x => x * 2)的结果是[2, , 6],中间那个空位仍然存在。

复习建议上,我会推荐一个顺序:先过基础语法题(变量提升、类型转换、作用域),再过闭包和 this,这两块是“理解型知识”,不会就是真不会;接着过原型链和继承,这块偏“抽象记忆”,需要画图辅助理解;最后是异步和手写题,这块需要大量刷题来形成肌肉记忆。把每个知识点都整理成一个“结论 + 机制 + 例子 + 坑”的小卡片,面试前翻一遍比临时抱佛脚背大段文档高效得多。

6.3 一个用来自检的综合输出题

最后给大家留一道综合练习题,它几乎覆盖了前面讲到的所有核心考点:

async function async1() { console.log("async1 start"); await async2(); 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("promise executor"); resolve(); }).then(() => { console.log("promise then"); }); console.log("script end");

我在面试中见过这道题的变体出现很多次,它同时考了同步代码执行顺序、async/await 的暂停与恢复、宏任务与微任务的调度。输出结果是:script start → async1 start → async2 → promise executor → script end → async1 end → promise then → setTimeout。

这里最容易错的是 async1 end 和 promise then 的顺序。记住 await 会交出线程控制权,async1 end 是作为微任务排在 await 之后的。从“同步代码结束后先清空微任务队列”的角度看,async1 end 和 promise then 都属于微任务,谁先进入队列谁先执行,所以 async1 end 在前。大家可以在本地跑一下,亲眼看效果比背答案有用得多。

前端面试的 JS 八股文是一个整体,这几个核心专题弄明白后,衍生出来的手写题和场景题基本都能应付。下一篇我会接着整理 Promise 进阶、事件循环在 Node 端的差异、以及更多场景面试题,大家先把这一篇吃透再说。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 4:16:16

2026年7月三明市新房价格深度分析报告

一、报告背景与数据说明本报告基于2026年7月三明市新房实际成交案例&#xff0c;结合成交价格、成交面积、成交区域分布等维度&#xff0c;对当前三明市新房市场进行深度分析。报告数据来源于2026年7月三明市各主要城区新房项目的实际成交记录&#xff0c;覆盖梅列区、三元区、…

作者头像 李华
网站建设 2026/9/5 10:36:06

基于SG3525的600W大功率DC-DC升压电路设计与实战指南

你是否曾遇到过这样的困境&#xff1a;手头有一个12V的铅酸电池&#xff0c;却需要驱动一个24V、数百瓦的工业设备&#xff1f;或者&#xff0c;在太阳能供电系统中&#xff0c;需要将不稳定的低压直流电高效、稳定地提升到可用的高压&#xff1f;面对这类需求&#xff0c;一个…

作者头像 李华
网站建设 2026/9/6 5:07:01

数字半色调原理详解(三)

目录 12.2 周期性抖动算法 抖动点阵成像规律 12.2.1 聚簇型有序抖动 12.2.2 分散网点型有序抖动 12.2 周期性抖动算法 有序抖动滤波器是周期性抖动算法当中十分重要的一类。此处 “有序” 与前文的 “随机” 相对应&#xff1a;该类算法的核心思路是采用一个有限、确定、局部…

作者头像 李华
网站建设 2026/9/5 11:36:51

Docker Compose 模块化多环境配置规范指南:示例搭建kkFileView 4.1.0

Docker Compose 模块化多环境配置规范指南&#xff1a;示例搭建kkFileView 4.1.0Docker Compose 模块化多环境配置规范指南示例&#xff1a;kkfileview_V4.1.0目录结构规范配置文件详解与完整注释1. 全局公共环境配置&#xff1a;dev/.env2. 服务私有环境配置&#xff1a;dev/k…

作者头像 李华
网站建设 2026/9/4 21:04:08

C语言结构体:从数据碎片到数据实体的编程思维转变

如果你刚开始学C语言&#xff0c;可能觉得变量、数组、函数这些概念已经够用了。直到你遇到一个真实问题&#xff1a;需要同时记录一个学生的学号、姓名、年龄和成绩&#xff0c;或者需要管理一个商品的信息&#xff0c;包括编号、名称、价格和库存。你发现&#xff0c;用一堆零…

作者头像 李华
网站建设 2026/9/4 7:54:19

从ABCD矩阵到EET:电路传递函数推导的两种高效路径

调试一个硬件问题&#xff1a;在运放反馈回路里多并联了一个电容&#xff0c;理论上只增加一个极点&#xff0c;可仿真曲线却显示相位提前了&#xff0c;手算怎么都对不上。后来才意识到&#xff0c;新增电容不止影响极点&#xff0c;还悄悄引入了一个零点&#xff0c;而手工重…

作者头像 李华