1. 从简历到现场——为什么JavaScript基础题总在前端面试的第一关今年的前端行情大家都有体会投简历的人多靠谱的少面试官也学精了。不再一张口就问你用过Vue还是React而是先拿JavaScript最基本的东西试底。原因很简单框架可以速成但语言功底骗不了人。你让他手写一个reduce说没用过问他Promise.all怎么实现说背过但忘了。这两分钟基本就决定这场面试的走向了。这份2023年前端JavaScript高频24问是我结合近半年真实面试场景、牛客网和掘金的面经、以及身边朋友被问到的题目整理出来的覆盖基础语法、函数作用域、原型链、异步编程、ES6 和运行机制六个方向。按照惯例我先把问题清单放出来JS数据类型有哪些、null和undefined有什么区别、和的区别、深拷贝和浅拷贝怎么实现、闭包是什么、this的指向规则、call/apply/bind的区别、箭头函数和普通函数有什么不同、原型链是什么、new操作符做了什么、怎么实现继承、instanceof的原理、事件循环机制是什么、Promise有哪几种状态、async/await的原理、宏任务和微任务怎么区分、怎么实现Promise.all、let/const/var的区别、ES Module和CommonJS的区别、防抖和节流怎么实现、内存泄漏有哪些常见场景、类型转换有哪些坑、怎么判断数组、怎么实现一个可用的深拷贝工具函数。每个问题我都会给出一个能过面试官这关的答法同时站在面试官的角度解释一下他到底在考察什么。有些题你表面上看是考代码实际上考的是你有没有踩过那些坑。2. 基础语法与数据类型——第一关就筛掉一半人2.1 JS数据类型有哪些怎么准确判断凡是一上来就只回答基本类型和引用类型的我基本会再追问一句那typeof null是什么——这题没答好后面的问题我也就不太想继续了。规范答案是JavaScript共有7种基本数据类型undefined、null、boolean、number、string、symbol、bigint和1种引用类型object其中function和array是object的特殊形态。判断类型的方法分三档typeof适合判断基本类型但typeof null返回object这个历史bug必须知道instanceof可以判断引用类型但跨iframe场景下会失效Object.prototype.toString.call()最可靠能精确区分array、date、regexp等。面试官问怎么判断一个变量是不是数组考察的不是你会不会写Array.isArray而是你知不知道为什么要用这个方法以及它和instanceof Array的区别在哪儿。Array.isArray是ES5就有的方法内部走的是Object.prototype.toString的通道不依赖原型链所以跨iframe也不会出错。如果面试官继续追问怎么实现一个万能的类型判断函数我一般给这个版本function getType(value) { const typeStr Object.prototype.toString.call(value); return typeStr.slice(8, -1).toLowerCase(); }这个函数的原理是利用toString方法返回的标签字符串比如[object Array]、[object Null]用slice(8, -1)把中间的Array、Null截出来再转小写就完成了。用这套方式判断null、array、regexp、date都不会出错算是面试里最稳妥的答案。补充一个小技巧如果你要判断某个值是不是NaN直接用Number.isNaN(value)而不是全局的isNaN前者不会先把值转成number再判断避免了isNaN(abc)返回true这种脑壳疼的结果。2.2 null和undefined有什么区别和到底差在哪儿这两个问题我放在一起说因为它们太容易答混了。null表示定义了但没有对象通常由程序员主动赋值undefined表示声明了但没有赋值是引擎给的默认值。你声明一个变量不赋值打印出来就是undefined你把一个变量的值主动设为null是为了告诉别人这个变量我清空了。函数没有return语句默认返回undefined访问对象上不存在的属性返回的也是undefined。这些细节都答清楚说明你不是背概念而是在真的写过代码。和的考点主要在的隐式转换规则。null undefined返回truenull undefined返回false1 1返回true1 1返回false。实际开发中一律推荐使用但面试时也可能会让你说出转换的完整规则。我一般会回答如果两端类型不同会先尝试把一端转成另一端的类型转换顺序大致是boolean先转number然后字符串转number再比较对象类型先调用valueOf如果有toString再走toString。完整规则太琐碎有一个记忆技巧是记住null和undefined在下互相相等且不和任何其他值相等其余情况优先转number即可。注意NaN不等于任何值包括它自己。在面试里问怎么判断一个值是不是NaN你要区分NaN NaN是false所以有了isNaN和Number.isNaN。2.3 深拷贝和浅拷贝怎么实现手写一个深拷贝这道题出现频率极高而且通常会要求现场手写。浅拷贝只复制第一层用展开运算符{...obj}或者Object.assign({}, obj)就能搞定但嵌套对象还是共享引用修改副本会影响原对象。深拷贝要递归复制所有层级最简单的答案是JSON.parse(JSON.stringify(obj))但这个方法有致命缺陷函数、undefined、symbol、循环引用都会被处理成undefined或直接报错。手写深拷贝的正确姿势是先判断基础类型直接返回再判断是不是数组或对象然后递归复制。要处理循环引用得用一个WeakMap记录已经拷贝过的对象。我面试时给出的版本大致是这样的function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) return target; if (map.has(target)) return map.get(target); const result Array.isArray(target) ? [] : {}; map.set(target, result); const keys Reflect.ownKeys(target); for (const key of keys) { result[key] deepClone(target[key], map); } return result; }这段代码用Reflect.ownKeys是因为它能拿到symbol类型的key和不可枚举的key比Object.keys更完整。用WeakMap存引用关系循环引用时第二次遇到同一个对象就能直接返回不会死循环。面试时能写出处理循环引用的版本基本就是高分答案了如果还能提一句函数和symbol可以作为引用直接返回不需要深拷贝那就更完美了。2.4 JavaScript类型转换有哪些坑这题最近问得也很多因为它能看出你对JS怪异行为的理解程度。1 2等于123 - 1等于2a * 1等于NaN[] []等于空字符串[] {}等于[object Object]{} []却等于0——这些看似无聊的题目背后全是规范里的ToPrimitive规则。简单说加号运算符只要有一端是字符串就优先做字符串拼接减号、乘号、除号会尝试把两边转成number。对象在做隐式转换时会先调用valueOf如果返回值不是基本类型再调用toString。面试的时候我不建议你背一堆变态用例更重要的是表达我知道这些坑所以我在项目里都是靠显式转换来规避的。比如从接口拿数据时习惯性用Number()、String()、Boolean()包装一层判断用户输入的数字时不用而用Number.isNaN()显式校验。能把自己规避隐式转换的实践经验讲出来比背十个诡异表达式更有说服力。3. 函数与作用域——闭包和this是面试重灾区3.1 闭包到底是什么有哪些实际应用闭包是我见过被问最多、也最容易被讲虚的一道题。很多人的回答是函数中返回一个函数这就是闭包这个答案只能拿30分。更准确的说法是当一个函数内部引用了外部作用域的变量并且这个函数在定义它的作用域之外被调用时就产生了闭包。JavaScript的引擎不会立刻回收被闭包引用的变量而是把它们保存在所谓的闭包环境中这就是闭包能够记住外部变量的原因。面试官真正想听到的是应用场景。我一般会讲三个第一个是防抖节流函数每次调用都通过闭包保存timer变量第二个是柯里化通过闭包记住之前传入的参数第三个是模拟私有变量比如一个计数器对象只有内部方法能修改它的值外部直接读不到。讲应用场景的时候可以顺带提一下内存泄漏风险闭包长期持有大对象可能导致内存无法回收解决办法是及时把不再使用的引用置为null。这样既有理有据又有实操经验面试官会觉得你确实写过东西。3.2 this的四种指向规则箭头函数有什么区别this指向判断几乎是必考题我总结了一个四步判断法默认绑定非严格模式下this指向window或globalThis严格模式下是undefined、隐式绑定方法作为对象属性调用时this指向调用它的对象、显式绑定用call/apply/bind指定this、new绑定构造函数中this指向新建的实例。优先级从高到低是new绑定 显式绑定 隐式绑定 默认绑定。这里有个混淆点对象方法内的嵌套函数的this指向的是window而不是外层对象这是因为嵌套函数是独立调用不满足隐式绑定规则。想保留外层this要么用箭头函数箭头函数没有自己的this继承定义时外层作用域的this要么在外层先把this存成变量var self this。面试时如果你能把这个嵌套函数的坑讲明白同时说明bind只能绑定一次、多次bind不会生效基本就稳了。3.3 call、apply、bind的区别和手写实现三个方法都是用来改变函数执行时this指向的。call和apply都会立即执行函数区别只是传参方式不同call是逐个传参apply是传数组。bind不立即执行而是返回一个新函数同时把this永久绑定新函数可以多次调用。手写实现也是高频题我给出一个简易版Function.prototype.myCall function(context, ...args) { context context || window; const key Symbol(); context[key] this; const result context[key](...args); delete context[key]; return result; };核心思路是把函数挂到指定的context对象上然后以obj.method()的形式调用这样this就自然指向context了。用Symbol作为key是为了避免覆盖context上原有的属性调用完再删除。手写apply只是参数不同手写bind则要结合call或者apply同时处理好返回新函数的逻辑。能把Symbol加进去这个细节讲出来瞬间就和只会背代码的人拉开差距。3.4 arguments和剩余参数有什么区别这题和箭头函数结合在一起问箭头函数没有自己的arguments它里面的arguments指向外层函数的arguments。ES6的剩余参数...args是用来替代arguments的更现代方案。arguments是类数组对象有length但没数组方法需要先转成数组才能用map、filter剩余参数本身就是真数组直接就能调用数组方法。面试时我还会补充一个高级用法剩余参数可以结合解构使用比如const [first, ...rest] arr拿掉第一个元素后剩下的放进rest数组。这个写法在处理函数参数时非常常用。如果你发现面试官在追问arguments怎么转数组三个方案都要说出来Array.from(arguments)、[...arguments]、Array.prototype.slice.call(arguments)。4. 原型链与面向对象——new和继承必须手写4.1 原型链是什么instanceof的原理原型链这个概念是JavaScript面向对象的核心。每个函数都有一个prototype属性指向原型对象每个对象实例都有一个__proto__属性指向创建它的构造函数的prototype。当访问一个对象上不存在的属性时引擎会沿着__proto__链向上找一直找到Object.prototype的__proto__也就是null为止。这条链就是原型链。instanceof的原理就是判断构造函数的prototype是否出现在实例的原型链上。手写一个myInstanceof可以这样function myInstanceof(instance, Constructor) { let proto Object.getPrototypeOf(instance); const prototype Constructor.prototype; while (proto ! null) { if (proto prototype) return true; proto Object.getPrototypeOf(proto); } return false; }面试官一般会接着问instanceof能判断基本类型吗答案是不能因为基本类型没有原型链。但Symbol.hasInstance这个静态属性可以改写instanceof行为比如用Object.create(null)创建的对象没有原型链instanceof就失效了。还有一个小坑1 instanceof Number是false但new Number(1) instanceof Number是true这一对正好用来考你包装对象和字面量的区别。4.2 new操作符到底做了什么怎么手写new操作符的步骤背下来不难难的是把它和原型链、构造函数联系起来。new一共做四件事创建一个新对象、把新对象的__proto__指向构造函数的prototype、把构造函数里的this绑定到新对象上并执行、如果构造函数显式返回了引用类型对象就返回那个对象否则返回新对象。手写版本function myNew(Constructor, ...args) { const obj Object.create(Constructor.prototype); const result Constructor.apply(obj, args); return (result typeof result object) ? result : obj; }这段代码用Object.create(Constructor.prototype)一步完成了创建对象和设置原型两件事。注意最后一行要判断构造函数返回的是不是引用类型如果是基础类型就直接忽略返回新对象。如果构造函数里写了return {name: xxx}那new出来的实例就是这个返回的对象。这个细节很多人会漏面试官一提就露馅。4.3 实现继承的几种方式和优缺点比较继承考的是你对JavaScript对象机制的理解深度。古典答法是原型链继承子类的prototype指向父类的实例、构造函数继承子类构造函数里调用Parent.call(this)、组合继承两者结合、寄生组合继承。现在ES6的extends关键字已经把这些复杂写法收编了但面试官还是会问底层原理。寄生组合继承是目前最优解核心思路是子类构造函数里用Parent.call(this)继承实例属性子类原型用Object.create(Parent.prototype)继承方法同时修正构造函数的指向function Child(...args) { Parent.call(this, ...args); } Child.prototype Object.create(Parent.prototype); Child.prototype.constructor Child;一定要记得修正constructor指向否则子类的实例会被误判为父类创建的。面试时你把这个版本的每一步为什么这么写讲清楚比背十个继承版本都管用。4.4 ES6 class和构造函数有什么区别class本质是什么class本质上是构造函数的语法糖typeof class Foo {}返回的是function这一点很多人都不知道。但语法糖不代表它们完全等价class和构造函数有几个关键区别class声明不会提升有暂时的死区class内的所有方法都是不可枚举的class必须用new调用直接调用会报错class内部默认是严格模式。class的方法定义、静态方法、getter/setter、私有字段ES2022的#前缀整套语法写起来干净很多。面试官如果接着问class能不能模拟private方法我会说三个方案用#私有字段ES2022标准、用闭包管理状态、用WeakMap存私有数据。每个方案的适用场景不同但都能体现你对封装概念的理解。5. 异步编程——事件循环和Promise是必考大题5.1 事件循环机制宏任务和微任务怎么区分这题几乎每场面试都会出现而且经常是压轴题。要讲清楚事件循环记住一个核心JavaScript是单线程的但浏览器的执行环境是多线程的。JS主线程执行完当前同步代码后会先清空微任务队列然后从宏任务队列取一个任务执行执行完再清空微任务队列如此反复。这个宏任务结束后清空微任务的顺序是整个事件循环最关键的细节。宏任务和微任务怎么区分宏任务包括setTimeout、setInterval、I/O、UI渲染、requestAnimationFrame有些场景算宏任务微任务包括Promise.then、queueMicrotask、MutationObserver、process.nextTickNode环境。遇到输出顺序题先用同步代码先执行微任务先于宏任务每个宏任务执行完都再清一次微任务这个口诀推一遍。常见的setTimeout里面再包一个Promise这类嵌套题只要把这个循环逻辑理顺就不会错。5.2 Promise的三种状态then和catch怎么串联Promise的三种状态是pending进行中、fulfilled已成功、rejected已失败状态只能从pending变为fulfilled或rejected一旦改变就不可逆。then和catch都返回新的Promise所以可以链式调用。catch本质上是.then(null, onRejected)的语法糖。这里有个常见考点在then的第二个参数里处理rejection和在catch里处理有什么不同第二个参数只能捕获当前Promise的错误catch能捕获前面所有then里新抛出的错误。从这个角度讲catch是更安全的写法。面试还会考Promise的几个静态方法Promise.all在任一失败时整体失败但会立即返回第一个错误Promise.allSettled等所有任务结束返回每个任务的状态和结果Promise.race只返回第一个完成的结果不管成功还是失败。有些公司会要求手写Promise.all我后面单独讲。5.3 async/await的原理串行和并行的区别async/await是Promise的语法糖async函数返回的永远是一个Promiseawait后面的表达式会被转成Promise.resolve()的形式等待。很多人不知道的是await关键字其实可以出现多次每次await都会中断当前async函数的执行把它变成微任务挂到队列里。这个中断-恢复的机制是理解async/await性能的关键。面试时易踩坑的是await到底是串行还是并行多个await依次写就是串行第二个等待第一个完成如果几个请求之间没有依赖关系应该用Promise.all直接并行执行。某些人写的业务代码几百行里全是await await await接口一个等一个比串行瀑布还慢这就是没理解await的本质。我会现场举一个商品详情页的例子需要同时请求用户信息和商品信息这两个接口没有依赖正确的做法是const [user, product] await Promise.all([getUser(), getProduct()])而不是先后await两次。5.4 手写Promise.all手写Promise.all是一道高频题考的是对Promise静态方法原理的掌握。核心逻辑遍历传入的Promise数组每个Promise的then回调里把结果存到对应索引的位置用计数器判断是否全部完成然后resolve结果数组。只要有一个reject整体就rejectfunction myPromiseAll(promises) { return new Promise((resolve, reject) { const result []; let count 0; if (promises.length 0) resolve(result); promises.forEach((promise, index) { Promise.resolve(promise) .then(value { result[index] value; count; if (count promises.length) resolve(result); }) .catch(reject); }); }); }有两个细节一定要说清楚一是用index存结果而不是用push因为异步任务完成的顺序不一定是数组顺序用push会把顺序搞乱二是对传入的非Promise值要先包一层Promise.resolve()不然基础类型值没有then方法会直接报错。6. ES6与运行机制——新特性加内存排查6.1 let、const、var的区别为什么const声明的对象还可以修改这道题几乎是送分题但能全说利索的人真不多。var有变量提升、没有块级作用域、可以被重复声明let和const有块级作用域、有暂时性死区、不能重复声明。const在声明时必须赋值且基本类型的值不能修改但对象和数组可以修改这是因为const锁定的只是变量的绑定关系指针对象内部的属性不受影响。如果想冻结对象要用Object.freeze但它只冻结一层深层对象需要递归处理。面试官还常追问暂时性死区是什么在块级作用域内let和const声明的变量在声明语句之前是不可以访问的这个区间就是暂时性死区。有意思的是typeof这个操作符在TDZ内也会抛ReferenceError这一点和typeof未声明的变量返回undefined是完全不同的行为。6.2 ES Module和CommonJS有什么区别循环依赖怎么处理这个问题考察的是你对模块化演进的理解深度。CommonJS是Node.js的模块规范是同步加载模块导出的是值的拷贝运行时才能确定模块结构ES Module是浏览器原生支持的模块规范是异步加载导出的是值的引用live binding静态分析阶段就能确定依赖关系。经典面试题两个模块循环引用会发生什么通常会伴随一个示例代码让你推结果。处理循环依赖的策略CommonJS用的是缓存加先返回已加载部分的导出对象ES Module用实时绑定谁先被加载就先解析谁依赖等真正用到时再从模块里取。回答的关键是循环依赖本身不是问题问题是你是否在模块顶层立即使用了对方模块的值如果是就会拿到undefined或者不完整对象。工程上的规避方案是尽量往更细的粒度拆模块避免顶层加载时产生副作用。能把这个逻辑讲清楚面试官会认可你的架构意识。6.3 防抖和节流怎么实现适用场景是什么这题我基本都会让候选人现场手写。防抖debounce是在事件触发后等待一定时间再执行如果过程中又触发了就重新计时节流throttle是固定时间间隔内只执行一次。防抖适合输入框搜索、窗口resize结束后的计算节流适合滚动事件、按钮点击频率控流。手写要点有两个一是要保留this指向回调函数里的this应该指向触发事件的元素二是要有首尾执行的控制。带立即执行功能的防抖写法比较加分function debounce(fn, delay, immediate false) { let timer null; return function(...args) { const callNow immediate !timer; clearTimeout(timer); timer setTimeout(() { timer null; if (!immediate) fn.apply(this, args); }, delay); if (callNow) fn.apply(this, args); }; }这段代码里callNow的作用是判断是否立即执行一次timer置null是为了在结束后允许下一次立即执行。面试官如果有追问防抖和节流的区别能不能用生活场景比喻我会说防抖像电梯门有人进就重新计时关门时间节流像地铁发车不管多少人到站固定时间就开走一班。6.4 内存泄漏有哪些常见场景怎么排查内存泄漏这几年问得越来越多因为大家开始关注长页面和移动端的性能问题。常见场景包括全局变量挂载东西未用var/let/const声明的变量会变成全局变量、被遗忘的定时器和回调函数、脱离DOM的引用已经是null的对象还存着它的引用、闭包长期持有大对象、事件监听器未解绑。排查方法要讲得具体一些打开Chrome DevTools的Memory面板录制heap snapshot对比前后快照找出持续增长的对象或者用Performance面板录制一段时间观察JS heap的变化曲线。如果曲线只升不降基本就是有泄漏。实战中我遇到过这样一个案例一个列表页轮询接口定时器每分钟拉取一次数据但页面卸载时没有clearInterval导致组件被不断重新挂载时创建了新的定时器旧定时器又没清最后页面每秒钟会发几十个请求。这种问题光靠写代码是防不住漏的需要形成凡是setInterval必须有对应的clearInterval的肌肉记忆。7. 最后再分享一点面试心态层面的经验以上24个问题全部过一遍其实只完成了50%的功课。我在面试别人的时候发现真正拉开差距的从来不是答案背得有多熟而是你在回答过程中展现出来的被问题引导的能力。比如面试官问闭包你不仅能说出定义还能主动讲到内存泄漏的规避问Promise你主动提到allSettled的应用场景问原型链你延伸到class的语法糖本质。这种主动延伸而不是被动挤答案的状态面试官是能感受到的。剩下50%是大量真项目经验的积累——手写代码练完之后把手上的项目仔仔细细过一遍想一想每个API调用背后的原理比死刷一百道题更有用。祝各位准备面试的朋友都能拿到心仪的offer。