
准备前端面试JavaScript是绕不开的第一道门槛。不论你是刚毕业投校招还是工作了三五年想跳槽面试官基本都会在JS基础这个环节上做文章。我在带人和面试的过程中见过太多次类似的场景简历上写着“熟悉Vue/React能独立负责模块开发”结果被问到this指向、闭包、事件循环这类问题时答得支支吾吾或者只能背出定义讲不清楚原理。这篇文章把我这些年筛选过的、在高频面试中反复出现的JS考点整理了出来不是简单罗列题目而是把每个知识点背后的考察意图、答题思路和易错点都拆开讲。阅读的时候建议你直接把它当成一份自查清单每个小节先暂停一下在脑子里组织一遍回答讲不出来的地方打上标记回头再重点看。尤其是那些带代码的题最好亲手在控制台跑一遍面试的时候才能说得有底气。1. 类型、拷贝与传参第一轮“摸底”必问面试官开场往往不会直接上难题目而是先用几个看似基础的问题把你稳住但这几个基础题恰恰是最能看出底子深浅的。类型判断、深浅拷贝、值传递和引用传递就是这个环节的常客。1.1 typeof 与 instanceof边界问题才是考点typeof 是大多数人最早接触的类型判断方式但面试官考的不是你有没有背过结果表而是你对边界的理解。比如typeof null object这是JavaScript诞生时就留下的bug因为早期的实现里JS的值是用一个类型标签加对象引用来表示的null的标签是0正好对应object后来想改已经改不动一堆老代码依赖这个行为。你能讲出这段历史比单纯背结果要加分得多。还有几个容易漏掉的点typeof一个未声明的变量返回undefined而不是抛错这是typeof相对其他操作的独特之处typeof function(){}返回function但function本质也是对象所以面试里如果被问到“JS中函数是对象吗”答案应该是“函数是对象但它是可调用的对象而且函数对象上还有prototype属性这与普通对象不同”。这道题我在热词里也看到了说明确实是大家关注的高频点。instanceof 的原理则是沿着原型链查找左侧对象的原型链上有没有右侧构造函数的prototype对象。这里有个边界容易被问到1 instanceof Number返回false因为基础类型的字面量不是Number实例只有new Number(1) instanceof Number才是true。另外跨iframe时instanceof会失效因为不同全局环境下的构造函数不是同一个引用。最稳定的类型判断方式是Object.prototype.toString.call(value)它返回[object Array]这类内部标签ES6还有个Symbol.toStringTag可以自定义这个标签算是比较进阶的知识点。1.2 值传递还是引用传递一道题试出基本功面试官只要给出下面这段代码基本就能判断你对JS传参模型有没有想清楚const obj { n: 1 }; function change(o) { o.n 2; o { n: 3 }; } change(obj); console.log(obj.n); // 输出什么答案是2。函数参数o拿到的是obj的一个引用副本所以o.n 2改的是同一个对象但o { n: 3 }只是让局部变量o指向了一个新对象并不会影响外部的obj。所以准确的说法是JS中所有参数传递都是“值传递”只不过引用类型传递的是“引用值的副本”。很多人笼统地说“引用类型是引用传递”严格来讲站不住脚。我面试时还喜欢用另一道延伸题考察对象赋值的执行顺序let a { n: 1 }; let b a; a.x a { n: 2 }; console.log(a.x); // undefined console.log(b.x); // { n: 2 }a.x a { n: 2 }这条语句看起来绕但规则是先从右往左确定右边的值再从左往右依次赋值。执行时右侧是{ n: 2 }这个对象先把a.x赋值为这个对象此时a还指向旧对象{ n: 1 }再把a重新指向新对象。所以旧对象{ n: 1 }上的x指向新对象而新对象上并没有x属性。能把这个过程分析清楚说明你对“对象是引用、赋值是拷贝引用”有真实理解。1.3 深拷贝别只背 JSON.stringify 的用法深拷贝几乎是必考题因为业务里太常用了。最简单的方案是JSON.parse(JSON.stringify(obj))但它有一堆坑对象里的undefined、函数、Symbol会被直接忽略NaN和Infinity会变成nullDate对象会变成字符串RegExp会变成空对象循环引用直接抛TypeError。面试官问这个点就是想看你能不能说出这些限制以及项目里遇到这些问题时怎么处理。正确的思路是回答到“分层处理”先用WeakMap解决循环引用再对 Date、RegExp、Map、Set 这类特殊对象单独造克隆键名时用Reflect.ownKeys来兼顾Symbol属性。下面这个版本是我在实际手写题里比较推荐的function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) return target; if (target instanceof Date) return new Date(target); if (target instanceof RegExp) return new RegExp(target.source, target.flags); 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; }注意一点实际业务里如果环境支持优先用structuredClone或lodash.cloneDeep手写版本的价值在于面试时展示你对边界问题的考虑而不是让生产环境跑一段半吊子实现。我见过太多候选人写了循环依赖处理就觉得万事大吉其实连Date都没判断这种半桶水在面试官追问下很快会露馅。2. 作用域、提升与闭包JS 的灵魂考点作用域和闭包是JS面试里最大的一个板块也是区分“背题型选手”和“理解型选手”的分水岭。面试官爱在这里深挖因为闭包直接关系到你平时写的组件、Hooks、函数式工具代码里很多莫名Bug的根因。2.1 变量提升与暂时性死区JavaScript在执行代码之前会有一个变量收集的过程var声明的变量会被提升到当前作用域顶部并初始化为undefined所以console.log(a)写在var a 1之前不会报错输出undefined。但let和const不是这样它们同样会被提升但从“进入作用域”到“声明被实际执行”之间的区域是暂时性死区在这个区域里访问变量会直接抛ReferenceError。console.log(a); // undefined var a 1; console.log(b); // ReferenceError: Cannot access b before initialization let b 2;面试官如果继续追问“let到底有没有提升”你可以说“有提升只是不初始化”C或Java背景的面试官听到这个差异会眼前一亮。另外函数声明比var提升得更靠前所以一个函数声明和一个var变量重名时函数优先。这个细节可以用来解释一些看起来很奇怪的代码顺序问题。2.2 闭包概念、应用与内存闭包的定义就一句话函数和它声明时所在的词法作用域的组合。但更实用的理解是一个函数在定义时记住了自己所在的作用域即使外层函数已经执行完了它依然能访问外层函数的变量。典型例子function counter() { let count 0; return function () { count; return count; }; } const c counter(); console.log(c()); // 1 console.log(c()); // 2counter执行完后本应被回收但返回的函数还占着count的引用所以count一直留在内存里。这就是闭包的应用来源私有变量、模块化封装、防抖节流、函数柯里化都是靠闭包来保存“私有状态”。缺点是闭包会让变量常驻内存滥用闭包可能导致内存占用上升所以用完的引用可以置为null帮助回收。答题时如果能主动说出“闭包是延迟回收机制的一部分有内存代价”面试官会认为你有实践经验而不只是会背“闭包就是函数套函数”。我在面别人的时候经常追问“闭包产生内存泄漏最常见的业务场景是什么”很多人答不上来答案是事件监听器里绑定的闭包函数在组件销毁时没有移除监听导致老的DOM和组件数据一直被引用。2.3 循环里用 var 还是 let为什么这题应该算JS面试的“送分题”但每年都有大量候选人栽在上面for (var i 0; i 3; i) { setTimeout(() console.log(i)); } // 输出 0 1 2 还是 3 3 3答案是 3 3 3。var声明的i没有块级作用域setTimeout里的回调到执行时才会去读取i那时候循环已经结束了i的值是3。如果把var改成let输出就是0 1 2因为let为每次迭代创建了独立的块级绑定每个回调里的i都是那一轮循环独有的值。另一种解决方式是IIFE包裹或者用闭包保存当前值for (var i 0; i 3; i) { (function (j) { setTimeout(() console.log(j)); })(i); }这个题考的不只是语法差异还牵扯到异步回调、词法作用域、闭包保存机制所以面试官喜欢把它作为“基础到进阶”的桥梁题。回答时可以顺带提一句let在循环里的语义是规范级别的特殊处理每轮迭代都会创建一个新的词法环境。3. this 指向与事件机制最容易被绕晕的部分this的指向问题在JS面试里的出现频率仅次于闭包。老实说很多写了两三年业务的开发者都搞不清this因为框架帮他们把this包装好了脱离了框架一问就露馅。这一章要讲清楚绑定的规则以及事件机制里常见的一些迷惑点。3.1 this 的四条绑定规则this的指向不取决于函数在哪里定义而是取决于函数在哪里被调用。规则按优先级排分别是new绑定、显式绑定call/apply/bind、隐式绑定obj.fn()形式、默认绑定独立调用。默认绑定非严格模式下独立函数调用this指向全局对象严格模式下this是undefined。浏览器里的全局对象是windowNode环境的全局对象是global很多人控制台跑代码和在浏览器环境里跑代码结果不一样就是因为环境不同。隐式绑定obj.fn()调用时this指向obj。但要小心“引用丢失”的情况const obj { name: obj, fn() { console.log(this.name); } }; const fn obj.fn; fn(); // this指向全局不是obj把方法赋值给变量再调用就丢失了隐式绑定。同理setTimeout(obj.fn, 0)也会丢失this。解决方式是用bind绑定setTimeout(obj.fn.bind(obj), 0)。显式绑定fn.call(obj, ...)、fn.apply(obj, [...])立即执行并指定thisbind返回一个新函数永久绑定this。优先级上new绑定可以覆盖显式绑定这就是后面手写bind时为什么要考虑new场景的原因。3.2 箭头函数的 this 到底怎么取箭头函数没有自己的this它捕获的是定义时所在词法作用域的this并且之后用call、apply都改不了。同时它也没有arguments对象不能当构造函数用没有prototype属性。面试高频追问是为什么Vue组件里不推荐用箭头函数定义method因为method需要把this绑定到组件实例上如果用箭头函数this在定义时就已经被固定成了外层作用域的this运行时的组件实例根本拿不到。React里同样有类似坑class组件里如果不绑定事件处理函数或者用了箭头函数属性this行为会比想象的复杂。回答箭头函数时我建议主动做一个对比普通函数看“调用点”箭头函数看“定义处”。这个概括很简洁面试官一听就知道你是懂的。3.3 事件机制event、冒泡捕获与事件委托热词里有“js 事件中的 event”这确实是前端开发绕不开的。事件的传播分三个阶段捕获阶段从window到目标、目标阶段、冒泡阶段从目标回到window。addEventListener的第三个参数传true是捕获不传或false就是冒泡。stopPropagation()可以阻止继续传播stopImmediatePropagation()还能顺带阻止同元素上的后续监听器执行。preventDefault()只阻止默认行为比如表单提交、链接跳转但不会阻止事件传播。事件委托是这个板块里最实用的考点利用冒泡机制把事件监听加在父元素上通过event.target判断实际触发子元素。好处是减少监听器数量、对动态新增的节点同样有效。我面试时问过一道实际场景一个商品列表每个商品项有一个删除按钮动态新增商品后删除功能如何保证有效答案就是事件委托。很多业务里小数点这个问题比想象中多比如弹窗组件、表格组件里大量用到。顺带提一句前端圈里常见的“验证码输入框”交互也是典型的事件机制应用监听容器的input事件根据当前值自动跳转到下一个输入格而“三级联动”下拉则更偏数据驱动用change事件监听选择结果再渲染下一级本质是把数据结构和事件状态结合起来。这两个方向如果面试中遇到思路其实是通的。4. 原型与继承原型链是理解JS对象的钥匙原型和原型链这块很多人停留在“背图”的阶段真正被问到细节就卡壳。其实它没有想象中抽象把握住几个核心对象之间的关系基本能应对绝大多题目。4.1 构造函数、原型对象与实例的关系记住三条链就行每个函数都有prototype属性指向原型对象。每个实例都有__proto__属性指向构造该实例的构造函数的prototype。原型对象上有constructor属性指回构造函数。验证一下function Foo() {} const f new Foo(); console.log(f.__proto__ Foo.prototype); // true console.log(Foo.prototype.constructor Foo); // true console.log(f instanceof Foo); // true沿着__proto__一路向上最终会到达Object.prototype它的__proto__是null这就是原型链的终点。数组、函数、普通对象的原型链都要经过这个终点所以Object.prototype.toString对所有对象都可用。4.2 new 到底做了什么手写new是一个很好的检验题因为new的每一步都对应原型链的核心概念function myNew(Constructor, ...args) { const obj Object.create(Constructor.prototype); const result Constructor.apply(obj, args); return result ! null (typeof result object || typeof result function) ? result : obj; }关键点是如果构造函数显式返回了一个引用类型实例就是那个返回的对象而不是新创建的obj如果返回的是基础类型或者没返回实例就是obj。这段代码里Object.create(Constructor.prototype)直接完成了“原型绑定”这一步是比obj.__proto__ Constructor.prototype更稳的写法。4.3 继承方案怎么演进从原型链到 class面试里常见的继承考法是让你分析“为什么不用某种继承”。简单梳理一下演进线原型链继承Child.prototype new Parent()。问题有两个父类实例上的引用类型属性会被所有子实例共享改一个全变而且没法向父类传参。借用构造函数在子构造函数里Parent.call(this, name)。解决了传参和引用共享问题但方法都定义在构造函数里没法复用还调用了一次父构造函数。组合继承借用构造函数原型链继承救回了复用但父构造函数被调用两次子原型上有一份多余的父实例属性。寄生组合继承只用Object.create(Parent.prototype)来建立原型关系父构造函数只在子构造函数Parent.call时调用一次这是ES5里最优雅的方案。function inheritPrototype(child, parent) { child.prototype Object.create(parent.prototype); child.prototype.constructor child; }ES6的class只是寄生组合继承的语法糖extends的底层逻辑还是基于原型链。面试被问到时能把这个演进过程讲一遍比单纯写一行class Child extends Parent有说服力得多。5. 事件循环、宏任务微任务与异步编程异步是前端面试里最硬核的板块也是最能拉开差距的部分。因为它不考“背概念”而是考你能不能准确推演一段代码的输出顺序。5.1 JS 单线程模型与执行顺序JavaScript是单线程的但通过事件循环机制实现了非阻塞。执行顺序可以概括为同步代码先执行执行栈清空后处理微任务队列微任务队列清空后从宏任务队列取一个任务执行每执行完一个宏任务都会重新清空微任务队列。注意是“每执行完一个宏任务都要清空微任务”不是等所有宏任务执行完再清微任务。常见的宏任务有setTimeout、setInterval、I/O、UI渲染、requestAnimationFrame。常见的微任务有Promise.then、queueMicrotask、MutationObserver。下面这段代码是入门级但高频console.log(script start); setTimeout(() console.log(timeout), 0); Promise.resolve().then(() console.log(promise)); console.log(script end); // script start // script end // promise // timeout即使setTimeout的延迟是0它也是宏任务要等当前同步代码和微任务全部清空后才执行。进阶一点的题目是嵌套微任务的清空行为setTimeout(() console.log(timeout1)); Promise.resolve().then(() { console.log(p1); Promise.resolve().then(() console.log(p2)); }); setTimeout(() console.log(timeout2)); // p1 // p2 // timeout1 // timeout2原因就是微任务队列里新增的微任务会在当前队列被清空之前一并执行所以p2在timeout1之前输出。5.2 Promise、async/await 的实质Promise是一个状态机pending、fulfilled、rejected单向流转且状态一旦确定不再变化。then里返回的Promise会形成链式调用每次返回的值会被包装成新Promise。手写一个Promise曾经是大厂面试题现在虽然不太要求完整实现但至少要知道then的回调是放入微任务队列的。async/await本质上是Generator加Promise的语法糖。一句话记法await右边的表达式不管是不是Promise都会被Promise.resolve包一层await之后的代码相当于放在.then回调里。所以async function test() { console.log(1); await console.log(2); console.log(3); } test(); setTimeout(() console.log(4)); console.log(5); // 1 // 5 // 2 // 3 // 4await console.log(2)会先同步执行console.log(2)输出2然后await的运行结果被包装成Promise后面的console.log(3)作为微任务落到队列里。而test函数是在同步代码中执行到第一个await时挂起把执行权交回给调用方所以5在2之前输出。这段推演能讲明白异步这部分基本稳了。5.3 手写 Promise.all高分模板面试官让你手写Promise.all多半不是考记忆而是看你对边界条件的考虑。我的参考写法function myAll(promises) { return new Promise((resolve, reject) { if (!Array.isArray(promises)) { throw new TypeError(arguments must be an array); } let count 0; const results []; const len promises.length; if (len 0) { resolve([]); return; } promises.forEach((promise, index) { Promise.resolve(promise).then(value { results[index] value; count; if (count len) resolve(results); }, reject); }); }); }这里有几个细节要说出思路一是用Promise.resolve把非Promise值统一包装保证兼容二是results[index] value而不是push保证结果顺序与数组顺序一致三是任何一个失败立即reject因此第二个回调可以直接传reject。被追问到时可以再对比一下Promise.allSettled它等所有任务都出结果才返回不会因为某个失败而中断这在接口并行的兜底场景里更实用。6. 数组与函数高频手写题不仅会用还要会写手写题是前端面试里的“硬通货”尤其是数组API和防抖节流。不是说生产环境真的需要你手写而是这些题能扼要地反馈出你对回调、闭包、this绑定和边界处理的理解。6.1 手写 map、filter、reducemap的核心是“对原数组的每个元素执行回调返回一个新数组”时间复杂度O(n)不会修改原数组。一个参考实现Array.prototype.myMap function(callback, thisArg) { if (typeof callback ! function) { throw new TypeError(callback must be a function); } const arr this; const result []; for (let i 0; i arr.length; i) { if (i in arr) { result[i] callback.call(thisArg, arr[i], i, arr); } } return result; };用i in arr判断是为了兼容稀疏数组面试时能提这个说明你读过规范。filter类似只是改成用if判断回调返回值的真假来决定是否push。reduce则是更底层的工具它通过一个累加器把数组折叠成任意值Array.prototype.myReduce function(callback, initialValue) { const arr this; let acc initialValue; let startIndex 0; if (acc undefined) { acc arr[0]; startIndex 1; } for (let i startIndex; i arr.length; i) { acc callback(acc, arr[i], i, arr); } return acc; };面试很爱考“用reduce实现map或filter”因为能展示你对reduce的掌握程度const myMap (arr, callback) arr.reduce((acc, item, index) { acc.push(callback(item, index, arr)); return acc; }, []);6.2 防抖节流从原理到代码防抖和节流是前端性能优化里使用频率最高的两个工具。防抖的行为是“事件停止触发N秒后才执行”适合搜索输入框、窗口resize节流的行为是“固定时间间隔内最多执行一次”适合滚动加载、按钮重复点击。两者的区别一定要说清楚这是高频追问点。防抖参考实现function debounce(fn, wait 300, immediate false) { let timer null; return function (...args) { const callNow immediate !timer; if (timer) clearTimeout(timer); timer setTimeout(() { timer null; if (!immediate) fn.apply(this, args); }, wait); if (callNow) fn.apply(this, args); }; }immediate参数是为了支持“第一次立即触发”的场景比如按钮点击防抖希望用户点击后立刻有反馈。节流的实现有“时间戳版本”和“定时器版本”时间戳版本保证第一次触发立即执行最后一次触发在下个间隔到来时补执行两者各有取舍。生产环境推荐直接用lodash的_.debounce和_.throttle但手写题要能写出思路。6.3 字符串处理判断包含与忽略大小写热词里有一组“js判断字符串是否包含”和“js 忽略大小写”这虽然是使用场景不是面试题但面试问法会很直接写一个方法判断字符串A是否包含字符串B且要求忽略大小写。最简单可靠的写法function containsIgnoreCase(str, search) { return str.toLowerCase().includes(search.toLowerCase()); }面试官可能追问两个点一是includes与indexOf的区别includes返回布尔值语义更直观且ES6之后支持startsWith、endsWith二是不用toLowerCase能不能做忽略大小写可以用正则new RegExp(search, i).test(str)但要小心search里包含正则特殊字符。如果你想展示对Unicode的了解可以说toLocaleLowerCase在某些语言环境里比toLowerCase更准确。这类小函数虽然简单但把边角讲全了很容易给面试官留下“基础扎实”的印象。另外 “js验证url有效性”也算个常见小问题直接用new URL(str)包裹判断或者在浏览器环境里用a标签探一手function isValidUrl(str) { try { new URL(str); return true; } catch (e) { return false; } }6.4 手写 call、apply、bind手写call/apply的通用思路是把函数作为对象的临时属性通过对象方法调用来改变this指向最后删除临时属性。用Symbol生成唯一键可以避免临时属性名冲突Function.prototype.myCall function(context window, ...args) { if (typeof this ! function) { throw new TypeError(myCall must be called on a function); } const key Symbol(key); context[key] this; const result context[key](...args); delete context[key]; return result; };apply的区别只在于参数是数组context[key](...args)换成context[key](...args)即可或者参数收集方式不同。bind稍复杂因为绑定后的函数还能被new调用且new时this要用新对象而不是绑定的contextFunction.prototype.myBind function(context, ...bindArgs) { const fn this; return function (...callArgs) { return fn.apply(this instanceof fn ? this : context, [...bindArgs, ...callArgs]); }; };this instanceof fn判断的是新函数的this是不是通过new创建出来的实例。面试时能把这个new场景考虑到已经超出大部分候选人的水平了。7. 模块化、前端安全等加分考点最后补几个高频但容易被忽略的考点。它们不一定每个面试都会问但只要问了基本是“一票加分”或“一票扣分”的题。7.1 CommonJS 与 ES Module前端模块化的演进是必知背景主要对比CommonJS和ES Module的区别。CommonJS是Node的模块规范require是运行时加载支持动态路径加载是同步的ES Module是ECMAScript标准import是编译时加载支持静态分析所以打包工具能做Tree-shaking把没用的导出在编译阶段去掉。对比项CommonJSES Module加载时机运行时编译时加载方式同步支持异步导出module.exportsexport default / 命名导出静态分析不支持支持可实现Tree-shakingthis指向指向模块自身的exports指向undefined还有一个高频追问ESM里import声明会被提升到文件顶部所以不管import写在文件哪个位置都会先于其它代码执行模块加载。这个特性可以用来在面试中展示你对ESM语义的理解。7.2 前端安全常考题XSS 与 CSRF前端安全在面试里的出现频率逐年上升热词里也提到“关于前端的一些安全技术操作”。XSS是跨站脚本攻击分反射型、存储型、DOM型核心思路是攻击者把脚本注入到被信任的页面里执行。防御方式主要有对用户输入做转义、对富文本内容做白名单过滤、通过CSP限制脚本来源、敏感Cookie加HttpOnly防止脚本读取。CSRF是跨站请求伪造攻击者诱导用户在不知情的情况下带着身份凭证发起请求。防御方式主要有服务端校验CSRF Token、Cookie设置SameSite属性、请求里加自定义Header头。面试时可以说清楚一点自定义Header的方式之所以有效是因为跨域请求默认无法携带自定义Header这需要服务端配合CORS预检。7.3 面试答题的节奏怎么说才加分最后分享一点个人经验。面试被问到一个知识点时最好的答题结构是“结论先行原理跟上示例收尾”。比如被问到闭包先说结论“闭包是函数和其词法作用域的组合”再讲“它通过引用让外部变量得以保存”最后给一个计数器例子三句话搞定简洁有力。不要一上来就长篇大论面试官没耐心听你背书。被问到不会的题千万别直接说“我不会”。先把能联想到的知识点说出来比如被问 “Object.create” 的实现可以先说“我知道它用于创建一个以指定对象为原型的新对象底层可以理解为新建一个构造函数并把prototype指向该对象再返回实例”这样就给了面试官一个往下接话的台阶。前端面试本来就像一场技术对话把对话延续下去本身就是一种能力。我在面试别人时发现一个规律能把一道题从头到尾讲明白的人通常不是靠背题准备的而是平时就养成了追问“为什么”的习惯。如果你现在正处于准备面试的阶段我的建议是不要把这篇文章当背诵材料而是把每个章节当一面镜子先合上书自己讲一遍讲不清楚的地方再回来看。这个过程比刷十道题更扎实。