资讯动态

JavaScript面试高频题全解析:从this闭包到Promise手写

发布时间:2026/8/29 22:49:20 来源:尧图企业网站定制
1. 写在前面这个 js 系列2到底要解决什么问题上一篇文章我把 JavaScript 的基础数据类型、作用域、原型链这些地基性质的内容梳理了一遍评论区不少读者留言说“面试时真正卡壳的不是背概念而是面试官拿着代码让我猜输出、让我手写实现”这句话我太有共鸣了。前端面试走到现在这个阶段八股文早就不是“背下来就行”而是要用自己的话把原理讲清楚并且能在白板上写出能跑通的代码——JS 系列2就是冲着这个目标去的。这篇文章我会把面试里出现频率最高的几个 JS 方向挨个拆开包括 this 指向与闭包、事件循环里的宏任务微任务、Promise 和 async/await 的手写与原理、数组方法的高阶用法以及几道经常会作为压轴题出现的综合场景题比如 iframe 刷新父页面、大文件上传里的 Worker 应用。每一块我都会先讲清楚面试官到底想听到什么再给出一套可以直接背下来然后“翻译”成自己话的答题框架最后配上实操代码和我在面试别人时实际遇到的“加分点”和“扣分点”。如果你正在准备前端面试或者已经工作两三年但觉得 JS 基础还有不少模糊地带这篇文章应该能帮你把零散的知识点串成一张网。阅读之前你最好已经知道变量提升、原型链、闭包的基本定义但我也会在关键位置做回顾纯新手不会完全看不懂只是建议先过一遍 JS 基础语法再来读效果会更好。2. 工具类方法面试题字符串、URL、数组操作的高频变体面试刚开始的阶段面试官通常不会直接甩一道“说说闭包”这种大题目而是先用几个小函数题热热身顺便看看你写代码的基本功。这类题看似简单但恰恰能暴露很多问题——比如是否知道 ES6 的 API是否注意边界条件是否写过真正在生产环境里能用的代码而不是只在 LeetCode 里“通关”。2.1 判断字符串是否包含某个子串别只答 indexOf“js 判断字符串是否包含”这个话题在热搜词里出现了好几次面试题也喜欢从这个角度切入。最朴素的写法是用indexOf返回值大于等于 0 就代表包含const str Hello, frontend engineer; console.log(str.indexOf(frontend) ! -1); // true但到了 2026 年再这样回答面试官大概率会追问一句“还有更语义化的方式吗”这时候你必须要说出includes方法console.log(str.includes(frontend)); // true console.log(str.includes(Frontend)); // false大小写敏感includes和indexOf的核心区别有两个。第一是返回值语义includes直接返回布尔值不用再手动做! -1判断第二是 NaN 的处理includes能正确判断字符串中是否包含NaN但indexOf做不到这在数组场景里也一样const arr [1, 2, NaN]; console.log(arr.indexOf(NaN)); // -1找不到 console.log(arr.includes(NaN)); // true这个细节非常经典很多面试者在这里翻车。如果面试官进一步追问“如何忽略大小写判断包含”那就把字符串统一转换成小写再判断就行。还有一种更进阶的需求是“统计子串出现次数”可以用split的返回值长度减一也可以用matchAll配合正则我个人在实际项目中更推荐split因为可读性最好function countOccurrences(str, sub) { return str.split(sub).length - 1; } console.log(countOccurrences(a-b-c-, -)); // 32.2 校验 URL 是否有效从正则到浏览器 API 的演进“js 验证 url 有效性”也是常见面试题很多人第一反应就是“写个正则”。但如果面试官把题面展开——请你封装一个isValidUrl函数并说明它能在浏览器和 Node 环境都工作——情况就复杂了。最直接的方案是用URL构造函数它能解析合法 URL 并暴露协议、域名、路径等属性同时一旦传入非法字符串就会抛异常function isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } console.log(isValidUrl(https://example.com/path?a1#hash)); // true console.log(isValidUrl(not-a-url)); // false但注意new URL对相对路径会报错所以如果你要判断的是“相对路径是否合法”这个方案就不适用了。更精益求精的写法可以自己实现一个相对宽松的判定逻辑先判断是否有协议头再判断域名部分是否包含点号或是否为 localhost同时利用new URL做兜底解析。这里的关键考点是让面试官看到你有“异常处理意识”和“边界场景思维”。还有一个进阶追问是“如何判断 URL 的协议是 http 还是 https”这个直接用URL.protocol就行function isHttpProtocol(url) { try { const parsed new URL(url); return parsed.protocol http: || parsed.protocol https:; } catch { return false; } }实际工作中这类工具函数经常被封装在utils/url.js里所以面试时体现出代码的健壮性和可复用性是很加分的。2.3 用扩展运算符合并数组不只是“把值加进去”“js 怎么用扩展运算符把一个数组里面的值都添加到另外一个数组”这个热搜关键词对应的题目其实很简单但衍生出来的考点特别多。基础写法大家都会const arr1 [1, 2, 3]; const arr2 [4, 5]; const merged [...arr1, ...arr2]; console.log(merged); // [1, 2, 3, 4, 5]面试官真正想考察的是你知不知道扩展运算符只能做“浅拷贝”。如果数组元素是引用类型合并后的两个数组会共享同一个对象引用const user { name: Alice }; const list1 [user]; const list2 [...list1]; list2[0].name Bob; console.log(list1[0].name); // Bob被改到了这时候你得能引出“深拷贝 vs 浅拷贝”的比较。展开运算符在拷贝对象时也有同样的局限真正做深拷贝要么用structuredClone要么用JSON.parse(JSON.stringify(obj))但后者有缺陷——函数、undefined、Symbol、循环引用都会丢失或报错。面试中如果能主动把这些边界条件说出来面试官对你的评价会明显上一个档次。还有一个高频衍生题是“合并数组并去重”常用解法是Array.from(new Set([...arr1, ...arr2]))或者用[...new Set([...arr1, ...arr2])]后者更简洁。这里也顺便提一下扩展运算符同样适用于函数参数传递的场景比如Math.max(...arr)面试题也喜欢拿它来让你写“求数组最大值”的优雅解法。3. this、闭包与作用域每次面试都躲不开的送命题如果说工具方法题是“热身”那 this 和闭包就是前端面试正餐里的“硬菜”。几乎每一轮技术面都会出现而且经常是连环追问问到你怀疑人生。好好理解这一章的内容你在面试现场的底气会完全不一样。3.1 this 指向的四种绑定规则别再死记硬背口诀关于 this最经典的一句话是“this 指向函数被调用时的上下文而不是定义时的上下文”。但这句话太抽象了我一般会让面试者掌握四类规则按优先级排列。第一是new绑定用new调用构造函数时this 指向新创建出来的实例对象。第二是显式绑定call、apply、bind会强制指定 this。第三是隐式绑定通过对象方法调用的函数this 指向该对象比如obj.run()里 run 的 this 是obj。第四是默认绑定在非严格模式下独立函数调用的 this 指向全局对象浏览器里是windowNode 里是global严格模式下是undefined。我面试时特别爱问的一道题是const person { name: Alice, greet() { console.log(this.name); } }; const fn person.greet; fn();很多人脱口而出Alice但实际上输出的是undefined严格模式下会直接报错因为fn是独立调用走了默认绑定this 指向全局对象。这里有一个很实用的判断技巧函数前面有“点”就看点前面的对象没有点就看有没有call/apply/bind/new都没有就是默认绑定。函数嵌套也是高频考法const obj { name: obj, arrow: () console.log(this.name), regular() { console.log(this.name); } }; obj.arrow(); // 取决于定义时外层作用域的 this不是 obj obj.regular(); // obj箭头函数没有自己的 this它捕获的是定义时所在词法作用域的 this。所以在对象方法里嵌套箭头函数时箭头函数能访问外层方法的 this这是实际开发中非常常见的用法比如在setTimeout回调里想访问外层 this用箭头函数最省事。这也是面试官最想听到的“为什么用箭头函数”的场景。3.2 闭包是什么面试官想听的答案其实就三层闭包的书面定义是“函数与其词法作用域的组合”但这样回答基本拿不到高分因为太“教科书”。我在面试时更希望听到拆成三层的解释。第一层是概念当一个内部函数引用了外部函数的变量并且在外部函数执行结束后仍然被保留下来就形成了闭包。第二层是原理JavaScript 作用域是词法作用域函数定义时的作用域链会保留下来即使外层函数执行完内部函数依然可以通过作用域链访问到那些变量。第三层是应用闭包常用于数据私有化、模块化开发、防抖节流、柯里化等。举个最经典的计数例子function createCounter() { let count 0; return function () { count 1; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2这里的count就是私有变量外部无法直接访问只能通过返回的函数去修改。这个模式在真实开发里对应的是“模块封装”——比如一个状态管理工具用闭包把状态藏在函数内部对外只暴露getState和setState。面试官接着大概率会问“闭包有什么缺点”这时候要答出两点一是内存占用闭包会让变量长期保存在内存中处理不当会造成内存泄漏二是性能问题频繁创建闭包可能带来额外的内存开销。然后一定要补一句现代浏览器引擎对闭包做了很多优化不能一杆子打死关键在于“用完的引用要置为 null”或者“避免过长的闭包链”。能主动说出这个“优化视角”的候选人在我这里会加分不少。3.3 经典送命题循环 定时器 var/let这道题是闭包和 var/let 区别的终极结合体我必须单独拉出来写因为它出现频率实在太高了for (var i 0; i 3; i) { setTimeout(() console.log(i), 0); } // 输出3 3 3原因拆开讲有两点。第一var声明的变量是函数作用域循环里就一个i循环结束后i变成 3第二setTimeout的回调是宏任务会在当前同步代码全部执行完之后才执行所以执行回调时看到的i已经是 3 了。如果把var换成letfor (let i 0; i 3; i) { setTimeout(() console.log(i), 0); } // 输出0 1 2因为let是块级作用域每一轮循环都会生成一个独立的i绑定定时器回调闭包捕获的是各自那一轮的i。这背后还有一个面试官可能追问的细节for 循环的圆括号和花括号之间的作用域关系——其实每次迭代都会用上一轮的值创建一个新的词法环境。如果面试官说“我已经知道原因了那如果坚持用var你还有什么办法让它输出 0 1 2”这时候你有两种思路一是立即执行函数IIFE创建独立闭包二是用bind预先传参for (var i 0; i 3; i) { setTimeout(console.log.bind(null, i), 0); }这道题能延伸出很多变体比如“把console.log换成console.log(i, i)呢”“把延迟设为 1000 呢”但核心原理都是一样的理解事件循环和块级作用域之后灵活应对就好。4. 事件循环与异步宏任务微任务的底层逻辑很多面试者面对事件循环题时只背了一个“先同步、再微任务、再宏任务”的口诀但一旦遇到await、Promise、setTimeout混在一起的代码就会算错输出顺序。这一章我把底层模型掰开来讲并配两道足够典型的代码题。4.1 浏览器事件循环里的两类任务首先要明确JavaScript 是单线程语言一次只能执行一个任务。事件循环可以理解成一个大仓库里的分拣员它从“任务队列”里不断取出任务执行。任务分两类同步任务立即执行阻塞后续代码。异步任务先挂起等时机成熟后被推入对应队列。异步任务又分宏任务setTimeout、setInterval、I/O、UI 渲染等和微任务Promise.then、queueMicrotask、MutationObserver等。事件循环的核心规则是每一个宏任务执行完毕后必须先把当前微任务队列清空再取下一个宏任务。微任务可以继续产生微任务所以理论上如果微任务递归不停宏任务永远等不到执行。这里有一个很关键的比喻宏任务队列就像“顾客排队窗口”微任务队列就像“每个顾客手里的加急订单”。顾客宏任务结账后营业员必须先把这位顾客的加急订单微任务处理完才喊下一位顾客。4.2 经典输出顺序题一次讲透直接上一道非常典型的题目console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); Promise.resolve() .then(() { console.log(promise 1); }) .then(() { console.log(promise 2); }); console.log(script end);正确的输出顺序是script start script end promise 1 promise 2 setTimeout过程拆解如下同步代码先执行输出script start遇到setTimeout就把回调放入宏任务队列遇到Promise.resolve().then就把回调放入微任务队列继续执行同步代码输出script end当前宏任务结束清空微任务队列输出promise 1然后promise 1的回调执行时又注册了promise 2的微任务于是继续输出promise 2微任务清空后从宏任务队列取出setTimeout回调输出setTimeout。很多背了口诀但原理不透的人会把promise 2排到setTimeout后面这就是因为没有意识到微任务队列是“链式处理”的新产生的微任务会排到当前队列尾部继续执行而不是跑到下一轮。面试时如果能在回答中主动加上“这里 promise 2 仍然属于同一轮事件循环的微任务阶段”基本就稳了。题目再来一个升级版混入async/awaitasync function foo() { console.log(foo start); await bar(); console.log(foo end); } async function bar() { console.log(bar); } foo(); console.log(main end);输出的顺序是foo start、bar、main end、foo end。这里的考点是await后面的代码不会立即执行而是被当成微任务延迟执行。await bar()会先同步执行bar()内部的代码只有bar()返回的 Promise 完成之后foo end才会进入微任务队列。4.3 async/await 的本质与错误处理技巧async/await本质上是 Promise 的语法糖。await后面可以跟任意值如果跟的不是 Promise会先被Promise.resolve()包裹再进入微任务调度节奏。理解这一点后很多“为什么await 1也会异步执行”这类问题就迎刃而解了。实际开发里async/await最关键的坑是错误处理。有些人这样写async function loadData() { const response await fetch(/api/data); const data await response.json(); console.log(data); }如果接口报错这个函数里没有 try/catch错误会变成未处理的 Promise rejection。面试官会问“你怎么处理 await 抛出的异常”两个方案一是 try/catch 包裹二是用.catch链式兜底async function loadData() { try { const response await fetch(/api/data); const data await response.json(); console.log(data); } catch (error) { console.error(请求失败:, error); } }还有一种比较优雅的方式是写一个to函数把错误和数据解构返回类似 Node 风格async function to(promise) { try { const data await promise; return [null, data]; } catch (error) { return [error, null]; } } const [error, data] await to(fetch(/api/data));这种写法在团队规范里很实用面试时聊到错误处理方案主动提出这种封装思路也会让人眼前一亮。5. Promise 与手写实现从会用到能写面试进行到中段面试官大概率会让你手写一个 Promise 相关的方法不一定从零实现整个Promise但Promise.all、Promise.allSettled、Promise.race这些静态方法都是高频考题。我建议你在准备时不要只看答案最好自己从零写一遍原生Promise的核心逻辑这样面试时无论怎么变题你都能接得住。5.1 从使用场景入手理解 Promise.all 的背景先举个实际需求页面要在同一时间请求用户信息、订单列表、消息通知三个接口等三个都返回后再统一渲染。这种情况最合适的 API 就是Promise.allconst [user, orders, messages] await Promise.all([ fetch(/api/user).then((res) res.json()), fetch(/api/orders).then((res) res.json()), fetch(/api/messages).then((res) res.json()) ]);Promise.all的特点是“有一个失败就整体失败”并且只要有一个 Promise 变 rejected整个 Promise 立即进入 rejected 状态不会等待其他 Promise 完成。这个特性在面试中经常被追问“如果你希望其中一个失败不影响其他请求应该怎么办”答案就是Promise.allSettled。它不会因为某个 Promise 失败而短路会等所有 Promise 都结束然后返回每个任务的结果数组const results await Promise.allSettled([ fetch(/api/user).then((res) res.json()), fetch(/api/orders).then((res) res.json()), fetch(/api/messages).then((res) res.json()) ]); // 每个结果都是 { status: fulfilled, value } 或 { status: rejected, reason }理解了使用场景手写实现就顺理成章了。Promise.all的手写思路很简单遍历传入的 Promise 数组用then收集结果用计数器判断是否全部完成一旦出现 rejected 就直接 rejectfunction myPromiseAll(promises) { return new Promise((resolve, reject) { const results []; let count 0; if (promises.length 0) { resolve(results); return; } promises.forEach((promise, index) { Promise.resolve(promise).then((value) { results[index] value; count 1; if (count promises.length) { resolve(results); } }).catch(reject); }); }); }这里有几个细节要特别留意。第一个是Promise.resolve(promise)包裹因为传入的元素不一定是 Promise可能是普通值做一次包装能保证.then一定存在。第二个是结果数组的下标赋值要保证结果的顺序和传入顺序一致不能因为请求完成先后不同就乱序。第三个是count计数器而不是直接判断results.length promises.length因为如果某个值是undefinedresults的长度仍然会增长但用稀疏数组判断长度会出现错误。5.2 Promise.allSettled 与 Promise.race 的手写思路Promise.allSettled的手写和Promise.all很相似区别在于不用提前 reject而是每个任务结束后把状态记录到结果对象中function myPromiseAllSettled(promises) { return new Promise((resolve) { const results []; let completed 0; if (promises.length 0) { resolve(results); return; } promises.forEach((promise, index) { Promise.resolve(promise).then( (value) { results[index] { status: fulfilled, value }; completed 1; if (completed promises.length) resolve(results); }, (reason) { results[index] { status: rejected, reason }; completed 1; if (completed promises.length) resolve(results); } ); }); }); }Promise.race更简单字面意思就是“赛跑”谁先落定谁说了算function myPromiseRace(promises) { return new Promise((resolve, reject) { promises.forEach((promise) { Promise.resolve(promise).then(resolve, reject); }); }); }实际场景里Promise.race常用来做超时控制比如某接口 5 秒内没返回就报超时。面试时能结合这个场景讲代码的实用感会更强。5.3 从手写 Promise 到理解 async/await 的关系手写Promise本身也经常被当成一道完整面试题。我不建议面试者在白板上把整个Promise现场写出来那是几百行的工作量但你需要理解几个核心机制状态机pending、fulfilled、rejected 只能单向流转、then的链式调用返回新 Promise、微任务调度onFulfilled和onRejected的回调、以及错误穿透.catch能捕获链上任意一个前置 Promise 的异常。一个能反映你理解程度的简版实现是这样的class MyPromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state pending) { this.state fulfilled; this.value value; this.onFulfilledCallbacks.forEach((fn) fn()); } }; const reject (reason) { if (this.state pending) { this.state rejected; this.reason reason; this.onRejectedCallbacks.forEach((fn) fn()); } }; try { executor(resolve, reject); } catch (error) { reject(error); } } then(onFulfilled, onRejected) { if (this.state fulfilled) { onFulfilled(this.value); } if (this.state rejected) { onRejected(this.reason); } if (this.state pending) { this.onFulfilledCallbacks.push(() onFulfilled(this.value)); this.onRejectedCallbacks.push(() onRejected(this.reason)); } } }这个版本是不支持链式调用的简化版面试中如果被追问链式调用如何实现核心就是then内部返回一个新的MyPromise并把 onFulfilled 的返回值传给下一个 then。你能把这个思路说出来面试官就知道你是真的懂了而不是背代码。6. 数组方法进阶map、reduce、forEach 的底层差异与实际应用js 的数组方法也是热搜里的高频区尤其是 map、reduce、forEach 这几个方法面试题基本都会问“它们之间有什么区别”“什么场景用哪个”。这题看似简单但很多工作两三年的开发也说不清底层本质。6.1 forEach 与 map 的本质区别返回值决定一切一句话说清楚forEach是“遍历并执行副作用”不返回新数组map是“遍历并映射成新数组”返回一个长度相同的新数组。所以如果只是对数组元素做打印、修改外部变量等操作用forEach如果你想要一个基于原数组变换后的新数据用map。面试里常考的一道题是const arr [1, 2, 3]; const mapResult arr.map((item) item * 2); const forEachResult arr.forEach((item) item * 2); console.log(mapResult); // [2, 4, 6] console.log(forEachResult); // undefined另外要注意map不会改变原数组它生成的是新数组。但如果你在map的回调里修改了对象类型元素的属性原数组中的对象会被改到因为那是浅拷贝。这一点和之前扩展运算符的浅拷贝问题遥相呼应面试官很可能把它们串起来问。6.2 reduce 的高阶用法从求和到手写 flatMapreduce是面试官最喜欢“考用法”的方法因为它的变形太多。最基础的用途是数组求和const total [1, 2, 3, 4].reduce((acc, cur) acc cur, 0); // 10进阶用法包括数组去重、数组扁平化、按条件分组、统计元素出现次数。我实习带人的时候发现很多人对reduce望而生畏其实是没理解它的两个参数第一个是回调函数回调的第二个参数cur表示当前元素第一个参数acc表示“上一次处理完积累下来的中间结果”第二个是初始值如果不传第一次会把数组的第一个元素当初始值从第二个元素开始遍历。用reduce做扁平化是一个非常典型的面试题function flat(arr) { return arr.reduce((acc, cur) { return acc.concat(Array.isArray(cur) ? flat(cur) : cur); }, []); } console.log(flat([1, [2, [3, [4]]]])); // [1, 2, 3, 4]这题其实也能引出“手写 flat”的完整考点配合面试官追问“如果不允许递归呢”可以用栈来实现这里我就不展开了但建议你在面试前把这两种思路都在本地跑一遍。6.3 手写 map 和 filter 的核心逻辑如果面试官让你手写map直接写实现function myMap(arr, callback) { const result []; for (let i 0; i arr.length; i) { result.push(callback(arr[i], i, arr)); } return result; }手写filter也类似function myFilter(arr, callback) { const result []; for (let i 0; i arr.length; i) { if (callback(arr[i], i, arr)) { result.push(arr[i]); } } return result; }这两道手写题难不在代码本身而在于你是否考虑到回调的第二个参数index和第三个参数arr。很多候选人在白板上写出了callback(arr[i])没有把索引传进去这样到真正使用自定义实现时回调里拿不到index就会出问题。所以哪怕代码看起来“简单”也要表现出对 API 签名完整度的把控。7. 综合场景题把 JS 原理放进真实业务里看面试临近尾声面试官有时会抛出几个“结合业务场景”的题目考察你能否把 JS 的底层能力映射到实际开发中。这类题没有标准答案但答得好的人往往能同时展现出知识宽度和落地能力。7.1 iframe 操作关闭 iframe 并刷新父页面“iframe 关闭并刷新父页面”是一个经典业务场景题。假设你的页面里嵌了一个子应用 iframe用户操作完以后点击一个按钮需要关闭这个 iframe同时刷新父页面。最直接的写法是在父页面里调用// 父页面 const iframe document.getElementById(childFrame); iframe.remove(); location.reload();但实际工作中子页面和父页面常常属于不同域跨域环境下子页面不能直接操作父页面的 DOM。这时候要绕道window.postMessage通信。子页面里这样发消息// 子页面 window.parent.postMessage({ type: close-iframe-and-refresh }, https://parent-site.com);父页面监听消息window.addEventListener(message, (event) { if (event.origin ! https://child-site.com) return; if (event.data event.data.type close-iframe-and-refresh) { document.getElementById(childFrame).remove(); location.reload(); } });这里有两个安全细节一是子页面发送时指定目标 origin不要用*二是父页面接收时一定要校验event.origin防止任意网站向你的父页面伪造消息。面试时能主动讲出这两点说明你做过真实跨域通信不是只会背 API。7.2 大文件上传为什么用 Web Worker 处理分片“前端使用 worker 上传大文件”也是近期热度很高的面试场景。大文件上传的关键是分片把一个 2GB 的文件切成若干片每片单独上传断点续传时只需要重传失败的分片。分片后的一个重要步骤是计算每片的哈希值比如用 SparkMD5用于校验完整性、实现秒传。但计算大文件哈希是 CPU 密集型任务放在主线程里会卡住界面。script const worker new Worker(hash-worker.js); const file document.querySelector(input[typefile]).files[0]; worker.postMessage({ file }); worker.onmessage (event) { console.log(文件的 hash 是, event.data.hash); }; /script在hash-worker.js里self.onmessage async (event) { const { file } event.data; const CHUNK_SIZE 2 * 1024 * 1024; // 2MB const chunks Math.ceil(file.size / CHUNK_SIZE); let hash ; for (let i 0; i chunks; i) { const chunk file.slice(i * CHUNK_SIZE, (i 1) * CHUNK_SIZE); // 这里可以累加计算每一片的 hash 值 } self.postMessage({ hash }); };这道题的考点有多个层次你知道用 Blob.slice 切分文件你知道用 FileReader 或者更现代的方式读取片数据你知道用 Web Worker 把耗时的哈希计算放到子线程避免阻塞 UI你还知道主线程和 Worker 之间的通信依赖postMessage。能把这些串起来讲就在“综合能力”这一项上拿到了高分。7.3 微前端场景里的 JS 沙箱问题“微前端”在热搜里热度很高。面试官如果问“微前端解决了什么问题核心难点是什么”除了 CSS 隔离JS 隔离一定是避不开的。子应用的全局变量不能泄漏到主应用主应用的全局也不能被子应用随意篡改这就要做沙箱。面向面试你至少要能说出三种方案。第一种是window快照沙箱子应用启动时记录 window 上的属性快照卸载时恢复快照。适合不需要长时间共存多个子应用的场景。第二种是代理沙箱通过 Proxy 拦截对 window 属性的读写把子应用的全局变量存储在一个独立对象上页面切换时销毁即可。第三种是 iframe 原生隔离因为 iframe 天然拥有独立的 JS 执行环境所以很多微前端方案会把子应用放进 iframe通信走postMessage。面试官通常会追问“iframe 隔离和 JS 沙箱相比有什么缺点”你可以答iframe 的通信成本高、状态管理复杂、部分弹窗和拖拽交互体验不如真实 DOM 页面所以很多团队选择“单实例 JS 沙箱 共享 DOM 结构”的 hybrid 方案。能把这个权衡过程讲出来就非常加分了。8. 常见面试追问与避坑记录最后这一章我把自己当面试官时常听到的错误答案和经常追问的问题整理成了一份速查表顺便说说哪些坑是候选人最容易踩的。高频追问点常见错误正确思路闭包一定会造成内存泄漏是不一定关键在于是否有外部引用长期持有闭包变量现代引擎有优化Array.prototype.sort默认怎么排序按数字大小默认转字符串后按 Unicode 码点排所以 10 会排在 2 前面typeof null是什么object 或 nullobject这是历史遗留 bug面试时最好主动补一句和区别前者比较值后者比较类型会做类型转换不转换只在类型相同后比较值深拷贝 JSON 方法的缺陷没考虑缺陷会丢失函数、undefined、Symbol循环引用会报错const声明的对象能修改吗不能能const只能保证引用地址不变对象内部属性可以改new一个箭头函数会发生什么可以创建实例不行箭头函数没有 constructor也没有 prototype如果你能在回答这些问题时不仅给出答案还顺带说出背后的原理或使用场景面试官对你的技术深度评估会显著提升。比如被问到typeof null如果你能说“这是 JS 诞生初期的遗留 bug因为在第一版实现里用低位存储类型信息null 的标记是 0而对象也是 0后来 ECMAScript 规范为了兼容就保留了这个行为”那这道题就不只是拿分而是形成记忆点。还有几个我实际面试中常遇到的情况候选人能把Promise.all背得很熟但一问手写就卡住原因是没理解Promise.resolve包裹的意义候选人能说出闭包定义但一到“循环 setTimeout”就懵原因是没把事件循环和执行上下文串起来。这些都是因为“背概念”和“懂原理”之间差了太多实操训练。我建议面试前把本文里所有手写代码都脱离编辑器重新默写一遍写不出来就再看一遍原理直到能顺畅写出为止。我在实际项目开发里还有一个习惯遇到不确定的 JS API 行为不会只靠记忆而是主动打开控制台跑一遍把输出结果记录下来。这个方法听起来笨但长期坚持下来对语言的理解会比只刷面试题扎实得多。希望这篇“js 系列2”能帮你在面试里少踩几个坑就算踩了下次也能自己爬出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价