:核心知识点与追问逻辑全解析)
没有项目正文和关键词单凭“前端面试必刷20题JS版”这个标题和一堆热搜词反而让我觉得有不少话想说。每年前端面试题都在变但JS核心知识点的考察方式、面试官的追问逻辑其实有很强的延续性。这篇文章我就以20道题的框架把JS面试里那些“必刷”的点拆开揉碎讲清楚每道题背后的原理、常见误区以及一条比较务实的复习路径。1. 从2026年热词看JS面试方向八股文没死但升级了1.1 热搜词透露出的信号我特意扫了一眼近期的热搜词前端面试、前端面试题2026、前端面试八股文、JS函数、JS map、前端传参、JS三级联动、前端组件库……说句实话这些词的分布跟三年前相比大的框架没变但细节变化很明显。变化在于面试官不再满足于“你知道这个API吗”而是会追问“你为什么在这里用它”“它底层是怎么实现的”“换成你会怎么设计”。比如“JS map”这个词搜的人多但真正拉开差距的题目是“手写reduce”或者“map和forEach在什么场景下绝不能互换”。“前端传参”这个词看着基础但面试官可以一路追问到GET和POST的区别、URL编码规则、请求体格式、跨域携带参数的限制甚至让你现场设计一个多端统一的传参规范。另一个信号是“前端面试八股文”这个词长期霸榜。我的态度是八股文本身不是问题问题是只会背八股文。同样一道题有人能引出一个完整的技术决策链路有人只能默写定义——面试结果自然天差地别。这篇文章里的20道题我会把“定义之外的决策场景”同步讲清楚。1.2 20题全景自测清单先把20道题的完整清单和考察维度放出来你可以先自测一遍再对照后文深入看编号题目核心考察维度推荐掌握程度1如何准确判断JS中的数据类型类型系统、边界意识必背能手写2判断字符串是否包含某个子串字符串API、正则、性能必背3闭包是什么业务中哪里用到了闭包作用域、内存、场景必背能讲场景4this指向如何判定绑定规则、箭头函数必背5防抖与节流的区别及手写实现闭包、定时器、性能优化必背能手写6手写深拷贝递归、循环引用、特殊类型必背能手写7宏任务与微任务的执行顺序事件循环、异步模型必背8Promise.all、race、allSettled的区别与实现异步并发、错误处理必背能手写9async/await怎么捕获异常异步语法糖、错误边界必背10原型链是什么如何实现继承原型、原型链、继承方式必背能画图11手写new操作符this、原型、构造调用必背能手写12call、apply、bind的区别与手写bind显式绑定、柯里化必背能手写13数组去重有哪些方式性能如何Set、Map、排序、双循环必背14map、filter、reduce的分工与reduce妙用数组高阶函数、函数式思维必背能用reduce实现map/filter15手写EventEmitter发布订阅设计模式、回调管理推荐能手写16手写Compose/管道函数高阶函数、函数组合进阶选手17前端传参方式有哪些HTTP、路由、状态管理必背18省市区三级联动数据结构怎么设计树形数据、递归、性能必背能手写19如何判断一个对象是不是Promise类型判断、Promise/A规范进阶选手20前端模块化的发展与ESM和CommonJS区别模块化、构建原理必背下文的展开顺序跟表格不完全一致因为有些题目需要串联着讲才会更清楚。整个复习过程中我建议你先做一遍表格里的自测给自己打个分。能立刻答上来且能说出理由的先跳过卡壳的再到对应章节精读。2. 基础题不是送分题类型判断、字符串与数组API的高频拆解2.1 类型判断为什么typeof和instanceof都会翻车第一题“如何准确判断JS中的数据类型”很多人张口就来typeof啊。但面试官追问“那typeof null是什么”时一多半人会卡壳。typeof null object这是JS从诞生起就存在的bug因为早期的实现中类型标签和null的空指针标记冲突了。规范为了兼容一直没改。同理typeof [] objecttypeof new Date() object函数返回function但函数本质上也是对象。instanceof能判断数组、日期这些引用类型但它判断的是“构造函数的prototype是否在对象的原型链上”。原型链是可以被修改的——arr instanceof Array可以被人为改写Array.prototype或者修改arr的__proto__来骗过。跨iframe的场景下一个iframe里的数组到了另一个iframe里instanceof Array会直接返回false因为两个iframe的Array构造函数不是同一个。所以真正稳妥的答案是用Object.prototype.toString.call()function getType(value) { const typeStr Object.prototype.toString.call(value); // 返回类似 [object Array] 的字符串 return typeStr.slice(8, -1); } console.log(getType(null)); // Null console.log(getType([])); // Array console.log(getType(new Date())); // Date console.log(getType(/abc/)); // RegExp console.log(getType(Symbol())); // Symbol为什么Object.prototype.toString能这么可靠因为它内部有一个[[Class]]内部属性规范符号叫[Symbol.toStringTag]对象的内部类型信息会通过这个方式暴露出来而且基本不会被改写除非显式定义Symbol.toStringTag。我面试别人时这道题我最想听的其实是最后一个追问“项目里你会封装一个isArray工具函数吗还是直接用Array.isArray”标准答案是Array.isArray就是为数组判断而生的其他类型再用Object.prototype.toString.call。如果候选人能主动提到这两种方式的差异说明他是真的在日常写代码而不是只看过题解。2.2 “判断字符串是否包含”的三种写法与正则边界这个热搜词单独出现说明它是很多人的知识盲区。题目本身不难hello world.includes(world)返回true。indexOf和includes在判断是否包含上的区别是什么indexOf返回的是索引位置找不到返回-1所以判断时要加! -1includes直接返回布尔值语义更清晰。但面试官真正想听的是你对API边界的理解第一includes区分大小写。要做忽略大小写的包含判断可以用toLowerCase()把两边都转小写或者用正则/world/i.test(str)。第二正则方案有转义问题。如果子串是动态变量直接拼正则是有坑的——比如子串里含.、*、?这些元字符会变成正则的通配或者量词。要安全地动态转正则需要先转义function escapeRegExp(str) { return str.replace(/[.*?^${}()|[\]\\]/g, \\$); } function containsIgnoreCase(source, sub) { return new RegExp(escapeRegExp(sub), i).test(source); }第三性能方面的对比includes和indexOf在V8里底层都是字符串搜索算法普通场景下差距可以忽略。但如果跑在长文本、高频循环里indexOf偶尔会略快因为它不需要创建布尔值包装不过这种微优化在业务中基本不值得做。真要说性能应该关注的是“你在一万次循环里重复构建正则”这种问题而不是纠结includes和indexOf的纳秒级差异。2.3 数组API高频题map、filter、reduce的分工边界map、filter、reduce是“前端面试八股文”里出现频率最高的三个API几乎每个前端开发者都能说出一两句用途。但真正拉开差距的是组合使用和边界认知。map的核心语义是“映射”输入一个数组输出一个长度相等的新数组每个元素经过回调函数转换。所以如果你只是想遍历数组做副作用操作比如修改外部变量、发起请求用map就是错误用法应该用forEach。面试官经常问“map和forEach有什么区别”除了返回值不同还有一个容易忽略的点forEach无法在遍历中提前跳出map同样不行。想提前跳出只能用some、every或者传统的for循环。filter的核心语义是“筛选”回调返回布尔值结果为true的保留下来输出数组长度小于等于原数组。reduce则更底层它把数组“归约”成任意值——数值、对象、数组甚至另一个函数。用reduce可以自己实现map和filter这个手写题我在面试中见过多次Array.prototype.myMap function(callback, thisArg) { return this.reduce((result, item, index, array) { result.push(callback.call(thisArg, item, index, array)); return result; }, []); }; Array.prototype.myFilter function(callback, thisArg) { return this.reduce((result, item, index, array) { if (callback.call(thisArg, item, index, array)) { result.push(item); } return result; }, []); };为什么要会这种写法因为面试官从“你会用API”到“你理解API”的追问最常走的就是这条路径。能写出来说明你理解reduce的累加器机制而不是单纯背过三个API的名字。另一个高频考点是reduce的经典用法数组求和、按属性分组、拍平数组、统计出现次数。我强烈建议你至少能手写一个“按属性分组”的例子因为这是从数组到对象结构转换的代表场景业务里太常见了。3. 闭包与this概念题如何答出“场景感”3.1 闭包不只是“函数套函数”三个业务场景才是得分点“闭包是什么”如果只回答“函数内部访问外部变量”只能拿一半分。面试官真正想听的是你在真实项目里哪里用到了闭包先说原理层面的标准答案闭包是函数与其词法作用域的组合。当内部函数引用了外部函数作用域中的变量时即使外部函数已经执行完毕内部函数依然持有对该变量的引用这个变量不会被垃圾回收。用我的话说闭包就是“函数记住了它出生时的环境”。然后说场景。第一个最典型的场景是防抖和节流。防抖函数内部保存了一个timer变量每次调用时重置定时器这个timer就是闭包变量外部无法直接访问。第二个场景是封装私有变量比如写一个计数器用闭包把计数变量藏起来只暴露increment和getValue方法。第三个场景是柯里化function add(a) { return function(b) { return a b; } }内层函数记住了a。回答闭包时还有一个隐藏加分项内存泄漏问题。如果你能主动提到“闭包会导致变量长期驻留内存所以在EventListener、定时器、全局变量引用链中要小心释放”这道题的分数会明显不一样。因为这说明你有内存管理意识而不只是会背概念。3.2 this指向判定表默认、隐式、显式、newthis的指向判定是JS面试里最容易被搞混的知识点。我总结了一个判定顺序面试时照着这个顺序推理基本不会错函数是否被new调用是 →this指向新创建的对象。函数是否通过call、apply、bind调用是 →this指向显式传入的对象。函数是否作为某个对象的方法被调用比如obj.fn()是 →this指向obj。以上都不是在严格模式下this是undefined非严格模式下指向全局对象浏览器里是windowNode里是global。箭头函数不适用以上任何规则它的this由词法作用域决定——在定义箭头函数时它外层的this是什么箭头函数里的this就是什么。有一个经常考的例子是事件监听器里的this。btn.addEventListener(click, fn)中fn里的this指向btn。但如果把fn改成箭头函数this就变成了外层作用域的this。这个点如果能在回答中自己提出来说明你真的写过事件绑定。还有一个容易踩坑的场景是“取出方法再调用”const obj { name: obj, getName() { return this.name; } }; const fn obj.getName; console.log(fn()); // undefined非严格模式为什么因为fn被赋值后调用时没有任何“隐式绑定”关系它变成了一个普通函数调用this自然就丢了。这个例子是面试官非常喜欢追问的因为它直观地展示了“this由调用方式决定而不是定义位置决定”箭头函数除外。3.3 防抖与节流20行核心实现防抖与节流这道题属于“背了不一定加分不背一定扣分”的题。关键在于手写实现不能有bug而且能说清二者的使用边界。防抖的核心思想在事件被连续触发时只在最后一次触发后的等待时间结束时执行一次。适合用在输入框搜索、窗口resize后停止调整再计算等场景。function debounce(fn, delay 300) { let timer null; return function(...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); }, delay); }; }注意这里用了fn.apply(this, args)这是很多手写答案会忽略的点。如果忘了绑定this在Vue组件里用防抖函数时函数内部的this就会丢失。节流的核心思想在单位时间内无论事件触发多少次只执行一次。适合用在滚动加载、鼠标移动、点击提交等场景。function throttle(fn, interval 300) { let last 0; return function(...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }这个实现是“时间戳版”第一次触发会立即执行最后一次触发如果没到时间间隔会被丢弃。还有一种“定时器版”节流第一次触发会有延迟但能保证最后一次触发被补执行。把两种实现的差异讲出来属于高分回答。4. 事件循环与Promise执行顺序题背后的调度逻辑4.1 宏任务与微任务输出顺序题的完整推理链“事件循环”和“宏任务微任务执行顺序”是JS面试里的重头戏。题目往往是一串console.log和setTimeout、Promise的混合代码让你写出输出顺序。这类题目的完整推理链是这样的第一JS是单线程的同步代码按顺序执行。遇到异步操作按类型分到宏任务队列或微任务队列。第二宏任务包括setTimeout、setInterval、I/O、UI渲染。微任务包括Promise.then、MutationObserver、queueMicrotask。注意Promise构造函数本身是同步执行的只有then和catch里的回调才是微任务。第三每执行完一个宏任务会清空当前微任务队列中的所有任务然后再取下一个宏任务。这就是“宏任务 → 微任务 → 渲染 → 宏任务 → 微任务……”的循环。举个例子console.log(1); // 同步输出1 setTimeout(() { console.log(2); }, 0); // 宏任务 Promise.resolve().then(() { console.log(3); }); // 微任务 console.log(4); // 同步输出4 // 输出顺序1, 4, 3, 2为什么3在2前面因为当前同步代码执行完后事件循环先检查微任务队列Promise.then排在微任务队列里会在这个“宏任务”结束前被执行。setTimeout回调放在宏任务队列只能等到下一个宏任务阶段。进阶版会加入async/await。await做的事情可以理解为“把后面的代码包装成像Promise.then一样的微任务”。所以async function foo() { console.log(A); await bar(); console.log(B); } function bar() { console.log(C); return Promise.resolve(); } console.log(D); foo(); console.log(E); // 输出顺序D, A, C, E, B关键点在于bar()是同步执行的输出C但await后面的console.log(B)被挂到了微任务队列要等同步代码E输出完之后才轮到它。很多人把await理解为“阻塞”这是不对的——它是“挂起”不是阻塞。4.2 Promise.all、race、allSettled从区别到手写Promise.all的特点所有Promise都成功才成功只要有一个失败就整体失败而且失败信息是第一个失败的那个原因。Promise.race的特点谁先出结果就用谁的结果不管成功还是失败。Promise.allSettled的特点等所有Promise都出结果无论成功失败然后返回每个任务的状态和值。很多面试题会追问“Promise.all中如果有一个请求特别慢其他请求都成功了还会等它吗”答案是会等。Promise.all本身不会因为部分任务完成就提前结束这是与Promise.race的根本区别。另一个高频追问是“Promise.all发生了失败其他请求还在继续吗”答案是还在继续。Promise.all只是拒绝了最终结果并不能取消已经发出的请求。如果你能主动提到“想要取消请求需要借助AbortController”那就是满分回答。手写Promise.all是面试常见代码题function promiseAll(promises) { return new Promise((resolve, reject) { const results []; let completed 0; const total promises.length; if (total 0) { resolve([]); return; } promises.forEach((promise, index) { Promise.resolve(promise).then(value { results[index] value; completed; if (completed total) { resolve(results); } }, reject); }); }); }这里有两个细节一是用Promise.resolve(promise)包装这样即使传入的是普通值也能正常工作二是结果按index存放保证返回的数组顺序与传入顺序一致而不是按完成先后排序。4.3 async/await的异常处理与并发控制async/await的异常处理通常用try/catch但有一个“陷阱”容易忽略async function fetchData() { try { const res await fetch(/api/data); const json await res.json(); return json; } catch (error) { console.error(请求失败, error); } }这个写法没问题。但如果你用forEach去遍历执行多个await并发度就是串行的如果你用Promise.all去并行只要一个失败整体就失败。实际项目里“部分失败也要继续渲染成功数据”的需求很常见这时候我一般用allSettled配合map。还有一个考点是“并发限制”比如同时最多只能有3个请求在跑超过的需要排队。简单实现思路是维护一个“执行中数量”计数器和一个待执行任务队列每完成一个就从队列里取下一个。这就是p-limit库的核心思想面试时能手写出基础版很加分。限流的意义在于保护后端和浏览器连接数尤其在做批量导入、批量拉取详情时非常实用。5. 原型链、new与bind造轮子题的给分点在哪里5.1 原型链图谱与instanceof的真实执行逻辑原型链几乎是JS面试必考的重点也是区分“背概念”和“真理解”的分水岭。核心概念要理清每个对象都有一个隐藏属性[[Prototype]]浏览器里可以通过__proto__访问它指向该对象的原型。每个构造函数都有一个prototype属性指向一个对象。当用new调用构造函数时新对象的[[Prototype]]会指向构造函数的prototype属性。instanceof的执行逻辑是沿着左操作数的原型链__proto__向上找看能否找到右操作数的prototype。如果obj instanceof Fn true说明Fn.prototype在obj的原型链上。看一个经典例子function Animal() {} const dog new Animal(); console.log(dog instanceof Animal); // true console.log(dog instanceof Object); // true console.log(dog instanceof Array); // false因为dog的原型链是dog → Animal.prototype → Object.prototype → null。Object.prototype在链上所以instanceof Object为true数组的Array.prototype不在链上所以为false。继承的关键词是“原型链的指向”。ES6的class本质是function的语法糖extends关键字背后做的就是把子类的prototype的原型指向父类的prototype同时把子类的__proto__指向父类。能把这个“双链”关系说清楚的人比只背class A extends B的人强很多。5.2 手写new四步走的关键手写new是出现频率最高的手写题之一因为它综合考了this、原型、构造调用三个知识点。new做的事情是创建一个全新的空对象。把这个空对象的__proto__指向构造函数的prototype。以这个新对象作为this执行构造函数。如果构造函数返回了一个对象返回该对象否则返回新创建的这个对象。function myNew(Constructor, ...args) { // 第一步和第二步创建新对象并设置原型 const obj Object.create(Constructor.prototype); // 第三步以 obj 为 this 执行构造函数 const result Constructor.apply(obj, args); // 第四步根据返回值类型做判断 return (typeof result object result ! null) || typeof result function ? result : obj; }第四步是很多人容易漏掉的。如果构造函数里显式返回了一个对象new出来的结果应该是那个显式返回的对象而不是this。JS的规范就是这么定义的。如果构造函数返回的是基本类型比如return 42则忽略返回this。这个边界条件考的就是你对new机制的完整理解。5.3 手写bind为什么需要“中转函数”和instanceof判断bind和call、apply的核心区别在于bind不立即执行函数而是返回一个新的、this已被绑定的函数。手写bind时有两个关键点第一返回的新函数要能处理new调用的情况。如果new fn()根据new的规则this应该指向新创建的对象而不是之前bind传入的目标。所以需要一个“中转函数”来衔接Function.prototype.myBind function(context, ...args) { const fn this; function BoundFunction(...newArgs) { if (new.target) { // 被 new 调用时this 指向新对象且要继承原函数的原型 return new fn(...args, ...newArgs); } return fn.apply(context, [...args, ...newArgs]); } // 让返回函数的原型指向原函数的原型保证 instanceof 关系 BoundFunction.prototype Object.create(fn.prototype); return BoundFunction; };第二为什么要把合并的参数拆成“bind时传入的 调用时传入的”因为bind支持“参数预置”也就是柯里化。fn.bind(ctx, 1, 2)返回的函数再调用时传(3, 4)最终执行fn(1, 2, 3, 4)。能把这个点答出来说明你已经从“会调API”进阶到了“懂机制”。6. 深拷贝、去重与手写题边界意识才是分水岭6.1 深拷贝的完整判型体系手写深拷贝几乎是中高级前端面试的必考题。核心考点不在于“递归”本身而在于边界情况的处理。初级版本function deepClone(target) { if (typeof target ! object || target null) { return target; } const clone Array.isArray(target) ? [] : {}; for (const key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { clone[key] deepClone(target[key]); } } return clone; }这个版本能过基础关但面试官会立刻追问“如果对象里有Date怎么办”“对象里有循环引用会不会死循环”“Symbol类型的key呢”完整版需要加这些处理function deepClone(target, map new Map()) { // 基本类型直接返回 if (target null || typeof target ! object) { return target; } // 处理循环引用如果已经拷贝过这个对象直接返回之前拷贝的结果 if (map.has(target)) { return map.get(target); } // 处理特殊类型 if (target instanceof Date) return new Date(target); if (target instanceof RegExp) return new RegExp(target.source, target.flags); if (target instanceof Map) { const cloneMap new Map(); map.set(target, cloneMap); target.forEach((value, key) { cloneMap.set(deepClone(key, map), deepClone(value, map)); }); return cloneMap; } if (target instanceof Set) { const cloneSet new Set(); map.set(target, cloneSet); target.forEach(value { cloneSet.add(deepClone(value, map)); }); return cloneSet; } // 处理数组和普通对象 const clone Array.isArray(target) ? [] : {}; map.set(target, clone); // 处理 symbol 类型的key const symbolKeys Object.getOwnPropertySymbols(target); symbolKeys.forEach(sym { clone[sym] deepClone(target[sym], map); }); // 处理普通key for (const key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { clone[key] deepClone(target[key], map); } } return clone; }这里最核心技术点是用Map做“已拷贝对象记录”。每次拷贝一个对象之前先查Map如果已有记录就直接返回这样循环引用就能被安全切断。能写出循环引用处理的候选人在我这里的评价会直接上一个档次。顺便说一句很多公司其实允许在注明的场景下用structuredClone或JSON.parse(JSON.stringify(obj))。但你仍然要能解释清楚JSON方案的缺陷不能处理undefined、function、Symbol、循环引用且Date会被转成字符串。知道什么时候可以用工具、什么时候必须手写这也是面试考察的一部分。6.2 数组去重的六种思路数组去重属于“题目简单但解法众多”的题面试官想通过你的解法选择观察你对API和性能的熟练度。从最简单的说起// 方法1Set最推荐O(n) const unique [...new Set(arr)]; // 方法2filter indexOfO(n²)但能说明思路 const unique2 arr.filter((item, index) arr.indexOf(item) index); // 方法3reduce includesO(n²)函数式写法 const unique3 arr.reduce((result, item) { if (!result.includes(item)) result.push(item); return result; }, []); // 方法4Map记录O(n)适合需要稳定顺序且量大的场景 const map new Map(); arr.forEach(item { if (!map.has(item)) map.set(item, true); }); const unique4 [...map.keys()];面试时我一般会追加一个追问“如果数组里既有数字1又有字符串1去重结果是什么”答案是Set和Map使用SameValueZero进行比较1和1是不同的indexOf使用严格相等也是不同的。所以去重结果会保留两者。但如果面试官说“我不想区分数字和字符串”那就需要自己先做一次统一类型转换。如果需要去重的是对象数组且想按某个字段比如id去重直接用Set就不行了因为每个对象的引用都不同。正确的做法是配合Map按id记录function uniqueByField(arr, field) { const map new Map(); return arr.filter(item { if (map.has(item[field])) return false; map.set(item[field], true); return true; }); }6.3 手写EventEmitter5行核心逻辑手写EventEmitter属于设计模式类题目考察的是回调管理和数组操作功底。核心数据模型是“事件名到回调数组的映射”。class EventEmitter { constructor() { this.events new Map(); } on(eventName, callback) { if (!this.events.has(eventName)) { this.events.set(eventName, []); } this.events.get(eventName).push(callback); } emit(eventName, ...args) { const callbacks this.events.get(eventName); if (callbacks) { callbacks.forEach(callback callback(...args)); } } off(eventName, callback) { const callbacks this.events.get(eventName); if (callbacks) { const index callbacks.indexOf(callback); if (index ! -1) callbacks.splice(index, 1); } } once(eventName, callback) { const wrapper (...args) { callback(...args); this.off(eventName, wrapper); }; this.on(eventName, wrapper); } }面试时如果能再提一些优化方向会很加分比如在emit时要不要拷贝一份回调数组来避免回调里调用off导致遍历错乱once包装器的引用如何被错误地off。这体现的是工程思维不只是背代码。7. 业务化考点前端传参、三级联动与组件通信7.1 前端传参的完整图景“前端传参”这个概念看着基础覆盖的范围其实很广。面试官大概率会让候选人系统地列举前端传参的几种方式然后挑一种深挖。我的系统性分类是这样的第一种HTTP请求传参。GET请求一般通过URL query/api/user?id1或路径参数/api/user/1POST请求一般把参数放在请求体里常见的有application/json、application/x-www-form-urlencoded、multipart/form-data。这里可以追问的点很多GET和POST的区别不只是“参数放在哪”还有涉及语义、缓存、幂等性form-urlencoded和json的区别在于后端的解析方式不同以及form-urlencoded需要做URL编码。第二种路由传参。在SPA中有params路径参数和queryURL参数之分。params更语义化、SEO友好但如果参数是可选的或对URL美观要求不高用query更灵活。第三种全局状态传参。比如Vuex、Pinia、Redux它们解决的是跨组件共享状态的问题。这里有一个容易回答不好的追问“什么场景该用全局状态什么场景不该用”我的经验是只在“多个不相关的组件需要同步同一份数据”时才考虑比如用户登录态、全局配置、购物车组件之间的局部通信优先用props 事件。第四种作用域传参。闭包里通过参数把变量传递到内部函数这是JS函数层面的传参。第五种跨页面传参。比如localStorage、sessionStorage、postMessage、URL参数。跨域页面之间的传参最常用的是postMessage但它的安全性问题不能确认消息来源也需要能说清楚。这个问题其实没有标准答案面试官考察的是你脑海中的“前端通信全景图”。能按“页面级、组件级、请求级、存储级”系统性梳理会比零散地背几个API有用得多。7.2 省市区三级联动数据结构才是核心这个热搜词几乎可以肯定来源于一个经典前端业务题省市区三级联动。它之所以高频出现在面试里是因为一道题能同时考察“数据结构设计”“递归解法”“异步加载”三个能力。核心问题是后端给你的数据是什么结构最常见的有两种。第一种扁平数组 父子关系字段const regions [ { code: 110000, name: 北京市, parentCode: null }, { code: 110100, name: 市辖区, parentCode: 110000 }, { code: 110101, name: 东城区, parentCode: 110100 }, ];这种结构的好处是存储清晰新增节点方便坏处是前端渲染联动需要反复遍历。通常做法是先一次遍历转成Map再通过parentCode查找子级。第二种嵌套树形结构const regionTree [ { code: 110000, name: 北京市, children: [ { code: 110100, name: 市辖区, children: [ { code: 110101, name: 东城区, children: [] } ] } ] } ];这种结构直接服务于渲染把数据传给递归组件就能出联动。它的问题在于如果有一级节点的children特别大一次性加载全部数据会造成首屏开销。所以很多系统的做法是“按需加载”——选择省时才请求该省下的市选择市时才请求区。面试时如果能主动提到“按需加载”配合“递归渲染”这道题基本就稳了。再深一层可以提到“扁平数组转树”的经典算法function buildTree(list, parentCode null) { return list .filter(item item.parentCode parentCode) .map(item ({ ...item, children: buildTree(list, item.code) })); }这个递归写法的关键是“先过滤出当前层级的节点再对每个节点递归构造子级”。如果数组有上万条这个每次filter都是全量遍历的写法会有性能问题优化方向是先用Map按parentCode分组再递归。7.3 组件通信与局部的数据流设计组件通信这个问题简单版我会直接说“props 和 emit”但面试官想听的往往是“消息总线”和“状态管理”的取舍。我用一个实际例子来展开一个订单列表页用户点击“查看详情”后要弹抽屉详情组件里修改订单状态后列表数据需要刷新。这种“相隔很远的组件之间通信”靠props逐层传会有严重的“属性钻透”问题。可选方案有三个方法一把“刷新列表”的方法通过provide/inject或者context传过去。适合跨层级但关系明确的组件。方法二用事件总线EventEmitter在列表组件和详情组件之前传递“刷新”事件。适合解耦但事件多了之后维护成本高不容易追踪。方法三直接把“列表状态”放到全局状态管理Pinia/Redux。适合多个页面共享同一份列表状态的场景。我实际项目中的经验是优先让数据流向清晰可见能体现在组件树上的就不走全局。全局状态管理每一次使用都在增加调试成本因为“不知道谁改了它”。能把这个体会说出来面试官会觉得你真写过复杂组件。8. 面试实战的节奏感与追问应对前面把20题里的重点拆得差不多了。最后分享一点面试实战层面的体会。第一先给结论再展开原理。面试官问“什么是闭包”第一句话直接说“一个函数记住了它词法作用域的变量引用即使外部函数执行完毕这个引用仍然存在”然后从现象讲到使用场景。很多候选人先铺垫半天“JS中有一种作用域机制……”说了一分钟还没到重点面试官其实已经有点走神了。第二主动暴露边界。面试官问map你说完定义后主动补一句“如果只需要遍历不需要新数组我会用forEach如果需要在遍历中提前跳出我会改用some或者普通for循环”这比等着被追问“那你觉得什么时候不用map”要强很多。主动暴露边界说明你对知识有过整理不是临时背的。第三代码题先说思路再写。手写题是面试中翻车率最高的环节。我的建议是拿到题目先花30秒说清楚“我打算分几步、每一步在做什么”再动手写。比如手写new先说“我先创建对象然后链接原型再用这个对象作为this执行构造函数最后按返回类型判断”即使中间写错了面试官也知道你思路是对的会提示你而不是直接否定。第四遇到不会的题把“不会”转换成“我熟悉相关领域的…”。这不是教你说谎而是展示学习能力。比如面试官问 “lxmusic的音源JS怎么实现的”这类你没接触过的偏门问题可以说“这个具体库我不熟悉但我了解一般音源解析的路径是发送请求、解析响应、转码流我可以基于我对请求封装和音频流处理的经验现场推演一下。”整个回答把面试引到你熟悉的领域比尬住强得多。第五准备一两个自己真正做过的小项目。所有的八股文题目最后都指向一个目标面试官想确认你能在真实场景中活学活用。哪怕是一个小的省市区联动组件、一个基于EventEmitter的请求队列工具只要你把实现过程讲得有细节、有取舍比背一百道题都管用。优秀的前端不会只背考点而是把考点放到真实的项目里去验证。