讲个有意思的事。我这两年帮团队做前端招聘,面了差不多两百个候选人,从初级到高级都有。你猜怎么着?不管简历上写着会 Vue 还是 React,不管项目经验是三年前端还是刚转行,面试题绕来绕去,最后基本都会落到 JavaScript 本身。
很多候选人挂在第一轮,不是因为算法题没刷,也不是因为项目讲不清楚,就是 JS 基础这块被问到底裤都没了。你问他==和===的区别,他能背出来。你问他0.1 + 0.2为什么不等于0.3,他也能说浮点数。但你让他现场说说执行上下文、事件循环、闭包在项目里到底怎么用,他就开始支支吾吾。
所以我想把这 12 个面试必问的 JS 核心点系统地整理一遍。这篇不是八股文背诵手册,而是从面试官视角出发,告诉你每个考点背后到底想考察什么,以及你该怎么回答才能让面试官觉得“这人真的懂”。不管你是准备跳槽,还是刚入行想补基础,这篇文章都值得你花两小时细读一遍,最好边看边敲代码。
1. 为什么是这12个点,面试官到底在考察什么
先说明白一个逻辑:面试官问 JS 基础,不是为了刁难你,而是想通过几个问题快速判断你的代码水平。假如你是个面试官,面前坐着一个号称三年经验的候选人,你总不能让他现场写个业务系统吧。但你可以通过几个基础问题,判断他写代码的时候脑子里有没有完整的模型。
1.1 前端面试的考察逻辑
前端面试考察的东西,其实就三层:语言本身、运行环境、工程实践。
语言本身指的是 ECMAScript 规范,比如类型系统、作用域、闭包、原型链、异步机制。运行环境指的是浏览器或者 Node.js 给 JS 提供的宿主能力,比如事件循环、DOM API、BOM API。工程实践则是指你在真实项目中怎么组织代码,比如模块化、组件通信、状态管理。
这里有个很多候选人容易忽略的点:面试官问同一道题,对不同级别的人期待值是不一样的。同样问闭包,对初级候选人,他背出“函数内部可以访问外部变量”就够了。但对高级候选人,面试官期待你能讲清楚闭包和内存泄漏的关系、闭包在模块化设计中的作用、闭包与柯里化的联动。
那有没有一套问题,能在有限时间内覆盖这三层考察逻辑?有,就是我要讲的这 12 个点。它们正好横跨语言基础、运行机制和工程实践三个维度,每个点都能由浅入深,从概念问到原理,再到手写实现。这也是为什么“90%前端面试必问”这个说法虽然夸张,但背后确实有现实依据。
1.2 12个核心考点全景
整体上,我把这 12 个点分成四组:
第一组:基础功(执行上下文、作用域链、闭包、this 指向)。
这组问题考察的是你对 JS 代码执行过程的理解。很多人写 JS 写了好几年,其实并不知道一段代码在被引擎执行前经历了什么。变量提升为什么存在?var和let的差异根源在哪?闭包是怎么形成的?this到底由什么决定?这些问题全部指向同一个底层模型:执行上下文。
第二组:数据与运算(类型判断、类型转换、深浅拷贝、数组方法)。
这一组考察你对数据操作的基本功。typeof和instanceof的区别是什么?null为什么被typeof判断成object?深浅拷贝怎么实现?数组的map、filter、reduce各自最适合什么场景?字符串的几个高频场景:判断是否包含、忽略大小写比较、验证 URL 有效性,这些在项目里天天用到,但你有没有踩过坑?
第三组:异步与事件循环(Promise、事件循环、setTimeout、async/await)。
这是面试的重灾区,也是区分候选人水平的分水岭。事件循环的宏任务和微任务顺序能写对吗?Promise的链式调用和错误捕获有什么陷阱?async/await和 Promise 的关系是什么?setTimeout(0)真的一定在 DOM 渲染后执行吗?
第四组:工程实践(防抖节流、柯里化、模块化与组件通信)。
这一组偏向应用。防抖节流怎么实现?柯里化到底解决什么问题?组件通信有哪些模式?这些题目的背后,是面试官在考察你是否具备工程化思维,能不能把语言特性转化为实际问题的解决方案。
把上面这些点串起来,就构成了我在后面几个章节里详细展开的内容。注意,这篇文章的编码风格偏“面试实战”,代码片段我都会给出可以直接跑的示例,建议你准备一个在线编辑器,边看遍敲。
2. 基础功:执行上下文、闭包与this指向
基础功这组问题,是每次面试的开场白。我面过的候选人里,十有八九能在“什么是闭包”上聊两句,但能把执行上下文讲明白的,可能不到两成。而实际上,执行上下文才是理解闭包和 this 的钥匙。
2.1 执行上下文与调用栈
JS 代码在执行前,引擎会做很多准备工作,其中最重要的一步就是创建执行上下文。你可以把执行上下文理解成一块“内存区域”,里面记录了这个执行环境中变量、函数声明、作用域链和三者的关系。JS 代码被运行时,会维护一个执行上下文栈,也常叫调用栈,它是一个后进先出的结构。
每次进入一个函数调用,就有一个新的函数执行上下文被压入栈顶。函数执行完,这个上下文就被弹出。全局代码运行在全局执行上下文里,它是栈底的那个。
这里有个面试高频考点:变量提升(hoisting)为什么存在。在创建执行上下文时,引擎会做“声明提升”——var声明的变量被初始化为undefined,function声明被完整赋值,let和const声明被放到暂时性死区。这个过程是编译阶段的产物,规范层面的原因是为了支持函数声明提前使用。
来看一段代码:
console.log(a); // undefined var a = 10; console.log(b); // ReferenceError: Cannot access 'b' before initialization let b = 20; sayHi(); // 可以正常执行 function sayHi() { console.log('hi'); }为什么var a输出undefined而不是报错?因为在全局执行上下文创建时,a已经被登记了,只是还没赋值。而let b虽然也被登记了,但被放入暂时性死区。在引擎看来,代码执行到console.log(b)时,b所在的字节还没有被“初始化”。这个机制是 ES6 为了纠正var的乱象而引入的。
面试中如果被问到这里,你可以主动补充一句:let和const的暂时性死区让 JS 的变量生命周期更可预测,这也是为什么现代项目全面采用let/const替代var的根本原因。这句话能体现出你不只是会背规范,而是思考过设计动机。
2.2 闭包的形成机制与典型应用
我之前总听候选人说:“闭包就是函数套函数,内部函数能用外部函数的变量。”这句话只对了三分之一。闭包的本质,是函数和它被声明时所处词法作用域的引用组合。当你从外部函数的内部返回一个内部函数时,这个内部函数会记住外部函数的活动对象,外部函数执行完被弹出栈后,它的变量并不会被垃圾回收,因为内部函数还握着对它们的引用。
经典代码几乎所有人都会背:
function createCounter() { let count = 0; return function () { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2但面试官很少满足于这个。他会接着问:闭包有什么实际应用场景?这里我建议你答三个角度,每个都附带一个例子。
第一个角度:数据私有化。上面这个计数器就是一个简单例子。闭包让count成为外部不可直接访问的私有状态,只有通过返回的函数才能操作。这种模式在前端组件封装中很常见。
第二个角度:函数式编程的工具。比如柯里化、部分应用、防抖节流,底层都依赖闭包来保存状态。以防抖为例,为什么setTimeout的返回值能被记住?就是因为闭包机制。
第三个角度:模块化封装。IIFE(立即执行函数)配合闭包,可以在 ES6 模块出现前模拟模块化开发,实现私有变量和公开方法的分离。
面试官接着很可能追问:闭包会造成内存泄漏吗?回答这个问题要有分寸。在 ES6 之前,闭包确实容易导致内存泄漏,因为 IE 的垃圾回收机制有 bug,循环引用无法被回收。在现代引擎里,只要你的闭包引用了一个 DOM 元素,而这个闭包本身还被变量持有着,那这个 DOM 元素确实不会被回收。举个例子:
function setup() { const bigData = new Array(1000000).fill('x'); document.getElementById('btn').onclick = function () { console.log('clicked'); }; }这段代码里,事件回调函数通过闭包捕获了bigData,只要元素不被移除、回调不被释放,这 100 万个元素的数组就一直滞留在内存里。正确的做法是业务侧在不再需要时把onclick置为null。面试时把这个例子讲出来,能立刻让面试官对你另眼相看。
2.3 this绑定的四种规则
this是前端面试的绝对高频点,有段时间几乎每个面我的候选人都能把规则背得滚瓜烂熟,但实际让其看一段代码写出输出结果时,经常全军覆没。
this的指向是一套运行时的绑定规则,它不关心函数在哪定义,只关心函数怎么被调用。这里我总结为四条规则,按优先级从低到高排列。
默认绑定:独立函数调用,this指向全局对象(浏览器里是window,Node.js 里是global)。如果开了严格模式,指向undefined。
隐式绑定:函数作为对象的方法被调用,this指向那个对象。这里有个最经典的坑——引用赋值丢失:
const obj = { name: '前端面试', getName: function () { return this.name; }, }; const fn = obj.getName; console.log(fn()); // undefined,this 指向 window/全局,而不是 obj为什么?因为fn是一个独立的变量引用,调用时它不再是obj.getName()这种形式,引擎只能走默认绑定。类似的坑还有把方法作为回调传给setTimeout:
setTimeout(obj.getName, 100); // this 丢失显式绑定:call、apply、bind。三个函数的区别要非常清楚——call和apply都会立即执行函数,只是参数形式不同;bind返回一个新函数,不立即执行,而且新函数的this被永久绑定。
new 绑定:用new调用构造函数时,this指向新建的实例对象。new干的事情包括:创建新对象、把新对象的原型指向构造函数的prototype、以新对象为this执行构造函数、如果构造函数没返回对象则返回这个新对象。
优先级从高到低是:new绑定大于显式绑定大于隐式绑定大于默认绑定。注意,bind绑定的this无法被call/apply改写,这也是判断你是否真懂this的试金石。
很多候选人还会混淆箭头函数。箭头函数没有自己的this,它继承的是定义时所处外层作用域的this。所以你不能给箭头函数用call/bind去改变this,它只会忽略这些方法。
2.4 原型与原型链
原型链在 JS 中的位置,就像基因图谱在生物学中的位置。它决定了对象之间如何继承属性,也是很多手写题的底层依赖。
每个函数都有一个prototype属性,每个对象都有一个__proto__属性,指向创建它的构造函数的prototype。访问一个对象的属性时,如果对象本身没有,就沿着__proto__一路向上找,直到Object.prototype的__proto__是null,链条终止。
面试经常这样考:
function Person(name) { this.name = name; } Person.prototype.sayHi = function () { console.log('你好,我是' + this.name); }; const p = new Person('张三'); console.log(p.__proto__ === Person.prototype); // true console.log(Person.prototype.constructor === Person); // true console.log(Object.getPrototypeOf(p) === Person.prototype); // true这里需要注意constructor这个属性。默认情况下,Person.prototype的constructor指向Person自身。如果你重写整个prototype对象,比如Person.prototype = { sayHi() {} },constructor就会丢失指向,需要手动补回来。
原型链的经典应用有两个。一是继承:子类构造函数的prototype指向父类的实例,或者用Object.create建立关联。二是扩展内建对象方法:比如给数组原型加自定义方法,但现代工程不太建议这样做,容易污染全局。
我在面试中有一个百试百灵的问题:p.hasOwnProperty('name')输出什么?很多候选人答true。接着问p.hasOwnProperty('sayHi'),很多人犹豫了。答案是不管存不存在都要沿原型链找hasOwnProperty这个方法本身,这才是原型链真正有意思的地方。
关于这组基础功,我的实际体会是:面试时不要只背结论,最好能把执行上下文、作用域链、闭包和 this 串成一条线讲出来。比如你可以这样组织回答:“当函数被调用时,引擎创建执行上下文,执行上下文里包含变量对象、作用域链和 this 值。由于 JS 是词法作用域,函数在定义时就确定了作用域链,闭包就是基于这条链形成的。而 this 是运行时绑定的,由调用方式决定,跟定义位置无关。”这段话一出来,面试官基本不会再追问初中阶问题。
3. 异步与事件循环:面试的重灾区
面试时我经常问一个问题:“你写setTimeout(0)想干什么?”十个候选人里有七个会说“延迟执行”,四个会说“让代码不阻塞”。真正答到点子上的,是把它放在事件循环的机制里去理解。
3.1 宏任务与微任务的执行顺序
前端面试的异步题,核心就一张图:执行栈 → 微任务队列 → 宏任务队列 → 渲染 → 循环。我把这套机制和浏览器环境绑定在一起,因为在 Node.js 里的轮询规则还有细节差别,但面试时把浏览器端的模型答明白已经足够了。
JS 引擎一次只执行一个任务,事件循环的本质是:执行栈清空后,先清空全部微任务队列,再从宏任务队列里取一个执行,执行完再回来清空微任务。为什么要插在中间清微任务?因为微任务通常代表“当前同步代码的延续”,比如 Promise.then 的回调,它应该尽可能快地执行,不应该被下一个宏任务延迟。
来看高频面试题:
console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 0); Promise.resolve().then(() => { console.log('promise1'); }).then(() => { console.log('promise2'); }); console.log('script end');输出顺序是:script start→script end→promise1→promise2→setTimeout。很多候选人能答对,但我要追问一句:为什么 Promise 反而比 setTimeout 先执行?能答出“微任务优先于宏任务”还不够,最好能说清楚“promise1和promise2同微任务队列,setTimeout是宏任务”这个细节。
还有一道更烧脑的升级题:
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); async1(); console.log('script end');输出顺序:script start→async1 start→async2→script end→async1 end。这里核心点是await关键字后面跟的是 Promise 时,await会把当前函数挂起,直到右侧 Promise 解决后,把剩下的代码作为微任务加入队列。很多人以为async2()执行时async1 end马上会跟着执行,其实这里有个隐式的微任务调度。
3.2 setTimeout的0毫秒陷阱
回到文章开头提到的热搜词:js settimeout 返回值的范围 有0存在么。这个问题可以引申出一个知识点:setTimeout的延时参数不是精确的,而且存在最小值限制。
规范上,setTimeout的最小延时是 4ms,这是 HTML5 规范里规定的。嵌套超过五级的setTimeout,浏览器还会自动增加至少 4ms 的延时。所以理论上setTimeout(0)实际生效的延时可能是 0 到 4ms 甚至更多。另外,浏览器标签页处于后台时,定时器会被节流,最小间隔甚至可能被提升到 1000ms 以上,用来节省 CPU。
还有一点很多人不知道:setTimeout的返回值是一个正整数,代表这个定时器的 ID。不同浏览器的实现不同,有些从 1 开始递增,有些从 0 开始。在 Chrome 里,第一个setTimeout返回的 ID 通常是 1,但在一些旧的页面里也可能是 0。这就是为什么判断定时器是否有效,要用timerId !== undefined而不是if (timerId)。
经常有候选人把setTimeout(fn, 0)当作“渲染完成后执行”的手段,这其实是个误区。setTimeout是宏任务,宏任务执行之后会不会进入渲染流程,取决于浏览器是否在恰当的时候合并渲染。想要在 DOM 变更后执行下一步,正确的思路是先用微任务(Promise),因为微任务清空之后才轮到浏览器渲染,而不是反过来。很多 UI 更新后的测量需求,应该用requestAnimationFrame而不是setTimeout。
3.3 Promise必须掌握的四个细节
Promise 相关的面试题,从简单的then/catch/finally到复杂的链式调用、并发控制都有。我认为最容易被忽略又最常被问的,是以下四个细节。
第一个细节:Promise 的状态只能改变一次。resolve之后再调用reject是无效的。这个特性决定了Promise很适合做异步任务的幂等控制。
第二个细节:.then()的回调里返回一个 Promise,会被展开。这个展开机制是实现链式串行的关键。比如:
Promise.resolve('a') .then((v) => { return new Promise((resolve) => { setTimeout(() => resolve(v + 'b'), 1000); }); }) .then((v) => console.log(v)); // 1秒后输出 ab第三个细节:.catch()不一定要写在最后一个.then()后面。中间任何一个.then()出错,都会被后面的同名.catch()捕获,但前提是你用了链式写法。如果中间某一步没有链上,错误就成了未处理拒绝。很多新人写axios.get().then(resp => ...).catch(err => ...)时,如果中间又包裹了其他 Promise,很容易漏掉链式关系,导致错误无法被捕获。
第四个细节:Promise.all和Promise.allSettled的区别。Promise.all只要有一个失败就整体失败,它适合“多个请求必须全部成功才行”的场景。Promise.allSettled等全部结束,返回每个任务的结果,适合“多个任务独立执行,即使某些失败也不影响整体”的场景。还有Promise.race,适合做超时控制。
3.4 async/await与错误处理
面试到高级阶段,async/await通常会被关联到错误处理上。很多人写完async函数后根本不处理错误,这在实际项目里是事故隐患。
用try/catch包裹await是基础,但要注意:try/catch只能捕获await这个表达式直接抛出的错误。如果await后面跟的是不会 reject 的 Promise,或者你把异步调用拆开了,错误就会逃逸。
另一个高频考点是:async函数返回的是一个 Promise,所以可以在async函数外面继续用.catch()捕获错误。这一点也可以用来优化错误处理:与其在async函数内部层层try/catch,不如让它的 Promise 抛出来,由调用方统一处理。
再补充一个实战经验:并发请求的等待,不应该写成.map(async () => await fetch)这种顺序等待的写法。应该先创建全部 Promise,再统一await Promise.all。第一版写法会让请求变成串行,第二版才是并发。面试官常把这道题伪装成“优化接口请求性能”的项目细节来问,如果你能给出Promise.all并用Promise.allSettled处理部分失败的方案,就已经展现出了不错的异步编程能力。
这组异步考点的备考建议:从事件循环的宏观模型出发,把所有异步 API 归入宏任务或微任务,然后反复手写几个混合执行顺序的代码段。不要只记输出结果,要思维上模拟整个执行流程。
4. 数据与运算:从类型判断到数组和字符串的实战
这一组考题的特点是:看起来简单,但往往失分最严重。因为简单,所以很多人不复习;因为不复习,细节全忘光。
4.1 类型判断的四种姿势
判断 JS 数据类型,方法很多,但要搞清楚每种方法的边界。typeof只能判断基本类型,它返回七个字符串:'undefined'、'boolean'、'string'、'number'、'bigint'、'symbol'、'object'。坑在于null、数组、正则、日期,它都返回'object'。
instanceof判断的是“实例的原型链上是否出现构造函数的prototype”,它适合判断引用类型,但不能跨 iframe 或跨 realm 使用,因为不同窗口的构造函数不是同一个。
综合判断引用类型最常用的方案是Object.prototype.toString.call():
Object.prototype.toString.call([]) // '[object Array]' Object.prototype.toString.call(null) // '[object Null]' Object.prototype.toString.call(/abc/) // '[object RegExp]' Object.prototype.toString.call(new Date()) // '[object Date]'这个方法几乎可以判断所有内置类型,也是很多工具库isArray的底层实现原理。
最后一种姿势是Array.isArray(),用来单独判断数组,ES6 之后直接用它,比toString更简洁。
4.2 深浅拷贝的实现与差别
浅拷贝只复制第一层属性,深层引用仍然共享。常见实现有:展开运算符{...obj}、Object.assign、Array.prototype.slice()。深拷贝则是递归复制每一层,彻底切断引用。
面试手写深拷贝,有一个常见的踩坑点:循环引用。如果对象里有自己引用自己的属性,简单的递归实现会无限递归导致栈溢出。一个合格的手写深拷贝要具备几个特征:能遍历对象和数组、能处理正则和日期、能处理循环引用、能判断 Symbol 属性。
项目里能直接用structuredClone或者JSON.parse(JSON.stringify())。JSON.parse(JSON.stringify())对新手来说是万能药,但它有明确缺陷:不处理undefined、函数、Symbol、循环引用,而且把Date变成字符串、把正则变成空对象。structuredClone是浏览器和 Node.js 17 之后原生支持的深拷贝方案,能处理多数情况,但对于函数的拷贝依然不适用。这块如果面试中被问,建议顺着这些坑往下讲,会让面试官觉得你真的用过而不是背过。
4.3 数组方法:不只是map和filter
数组方法太多了,但面试往往只追问少数几个核心的。我建议你把map、filter、reduce、forEach作为一个整体理解,再额外掌握flat、flatMap、some、every、find、findIndex。
这里有一个很多人都忽略的细节:map和forEach的区别。forEach永远返回undefined,不能用于链式操作;map返回一个等长的新数组。所以“把数组遍历一遍并返回新数组”应该用map,而不是先forEach再手动push。
还有一个高频考点是reduce的使用。它不仅能做累加,还能实现数组去重、数组转对象、按条件分组:
const arr = [ { type: 'js', name: '闭包' }, { type: 'css', name: 'flex' }, { type: 'js', name: '原型链' }, ]; const grouped = arr.reduce((acc, item) => { (acc[item.type] = acc[item.type] || []).push(item); return acc; }, {}); // { js: [{...}, {...}], css: [{...}] }这种写法在真实项目里非常常用,面试时能现场写出来,是能力展示,不是背题。
4.4 字符串高频场景:包含、忽略大小写与URL验证
热搜词里出现了一串字符串操作相关的内容:js判断字符串是否包含、js 忽略大小写、js验证url有效性。这三个场景确实太常见了,我单独放到一起讲。
判断字符串是否包含子串,ES6 之后用String.prototype.includes,它返回布尔值,比旧式的indexOf() !== -1更直观。但要注意,includes是区分大小写的。面试里考得比较细的点是:为什么includes不支持传正则,而indexOf也不支持正则。这是因为 JS 字符串方法分两类:接受正则的和不接受正则的。match、replace、search接受正则,includes、indexOf、startsWith不接受。
忽略大小写的场景,通常用toLowerCase()或toUpperCase()把两边归一化再比较:
const a = 'HelloWorld'; const b = 'helloworld'; console.log(a.toLowerCase() === b.toLowerCase()); // true如果是对单个字符比较,也可以考虑localeCompare加sensitivity: 'accent',不过这个 API 的兼容性和使用场景都比较特定,写项目时绝大多数用toLowerCase就够。
URL 有效性验证是另一个常见的面试手写题。完整的 URL 结构包括协议、域名、端口、路径、查询参数、哈希。一个“验证 URL 是否有效”的需求,要拆分来看:是验证格式还是验证可访问性?浏览器里可以用fetch(url)去探测,但它涉及跨域和网络请求,不适合在面试题里直接当答案。
格式验证可以用URL构造函数:
function isValidURL(url) { try { new URL(url); return true; } catch { return false; } } console.log(isValidURL('https://example.com')); // true console.log(isValidURL('not-a-url')); // false这里有个容易误判的地方:new URL('www.example.com')不会报错,因为它会把www.example.com当路径处理,生成一个没有协议基础的 URL。所以如果要求必须带协议,你还需要手动判断url.startsWith('http://') || url.startsWith('https://')。
关于类型和数据操作这一章,我强调一遍:代码题不仅要会写实现,也要能说明白每个方法的适用边界。面试官喜欢追问“还有没有更好的方案”,这时你需要能给出不同场景的取舍建议。
5. 进阶与实践:防抖节流、柯里化与组件通信
到了这一组,就不再是单纯的语言知识点,而是把语言能力转化为工程能力的体现。公司要的不仅是“会写 JS 的人”,更是“能处理真实业务场景的人”。
5.1 防抖与节流:性能优化必考题
防抖和节流几乎是前端面试“手写题”中出镜率最高的两道题。它们的应用场景有:输入框搜索联想(防抖)、滚动事件的懒加载计算(节流)、按钮点击防止重复提交(防抖)、图表 resize 的渲染控制(节流)。
防抖的核心思路:事件触发后延迟执行,如果在延迟时间内再次触发,就重新计时。要素是setTimeout加clearTimeout:
function debounce(fn, delay) { let timer = null; return function (...args) { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); }; }节流的核心思路:在一个时间段内只执行一次,时间段内重复触发被忽略。两种常见实现:时间戳版本和定时器版本。时间戳版本的优势是首次触发能立即执行,定时器版本的差异在于控制结束后是否补执行最后一次。很多面试官考这个,目的是看你是否理解“函数的执行时机”和“高频触发时的降频策略”。
function throttle(fn, delay) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= delay) { fn.apply(this, args); lastTime = now; } }; }面试时能答出这两种方案的差异,并说明在什么业务场景下选哪一种,就已经是较高分了。
5.2 柯里化:函数式编程的入场券
柯里化是“函数返回函数、逐步收集参数直到收集完才执行”的一种函数式编程技巧。它的应用场景包括:日志输出、表单校验规则的复用、事件处理中的预配置参数等。
手写一个通用curry函数,算是中高阶面试题。核心逻辑:返回一个函数,收集参数,参数攒够后就执行原函数:
function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } return function (...nextArgs) { return curried(...args, ...nextArgs); }; }; }注意fn.length是被柯里化函数的形参个数,这个细节很关键。写完这个函数,我一般会追问:柯里化解决了什么问题?答案不只是“延迟执行”,还有“参数复用”——固定一部分参数,生成一个新函数,减少重复传参。
5.3 模块化与组件通信
热搜词里出现了一些组件库和前端工程相关的词,比如前端组件库、hzero前端开发、前端传参、js三级联动。这些概念的共同底层,是模块化和组件间的数据通信。
ES6 模块的export/import是目前最标准的模块化方案。面试官常问:export default和命名导出的区别、循环依赖问题、动态import()的应用场景。如果你用过 Vite 或 Webpack 的代码分割,最好能结合动态import()来说明按需加载的原理。
组件通信的常见模式:父子组件通过 props/事件通信,兄弟组件通过状态提升或事件总线,复杂项目用状态管理(Vuex/Redux/Pinia)。面试中如果给你一个场景题,比如“两个列表组件如何联动”,核心思路就是状态提升:把共享状态放到二者最近的共同父组件。
“三级联动”就是状态提升的典型例子:省市区三个下拉框,选中省份后城市列表根据省份 update,选中城市后区县列表根据城市 update。这就是前端传参和组件通信的基础应用题。很多热词搜索都在找这个东西,说明它就是一道经典面试题。
关于这组进阶内容,我再多提醒一句:面试官考察防抖节流、柯里化,通常不是要求你像背题一样把代码默写出来,而是给你一个业务场景,让你用这些技巧来解决。所以平时准备的时候,一定要把每个手写函数和真实场景对应起来。
6. 手写实现与面试回答策略
手写题是四组知识点中让候选人最紧张的部分。但我观察下来,手写题的高分选手从来不是靠临场发挥,而是把高频手写实现练成了肌肉记忆,再加上真正理解了原理,可以应对变种题目。
6.1 手写new和bind
手写new的考察点非常经典,因为它同时覆盖了原型、this 和函数调用多个知识点。一个常见实现是:
function myNew(Constructor, ...args) { // 1. 创建新对象,并让它的原型指向构造函数的 prototype const obj = Object.create(Constructor.prototype); // 2. 以 obj 为 this 执行构造函数 const result = Constructor.apply(obj, args); // 3. 如果构造函数显式返回了一个对象,则返回那个对象,否则返回 obj return (typeof result === 'object' && result !== null) || typeof result === 'function' ? result : obj; }这里的边界点:构造函数的显式返回值。如果它返回一个基本类型,会被忽略,new仍然返回创建的新对象;如果返回一个对象,则new返回这个对象。多数人第一步、第二步能写对,但第三步经常漏掉,导致整体得分被扣掉一块。
手写bind的考察点则偏向“函数参数处理”和“new 优先级”。简单版本:
function myBind(fn, thisArg, ...boundArgs) { return function (...callArgs) { return fn.apply(thisArg, [...boundArgs, ...callArgs]); }; }这里有个进阶问题:如果用new调用这个绑定函数,thisArg应该被忽略。原生bind返回的函数是可以作为构造函数使用的。如果面试官追问这一层,能做出来的人非常少,但一旦做了,面试分就是质的提升。
6.2 手写Promise背后的意义
手写一个可用的Promise,是很多大厂面试的高阶题目。它的考点不只是异步,而是类设计、状态管理、回调队列。
最简版的Promise至少要有这几个部分:三个状态(pending/fulfilled/rejected)、值或原因的存储、then方法中两个回调、resolve/reject的状态锁定。一个极简骨架如下,帮大家建立基本认知:
class SimplePromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.reason = undefined; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.state !== 'pending') return; this.state = 'fulfilled'; this.value = value; this.onFulfilledCallbacks.forEach((fn) => fn()); }; const reject = (reason) => { if (this.state !== 'pending') return; this.state = 'rejected'; this.reason = reason; this.onRejectedCallbacks.forEach((fn) => fn()); }; try { executor(resolve, reject); } catch (err) { reject(err); } } 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)); } } }当然这只是演示基础结构,真正的Promise实现还需要处理 then 的链式调用、值穿透、异步执行等细节。面试中不一定要写到标准,但能体现你的状态机思维和异步处理能力。
6.3 面试回答的话术与心态
我最后想专门谈谈回答策略,因为这块很多候选人吃亏。
第一,先定性,再展开。比如被问到闭包,先说“闭包是函数和其词法作用域的组合”,再给例子,再讲应用和风险。先说结论能让面试官知道你脑子里有框架。
第二,主动讲边界条件和坑。比如讲typeof null === 'object'时,主动补一句“这是 JS 的历史 bug,ES6 之后依然保留”。这种细节能体现你对语言历史的了解。
第三,手写题先想思路,再动笔。一个合格的手写题答案,应该包含边界条件的讨论。如果你只说核心逻辑,哪怕代码写对了,面试官依然会认为你的工程意识不足。而主动讨论边界条件,就像在真实代码评审中一样加分。
第四,被问倒的时候别慌。说“这块我没实际用过,但我理解它的思路是这样的……”永远比直接说“不会”好。面试官考察的重点,有时候是看你面对未知问题的应对方法。
7. 常见问题与避坑实战速查
这一节把面试中候选人最容易踩的坑集中整理一下,当作实战速查表使用。每一行都是真实面试中出现过的高频错误。
| 考点 | 常见错误说法 | 正确姿势 |
|---|---|---|
| 变量提升 | “let 声明不会提升” | let 和 const 也会被提升,但会被放入暂时性死区,执行到声明语句前无法访问 |
| this 指向 | “箭头函数的 this 由 apply 决定” | 箭头函数没有 this,call/apply/bind 对它无效,它继承外层词法作用域的 this |
| 闭包内存泄漏 | “闭包一定会泄漏内存” | 关键看闭包持有的引用是否长期不释放,以及是否引用了大对象/ DOM 引用 |
| setTimeout | “setTimeout(fn, 0) 一定立即执行” | 最小延迟约 4ms;后台标签页节流;它属于宏任务,排在微任务之后 |
| Promise 错误 | “Promise 出错一定进 catch” | 只有链式写法中未捕获的拒绝才能被后面的 catch 接住;没有 catch 的错误会成为未处理拒绝 |
| 深拷贝 | “JSON.stringify 能拷贝一切” | 会丢 undefined、函数、Symbol、循环引用、把 Date 和 RegExp 转成非期望结构 |
| instanceof | “instanceof 可以判断所有类型” | 不能跨 iframe / realm;基本类型也不能 |
| URL 验证 | “只要能被 new URL 解析就是合法 URL” | 有的字符串会被当作相对路径解析成功,要结合业务要求判定协议前缀 |
| 数组方法 | “map 和 forEach 都能链式” | forEach 返回 undefined,不能链式 |
| 防抖节流 | “防抖节流能同时解决首尾触发问题” | 两种实现各有权衡,要根据业务场景分别配置立即执行和尾部补充 |
这些坑之所以反复出现,是因为很多候选人只背了“结论”而没理解“机制”。我特别想强调一个点:面试官问每一个问题,都会希望你从“这是什么”讲到“为什么这样设计”,再到“实际项目里怎么用”。这三个层次都答到了,面试就会非常稳。
再分享一个我面试时经常用的追问套路,你也可以用它自测。我问“事件循环是什么”,候选人答“微任务优先于宏任务”。接着我问“那浏览器渲染呢?”如果候选人能补充“渲染会在执行完一轮宏任务和微任务之后进行”,我就知道他真的理解浏览器线程模型。如果只能背出顺序,我会继续追问“看完下面这段代码输出什么”,这里十有八九能分出高下。
准备面试的节奏,我建议分成三步:第一周快速过一遍这篇里所有概念,确保每个点都能用自己的话说出来;第二周围绕每个概念手写实现,闭包、防抖节流、深拷贝、bind 每天各敲一遍;第三周做模拟面试,把每道题当成本人在面试现场,用“定性+展开+举例+边界”的方式口头回答一遍。三周下来,这 12 个核心点基本能形成肌肉记忆。
从面试官视角总结一下我的感受:大多数候选人在基础题上翻车,不是因为智商不够,而是因为太轻视基础,总以为会用框架写页面就等于会 JS。但框架更新换代太快,JS 的底层逻辑十几年没变过。真正理解执行上下文、事件循环、闭包和原型链的前端,面试时几乎不可能被问倒。就算被问到没见过的场景,也能靠基础模型推导出正确答案。这也是我坚持在面试中深挖这些基础点的原因——花二十几分钟问一段基础题,抵得上聊两个小时的业务项目。