不用怀疑,标题里那个“铜九铁十”不是错别字。这两年行情早就不是“金九银十”那套了,岗位少、候选人多、要求还卷到飞起,说是“铜九”都算客气。但这个阶段反而是查漏补缺、练内功的好窗口,尤其是JavaScript,作为前端的立命之本,面试里躲不开,也最容易一眼看穿水平高低。
这份内容我按自己这些年面人、被面、帮人改简历的经验,把JavaScript的面试核心整理了一份偏“八股”但又不纯八股的知识图谱。它不只适合正在准备2026届秋招和社招跳槽的朋友,也适合那些写了两年业务代码、突然发现自己说不清闭包到底怎么回事的兄弟。里面没有废话,全是高频考点、必背结论、还有我自己踩过的坑。
1. 作用域与闭包:JS面试的第一个分水岭
作用域和闭包基本是JavaScript面试的开场点心。你自我介绍完,对面十有八九会从这里切进去。原因很简单,这两个概念能同时考察你对语言机制的理解深度,以及你有没有真的写过复杂代码。
1.1 词法作用域:代码写在哪,决定了你能看见谁
首先要搞明白,JavaScript是词法作用域(静态作用域),不是动态作用域。这句话是闭包的基础,也是很多人在面试里翻车的起点。
所谓词法作用域,就是你代码在写完的那一刻,变量的可见范围就已经固定了——取决于函数声明的位置,不取决于调用位置。举个例子:
const name = '张三'; function sayName() { console.log(name); } function main() { const name = '李四'; sayName(); // 输出什么? } main();答案是“张三”。因为sayName函数定义在全局,它的作用域链里只有全局的name。main函数内的name不会因为它被调用了就混进来。很多人一看sayName()是在main里面调用的,下意识觉得应该输出“李四”,这就掉坑里了。
面试官问这个,考察的本质是你有没有理解作用域链的建立时机。那怎么展开回答呢?我的建议是提作用域链的查找过程:函数创建时会把当前环境的作用域链保存下来,调用时再结合活动对象形成完整的作用域链。顺着这个思路说下去,闭包、垃圾回收、内存泄漏就全都串起来了。
1.2 闭包的本质:函数带上背包去流浪
闭包的定义其实很朴素:一个函数内部定义的函数,由于词法作用域的规则,它可以访问外部函数的变量,即使外部函数已经执行完毕。从实现上看,闭包让函数“记住”了它的出生环境。理解这一步,你就理解了什么叫做“闭包”。
实话说,闭包的定义背出来很容易,但真正理解它,要看懂下面这个烂大街的场景:
function createCounter() { let count = 0; return function () { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3createCounter执行完之后,按理说局部变量count已经被销毁了。但因为内部匿名函数的引用被counter持有,V8引擎在垃圾回收时会发现这个变量仍然被引用,于是把它保留在堆内存里。这是闭包能够工作的核心原理:普通局部变量在栈上,闭包捕获的变量在堆上,跟着函数引用一起存活。
面试里的进阶问法是:如何用闭包实现私有变量。比如你想封装一个只有特定方法能改的数据:
function createPerson(initialName) { let name = initialName; return { getName() { return name; }, setName(newName) { name = newName; } }; }这背后体现的正是模块化思想的雏形,同时也是防篡改设计。顺着这个延伸,你可以主动提一句“模块模式”,再说说闭包可能导致的内存泄漏,面试官对你的好感度会立马上来。
1.3 闭包与内存泄漏:别让引用“忘了下车”
这里有个值得展开的经典问题:闭包是不是一定造成内存泄漏?答案是否定的。闭包本身是语言的正常机制,只有在某些使用不当的场景下才会导致意料之外的内存占用。
最常见的例子是事件监听里的闭包:
function setup() { const largeData = new Array(1000000).fill('data'); document.getElementById('btn').addEventListener('click', function () { console.log('clicked'); }); }点击回调虽然没用largeData,但它和setup函数共享同一个作用域,V8无法确定你会不会在回调里用它,所以干脆都留着。这就白占了内存。解决方法是把用不到的变量指为null,或者把事件监听函数定义在外面,不做多余的嵌套。
从这个角度切入回答内存泄漏,比单纯背“闭包会泄漏”要有深度得多。因为面试官真正想听的,是你能不能辨别哪部分内存是合理的、哪部分是不合理的。这也是为什么我把作用域和闭包放在第一章节:它的可延展性太强了。
2. this指向与绑定规则:八股里最叛逆的主角
JavaScript里最让初学者崩溃、也让面试官百问不厌的知识点,非this莫属。它和闭包正好是两极:闭包考理解,this考细心。可一旦你掌握了背后那几条绑定规则,你会发现它的逻辑比想象中要简单、死板得多。
2.1 四种绑定规则:默认、隐式、显式、new
this的指向可以归纳为四个词:默认绑定、隐式绑定、显式绑定、new绑定。优先级从低到高分别是:默认绑定(非严格模式下指向globalThis,严格模式为undefined)、隐式绑定(谁调用指向谁)、显式绑定(call/apply/bind指定)、new绑定(构造出来的新对象)。
来看一个经典的连续剧场景:
const obj = { name: 'obj', getName: function () { console.log(this.name); } }; const fn = obj.getName; fn(); // undefined(非严格模式指向window/globalThis,window.name通常为undefined)这个考的就是隐式绑定的丢失:你把方法赋值给了变量,调用时上下文丢了。面试官特别爱拿这个组合拳来问,一组代码下来,你对this的掌握程度就露馅了。
另一个高频变体是箭头函数里的this:
const obj = { name: 'arrow', getName: () => { console.log(this.name); } }; obj.getName(); // undefined箭头函数没有自己的this,它遵循词法作用域,向外层找普通函数的this。这个设计的意义在于解决回调函数里this丢失的问题,也是React类组件时代处理事件绑定最常见的坑之一。提到这里可以顺带说一句:箭头函数不能用new,也没有prototype属性。
2.2 显示绑定与偏函数哲学
call、apply、bind是三兄弟,但用法上有细微差别。call和apply是立即执行,差别只在参数传递方式:call是散列传参,apply是数组传参。bind则是返回一个新函数,下次调用时再用。这里有一个容易被问到的小细节——如何实现一个bind?
手写bind是个很好的热身题,因为要涉及this指向和闭包的结合:
Function.prototype.myBind = function (context, ...args) { const fn = this; return function (...restArgs) { return fn.apply(context, args.concat(restArgs)); }; };这样写基本能过,但如果面试官追问一句“那new调用这个绑定函数会发生什么?”,很多人的回答会卡壳。原生bind的特性是:如果用new调用绑定后的函数,this指向新创建的实例,context参数被忽略。更完整的实现还要考虑到这一点。遇到这种追问,你主动承认“这里需要加一个判断”并补上,比硬说“已经完成了”要靠谱得多。
2.3 箭头函数 vs 普通函数的底层差异
面试里一个必考切面是“箭头函数为什么不能作为构造函数”。要回答这个问题,最好从对象模型的角度来说:普通函数有自己的this、arguments,并且是function构造器创建的,可以通过new来创建实例。箭头函数没有自己的this、没有arguments、没有prototype属性,所以引擎层面就不支持构造调用。
这样的回答为什么比绕语法规则更好?因为它说明了JavaScript对象的底层差异,而不只是记忆层面。有一个高频追问是“箭头函数能改变this吗?”,答案是:不能,但可以间接改变——只能通过改变外层普通函数的this来影响它。比如外层是某个对象的方法,那你改那个对象的方法调用者,箭头函数里的this才能跟着变。
3. 事件循环与异步机制:面试重灾区、实战核心区
如果说闭包和this是面试热身,事件循环就是JS面试的正餐。它关系到代码执行的先后顺序、性能优化、以及你对浏览器渲染原理的理解。几乎每个二面、三面都会有一道事件循环代码题,而且经常是那种“看起来有坑,实际上确实有坑”的题。
3.1 单线程与任务队列:为什么要有事件循环
JavaScript是单线程的,这是由它的初始用途决定的——浏览器里,JS的主要职责是操作DOM和响应用户交互。假设多线程同时改同一个DOM节点,页面状态会无法预测,轻则显示错乱,重则直接崩溃。所以语言设计者干脆让它单线程,再配合事件循环来实现异步。
那异步怎么实现?核心就是任务队列。遇到setTimeout、Promise、事件监听这类操作,主线程不会干等着,而是把回调注册进任务队列,等当前执行栈清空后再来处理。事件循环做的事情很简单:不断从任务队列里取任务,放到执行栈上执行,周而复始。
这个模型可以用生活场景类比:你一个人在柜台办业务(单线程),一边办一边收新单子。老客户(微任务)会在你办完手头这件事后优先叫号进来,新外宾(宏任务)需要排到下一轮。这对理解微任务和宏任务的区别很有帮助。
3.2 宏任务与微任务的执行顺序
背结论容易,验证结论难。事件循环里最常见的口诀是:先执行同步代码,遇到微任务就排进微任务队列,遇到宏任务就排进宏任务队列;当执行栈清空后,清空所有微任务;然后提取一个宏任务执行,执行完再清空微任务,如此往复。
看一道典型题:
console.log(1); setTimeout(() => { console.log(2); }, 0); Promise.resolve().then(() => { console.log(3); }); console.log(4); // 输出顺序?答案是1、4、3、2。原因是Promise.then属于微任务,setTimeout属于宏任务;同步代码全部跑完后,优先清空微任务队列,然后才轮到宏任务。这道题是入门级的,但很多工作两三年的同学都会答错,错的根源就是没有区分微任务和宏任务的处理窗口。
进阶版本的题会在Promise里嵌套setTimeout,或者在setTimeout里再制造微任务。这类题的核心解法是画时间轴,用事件循环模型逐层分析,而不要凭感觉猜。面试时也可以主动说出你推导的过程,这比只说一个答案更能拉分。
3.3 浏览器渲染时机与requestAnimationFrame
多数人讲事件循环会漏了渲染时机,但面试官如果问“setTimeout(0)真的会立即执行吗?”,其实就是在考察你知不知道浏览器渲染和任务调度之间的协作关系。
浏览器并不是每执行一个任务就渲染一次页面,它有自己的渲染步骤,在事件循环里,两个宏任务之间通常会有渲染的机会。因此requestAnimationFrame这个API被设计为在下次渲染之前执行回调,能够保证帧率稳定。如果页面用setTimeout做动画,很容易出现抖动,因为setTimeout不感知渲染时机,可能在两次渲染之间挤入多个回调,也可以理解为你和朋友约定好了每15分钟碰一次头,但对方是按自己的节奏来,不一定会让你俩刚好碰对面。requestAnimationFrame则是保证“下一次浏览器绘制前来更新我的状态”,性能会好很多。
如果你的项目里有涉及大列表滚动、Canvas动画、手势交互等场景,把这一段主动讲出来,面试官会认为你有真实性能优化经验,而不只是背了一堆概念。
3.4 微任务在源码中的应用:浏览器如何执行Promise
有些面试官会追问:“Promise.then注册的回调,在V8里到底怎么执行的?”这个问题看着很深,其实可以简化理解:Promise的then方法内部会创建微任务,把回调封装后通过queueMicrotask或内部MicrotaskQueue提交给宿主环境。宿主环境(浏览器/Node.js)在适当的时候执行微任务队列。
如果你平时用Vue,可以提一下Vue的nextTick也利用了微任务机制,把DOM更新后的回调安排到微任务队列里,确保每次状态变化只触发一次渲染。这是事件循环知识在前端框架层面的落地,非常加分。
4. 原型与原型链:面向对象的根基,也是被低估的深水区
面试JavaScript的时候,原型链是必须过的关。它的常用问法是“原型链是什么”、“instanceof的原理”、“如何实现继承”。很多自学前端的人在这块有知识盲区,因为它不像闭包那样能被业务代码直观验证,更像一个抽象模型。但是面试官特别喜欢在这里展开问,因为能看出你是“背过题目”还是“通透了语言”。
4.1 原型、构造函数、实例:三角形关系图
要理解原型链,首先要理清三个概念:构造函数(constructor)、原型对象(prototype)、实例对象(instance)。构造函数通过new创建实例,实例的内部[[Prototype]]指针指向构造函数的prototype对象。浏览器里可以通过__proto__来访问这个内部指针,ES6之后推荐用Object.getPrototypeOf()。
一个常见误区是“prototype是构造函数的属性,也是实例的属性”。准确说,实例没有prototype属性,只有构造函数有;实例通过__proto__拿到的是它所属构造函数的prototype。所以判断一个属性是“自有属性”还是“原型链上的属性”,可以用hasOwnProperty。
面试时可以画简单三角形(口头描述也行),把Constructor、prototype、instance这三者关系理顺。很多同学栽在“Foo.prototype.constructor === Foo”这个细节上,这个结论虽然绕口,但它是原型关系里很重要的一条边——原型对象默认有一个constructor属性,指回构造函数。
4.2 原型链查找与instanceof的原理
当访问obj.someProp时,引擎会先从obj自身属性里找,找不到就顺着obj.__proto__找,再找不到就顺着obj.__proto__.__proto__继续找,直到null为止。这个过程就是原型链查找。
function Animal() {} const dog = new Animal(); console.log(dog instanceof Animal); // trueinstanceof的原理就是:沿着Animal.prototype是否出现在dog的原型链上。这是官方定义,也是面试里最常背的结论。但还有一个更隐蔽的细节:如果Animal.prototype被整体替换了,比如Animal.prototype = {},之前创建的实例还能不能通过instanceof Animal?答案是不能,因为实例的__proto__还指着旧的prototype对象。
这种细节对一般业务没影响,但面试官喜欢拿它来区分“理解”和“死记”。建议面试前自己动手跑一遍替换prototype的案例,真的实验过一次,记忆会深刻很多。
4.3 实现继承的N种姿势与class的底层真相
ES6的class是语法糖吗?这个问题的标准答案是:是语法糖,但不仅仅是语法糖。class底层还是原型的组合,只是语法上更像传统面向对象。它内部仍然通过prototype链来实现方法共享,实例属性则是构造时的自有属性。
面试里手写继承,常见的方案有:原型链继承、构造函数继承、组合继承、寄生组合继承。组合继承是最经典的一种:
function Parent(name) { this.name = name; } function Child(name, age) { Parent.call(this, name); this.age = age; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;为什么用Object.create(Parent.prototype)而不是直接new Parent()?因为Object.create不会执行Parent构造函数,不会在Child.prototype上创建多余的name属性,是干净的继承。这一步在寄生组合继承里叫“原型式继承”,也是推荐的标准方案。如果你再把这段封装成inheritPrototype函数,就算答得很完整了。
5. Promise与前端异步方案的进化史
如果面试只考一个异步知识点,那一定是Promise。它既涉及手写、也涉及运行机制理解、还涉及实际业务中的可维护性问题。从回调地狱到Promise再到async/await,这条路本身就是一个很完整的面试叙事线。
5.1 Promise的状态机与不可逆性
Promise三大状态:pending、fulfilled、rejected。状态一旦从pending变为fulfilled或rejected,就不可再变。这就是Promise的核心约束。面试里经常出的变体题是:
const promise = new Promise((resolve, reject) => { resolve('第一次'); reject('第二次'); resolve('第三次'); }); promise.then(console.log).catch(console.error);结果只输出“第一次”,因为状态已经锁死了。这个设计保证了异步结果不会被二次篡改,也为链式调用提供了稳定性基础。
理解状态机之后,接着要会说then的链式调用机制:then方法返回一个全新的Promise,且会把回调函数的返回值作为这个新Promise的resolve值。这个机制支撑了链式调用和错误传递,是手写Promise的基础。
5.2 手写Promise:从能用到能用明白
面试手写Promise,不一定要求你把Promise/A+规范完整实现,但至少要做到三点:状态流转正确、then支持链式调用和值穿透、catch能捕获前面的错误。这里分享一个简化版的核心骨架思路:
class MyPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.reason = undefined; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; // resolve/reject的实现 } then(onFulfilled, onRejected) { // 返回新的Promise,处理回调注册与执行 } }很多人会卡在“then方法如何拿到回调的返回值并把它传给下一个then”。其实关键就在then内部创建了一个新的MyPromise,在里面调用onFulfilled并把结果resolve掉。这个思路一旦通了,手写Promise就成功了一大半。
再往上加分的话,可以提一下Promise.resolve的静态方法、Promise.all和Promise.race的实现思路。尤其是Promise.all,需要维护一个结果数组和一个完成计数器,同时要处理“传入非Promise值”的情况。这些是业务里高频使用、能手写的人却不多的点。
5.3 async/await的错误处理与并发优化
async/await的本质是Promise的语法糖。面试常见的追问是“怎么捕获async函数里的错误”。标准方案是try/catch,但更优雅一点的做法是写一个await wrapper:
function to(promise) { return promise.then(data => [null, data]).catch(err => [err, null]); } // 用例 const [err, data] = await to(fetchData());这个模式在Node.js和前端数据请求里都很流行,也叫“Go风格错误处理”。它能避免大量try/catch嵌套,让逻辑更平铺。面试时主动用这个,会被当成有工程经验的人。
并发优化常考的是Promise.all和Promise.allSettled的选择。如果几个请求互不依赖,用Promise.all并行;但如果其中一个请求失败且你不想影响其他结果,用Promise.allSettled更合适。还有场景是控制并发数量,这时候可以手写一个pLimit式的并发池,这也是近年大厂面试的高频手写题。
6. ES6+核心特性:张口就来的背后要讲出原理
ES6+的新特性太多,面试不可能全考,但有几个点是必覆盖的:let/const与暂时性死区、解构赋值、展开运算符、Map/Set、模块化、Proxy/Reflect。同一个知识点,会背特性和能讲透原理,面试效果完全不同。
6.1 let与const:从变量提升到暂时性死区
let和const与var的差异是面试第一梯队,核心结论大家都背过:let/const块级作用域、不存在变量提升、不能重复声明。但“暂时性死区”才是真正的考点。
所谓暂时性死区(TDZ),是指在代码块内、变量声明语句之前,这个变量是不可访问的。哪怕外层作用域有一个同名变量,也不会被内层的“未声明”变量遮蔽,访问就会抛ReferenceError。这个机制的作用是“先声明后使用”的强制约束,比var要严谨得多。
有一个经典代码题:
var x = 10; function test() { console.log(x); let x = 20; } test(); // ReferenceError原因就是x在块内被let声明了,但console.log执行在声明之前,x处于暂时性死区。这题特别阴,因为它同时混入了var和let,考察的是作用域遮蔽机制。如果没答上来,停下来想一想再复述一遍,通常就能记住很久。
6.2 解构与展开:语法糖背后的迭代器协议
解构赋值看似基础,但它背后有“迭代器协议”在支撑。数组解构依赖可迭代协议,对象解构则依赖属性枚举。所以一个对象能被for...of遍历的前提是它实现了[Symbol.iterator]方法,而普通的对象字面量没有实现。
能讲清楚这点,等于把解构、for...of、扩展运算符背后的机制串起来了。因为展开运算符...在数组和对象上的行为其实不同:数组展开走迭代器协议,对象展开走属性拷贝(Object.assign类似),两者是不同路径。这也是为什么{...arr}能得到一个以索引为键的对象。
面试问到Map和Set时,也可以从迭代器切入:Map的key是有序的,可以按照插入顺序遍历;Set里的值是唯一的,判断唯一性的逻辑类似于Object.is但不是全等于,NaN在Set里算作同一个值。这里有个冷门考点:Set中NaN只能存在一个。
6.3 Proxy与Reflect:响应式系统的地基
Vue 3的响应式原理基于Proxy,这是2026年面试逃不开的知识点。手写一个简版的响应式reactive是很常见的二面题,核心就是利用Proxy的get/set拦截:
function reactive(target) { return new Proxy(target, { get(obj, key) { track(obj, key); return obj[key]; }, set(obj, key, value) { obj[key] = value; trigger(obj, key); return true; } }); }这里的track和trigger是依赖收集和派发更新的核心。能顺手说出这两步,说明你是真理解响应式原理,不是背API。Reflect通常和Proxy一起出现,它解决了Object.defineProperty等操作的返回值问题,让拦截操作更符合JavaScript的预期。
这里分享一个小技巧:面试被问到“Proxy和Object.defineProperty的区别”时,用三个维度回答——功能层面(Proxy能监听新增属性、删除属性、数组索引变化,defineProperty不行)、性能层面(Proxy拦截的是整个对象,defineProperty是逐属性劫持,对象大时defineProperty初始化开销高)、语义层面(Proxy是标准化元编程机制,defineProperty偏底层约束)。这个答案结构很加分。
7. 浏览器与JavaScript运行机制交叉考点
不少前端面试在聊完语言本身之后,会转向浏览器视角。因为JavaScript是跑在浏览器里的,语言特性和运行环境结合的题目,最考验候选人的综合能力。
7.1 垃圾回收:从闭包看V8的内存管理
很多同学只听过“垃圾回收”四个字,但说不出细节。近几年面试问法的趋势是往深入方向走,比如“V8的垃圾回收机制是什么”、“哪些情况会产生内存泄漏”。
V8对JavaScript堆内存采用分代回收机制:新生代用Scavenge算法,把存活对象复制到另一半空间;老生代用标记-清除和标记-整理算法。对于前端面试,不需要背到算法实现那么深,但至少要能说出“新生代、老生代”、“标记-清除”、“增量标记”这些词,并能解释它们解决什么问题。
一个很好的展开路径是:闭包引用 → 内存不释放 → 用WeakMap/WeakSet解决。WeakMap的键是弱引用,不会阻止垃圾回收器回收键对象,非常适合存储DOM节点相关的私有数据。Vue 3内部也大量使用了WeakMap来存放依赖关系。
7.2 事件委托与冒泡捕获:实现一个无限滚动列表
事件代理在业务里非常常用,面试也经常以“为什么要用事件委托”来考察。核心结论:通过给父节点绑定监听,利用事件的冒泡阶段,统一处理所有子节点的事件。
document.querySelector('#list').addEventListener('click', (e) => { const target = e.target.closest('li'); if (target) { console.log(target.dataset.id); } });这三行代码能回答很多问题:为什么动态添加的元素不需要重新绑定事件?事件委托的收益是什么?closest比手动遍历父节点要方便多少?如果想展示更扎实的基本功,可以顺着讲stopPropagation和preventDefault的区别:一个是阻止传播路径,一个是阻止默认行为。
7.3 localStorage、sessionStorage与Cookie:存储字段背后的取舍
前端存储也是常考基础。面试官会问它们三者的区别,以及什么时候用什么。核心差异点是容量、生命周期、作用域、网络传输。
localStorage容量一般在5MB左右,持久化存储,同源共享。sessionStorage是标签页级别的,关闭标签页就没了。Cookie容量只有4KB,会随HTTP请求自动发送,所以不适合存大数据,常用于身份凭证。
近年面试更倾向于问“如果让你在localStorage上封装一个带过期时间的存储工具,怎么写”。这是一个典型的“基础API + 工程能力”的题目,可以用时间戳简单实现:
function setItemWithExpire(key, value, expireSeconds) { const data = { value, expireAt: Date.now() + expireSeconds * 1000 }; localStorage.setItem(key, JSON.stringify(data)); } function getItemWithExpire(key) { const data = localStorage.getItem(key); if (!data) return null; const parsed = JSON.parse(data); if (Date.now() > parsed.expireAt) { localStorage.removeItem(key); return null; } return parsed.value; }类似的小工具在业务里很常见,面试中现场写出来了,是一个很大的加分项。
8. 高频手写题集锦:面试最后一公里的验收标准
手写题在2026年的面试里占比越来越高,因为它天然过滤掉了“背概念党”。如果只让我押五道必考手写题,那就是:防抖节流、深拷贝、Promise.all、数组拍平去重、call/apply/bind。这些题都有一些变化,但万变不离其宗。
8.1 防抖和节流:从场景到实现再到亮点
防抖(debounce)和节流(throttle)是业务使用频率最高、也被问得最多的手写题。理解它们区别的关键是看触发频率和执行的对应关系:防抖是“你停了我才执行”,适合搜索框输入、窗口resize;节流是“固定时间内最多执行一次”,适合滚动加载、按钮点击限制。
防抖的实现核心是每次调用都重置定时器:
function debounce(fn, delay) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }节流的实现核心是有“冷却期”:
function throttle(fn, interval) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } }; }到这里只是及格分。想加分,可以主动说两句:防抖节流函数要能apply(this, args),因为事件处理函数里this有业务含义;如果希望首次立即执行,需要加一个immediate参数;如果有取消需求,可以返回一个cancel方法。这些“边界”是面试官真正想听的。
8.2 深拷贝:从能写到能防住循环引用
手写深拷贝是必考题,最基础的版本就是JSON.parse(JSON.stringify()),但你要知道它的局限:会丢失函数、undefined、Symbol、循环引用会报错。
能写递归版本是第二层:
function deepClone(obj, map = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (map.has(obj)) return map.get(obj); const clone = Array.isArray(obj) ? [] : {}; map.set(obj, clone); Reflect.ownKeys(obj).forEach(key => { clone[key] = deepClone(obj[key], map); }); return clone; }用WeakMap来缓存已经克隆过的对象,是为了解决循环引用。这里再提供一个加分项:如果对象里有Date、RegExp等特殊类型,克隆时要保留它们的原型而不是单纯变成普通对象。能主动说出这个,说明你真的处理过复杂数据结构的克隆。
8.3 数组进阶:拍平、去重、乱序与原地操作
数组方法除了map、filter、reduce之外,手写题最喜欢的是:flat实现、去重、乱序。很多人不知道flat的底层可以用递归加reduce来实现,这就是手写flat的考点:
function flat(arr, depth = 1) { if (depth === 0) return arr.slice(); return arr.reduce((acc, item) => { if (Array.isArray(item)) { acc.push(...flat(item, depth - 1)); } else { acc.push(item); } return acc; }, []); }数组去重的三种方式也要能随手写出:Set、filter + indexOf、reduce + includes。如果要考虑NaN去重,Set方案天然支持;如果要考虑对象的去重,需要自定义比较函数。乱序排序则注意:不要用Math.random() - 0.5配sort,这样有概率偏斜,正确做法是Fisher-Yates洗牌算法:
function shuffle(arr) { const result = arr.slice(); for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; }9. 面试作答技巧与常见误区总结
说完了具体知识点,最后聊点非技术的但同样重要的东西。我从被面和面人的经历里总结了几个必须注意的坑,希望大家不要重蹈覆辙。
9.1 回答方式:先结论后展开,别绕圈子
面试官一天可能面五六个人,同样的问题要听很多遍。你“是不是答到点子上”他们几秒钟就能判断出来。回答技术问题时,最好的结构是:先给结论,再用代码或场景展开,最后补充边界条件。
举一个例子:如果问“闭包是什么”,不要一上来就念定义。先说“一个函数能访问它定义时的作用域变量,而且是在外部函数执行完之后依然能访问”,然后拿一个计数器或私有变量的例子做支撑,再补充“核心是作用域链的保留”。这样下来,你的表达既有层次,又有节奏,面试官会跟着你的思路走。
最忌讳的就是东扯西扯,先讲半天“我之前的项目里面遇到过”,就是不进入正题。有话直说,说完整的,这本身就是工程协作里的核心沟通能力。
9.2 代码书写习惯:在剑道场里,你的姿态就暴露了实力
手写代码时,面试官不仅看结果,也看过程和代码风格。命名是否语义化、有没有用const而不是var、是否考虑边界条件、函数是否拆分合理——这些细节在不经意间决定了你的评级。
比如手写防抖的时候,如果临时变量用var声明,面试官心里会打个问号:是不是平时还在写ES5风格?如果用了箭头函数还顺手处理了this和参数,形象分直接拉满。再比如写完代码之后主动说“我考虑一下边界情况”,然后补上循环引用或空值的处理,整个过程非常加分。
有一个实战习惯我特别推荐:面试前用一周时间,把高频手写题在纸笔上写一遍,不借助IDE。这样能倒逼你在写之前想清楚变量名、参数和边界,而不是靠编辑器的补全去凑。这种“裸写”能力在面试中非常占便宜。
9.3 从八股到体系:用“讲故事”串联知识点
最后再分享一个我自己常用的复习方法:不要孤立地背知识点,而是试着用一条主线把很多考点串成故事。
比如从“事件循环”出发,你可以串起同步/异步、微任务/宏任务、Promise、async/await、requestAnimationFrame、Vue的nextTick、浏览器渲染性能优化。再从“原型链”出发,你可以串起构造函数、class语法糖、继承、instanceof、原型链污染。从“this绑定”出发,可以串起普通函数、箭头函数、call/apply/bind、事件处理函数、构造函数。
这样的串法才是真正的知识体系。面试官问一个点,你顺着主线能引到另一个点,这种“知识迁移能力”是区分“背题选手”和“真懂选手”的关键。我在实际面试中见过不少人单个知识点都背得滚瓜烂熟,但一旦被追问“那你觉得这事跟你上个项目有什么关系”,就瞬间卡壳。原因不是他不努力,而是没有建立知识之间的连接。
这份内容覆盖了JavaScript面试的大部分高频考点,但真正的竞争力不来自收藏,而来自你亲自动手跑一遍、写一遍、总结一遍。铜九铁十虽然听着不景气,但这恰恰是认真准备的人拉开差距的时候。希望下次面试官问到你“讲讲闭包”的时候,你能从定义到原理到应用场景再到内存注意点,一口气说到对方点头。