news 2026/9/18 4:45:36

JavaScript高频面试题解析:从类型转换到事件循环与手写实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript高频面试题解析:从类型转换到事件循环与手写实现

最近两周陆续帮几个朋友做了模拟面试,有个现象挺扎心的:简历上写着“熟练掌握 JavaScript”的候选人,基础题答起来反而最容易翻车。问事件循环,能背出宏任务微任务的定义,换一道带 async/await 的输出排序题就乱;问闭包,嘴上说着“函数里返回函数”,再追问内存回收和真实应用就沉默。我自己当年面试也有过这种尴尬,所以干脆把平时积累和实践验证过的高频小题整理成一个系列,这是第二期。

这期覆盖数据类型、闭包、this 指向、事件循环、深拷贝和手写题六个方向。每道题我会拆成四个层面:题目本身、出题意图、标准答法、面试官可能的追问。适合正在准备前端面试的人,也适合工作几年后想自查基本功的开发者。标题里写着“八股”,但看完你会发现,这些题其实全都能在真实开发里找到对应场景。

1. 这期选题逻辑:高频小题不是背诵题,是思维题

先解释一下为什么偏偏选这六个方向。JavaScript 的面试题五花八门,但你把招聘 JD 翻一遍会发现,基础题翻来覆去就那几个核心:类型系统、闭包、this、异步、拷贝、手写 API。这不是面试官偷懒,是因为这几块恰好覆盖了语言最容易被误解、也最容易在日常开发中踩坑的区域。

面试题方向主要考察能力真实开发中的对应场景
数据类型与转换对语言底层表示的理解接口返回数据处理、后端字段类型异常兜底
闭包与作用域对函数式编程模型的理解防抖节流、React Hooks 闭包陷阱、模块封装
this 绑定对调用方式与上下文关系的敏感度事件回调、定时器、Vue/React 方法绑定
事件循环对异步模型和运行机制的整体把握接口竞态、加载顺序、渲染性能优化
深浅拷贝对引用类型本质的理解表单数据提交、状态管理、不可变数据更新
手写 API对语言内置能力的掌握 + 编码规范工程中抽取公共工具函数、降级方案

我在模拟面试时发现一个规律:能把八股题答出层次的人,往往不是背得最熟的人,而是真的自己写过、调试过的人。比如“闭包是什么”这种题,如果候选人能主动从闭包讲到内存回收,再讲到 Vue 的 watch 或 React 的 useEffect 依赖数组,面试官就算后面不追问,心里也已经给了高分。所以我整理这些题目时,特意把“跟开发实践的关联”放在重要位置,你背的时候也不是为了背,是为了把这些知识点在脑子里跟真实场景挂上钩。

另外说句实在话,面试阶段问八股,本质是在筛选“有没有建立过自己的知识体系”。没有体系的人,答完一个点就停住了;有体系的人,会自动往上下游延伸。后面每道题的“追问方向”,就是在帮你模拟这种延伸。

2. 数据类型与类型转换:一道题能问出十种答法

开头先来最基本的:JavaScript 的数据类型分两大类。原始类型包括 string、number、boolean、undefined、null、symbol、bigint;引用类型统称 object,往下细分有普通对象、数组、函数、Date、RegExp、Map、Set 等。面试官问第一题往往就是“JavaScript 有哪些数据类型”或“typeof 能返回哪些值”。

2.1 typeof 和 instanceof:基础问题里的暗坑

typeof 看起来简单,真往深了问有不少坑。它能返回的值有:'undefined''boolean''string''number''object''function''symbol''bigint'。注意,没有'null'这一项,typeof null === 'object'这是语言设计早期的遗留 bug,现在不能修是因为修了会导致大量已上线的代码行为变化。

typeof undefined // 'undefined' typeof true // 'boolean' typeof 'hello' // 'string' typeof 42 // 'number' typeof null // 'object' <- 历史遗留 typeof {} // 'object' typeof [] // 'object' <- 数组也是 object typeof function(){} // 'function' typeof Symbol() // 'symbol' typeof 10n // 'bigint'

这个表是面试官很爱深挖的点。如果只背到这一步,顶多算及格。真正拉开差距的是后面这一串追问:

  • 怎么准确判断一个变量是不是数组?答案是Array.isArray(arr),或者Object.prototype.toString.call(arr) === '[object Array]'
  • 为什么不用arr instanceof Array?因为 instanceof 的原理是沿着原型链找构造函数的 prototype,一旦涉及 iframe 或多 realm 环境,跨全局对象的数组判断会失败,而Array.isArray是内置的、跨 realm 也可靠。
  • Object.prototype.toString.call()能返回什么?几乎能区分所有内置类型:'[object Object]''[object Array]''[object Date]''[object RegExp]''[object Map]''[object Set]'等等,这是判断类型的最稳方案。

面试官问这块,本意是看你会不会在真实开发里优雅地处理“类型不靠谱”的问题。比如后端某天把userId从 number 改成了 string,你代码里到处写===比较,线上立刻出问题,但如果你在数据入口做了类型归一化,就没这事了。

2.2 == 的隐式转换:一套规则走天下

再看=====的区别,以及隐式类型转换的规则。很多人知道“==会做类型转换,===不会”,但被问到具体怎么转,就答不上来了。核心规则其实就几条:

  1. 如果两边类型相同,=====行为一致。
  2. 如果一边是 number、一边是 string,string 会转换成 number。
  3. 如果一边是 boolean,boolean 会先转换成 number,也就是true1false0
  4. 如果一边是对象、一边是原始类型,对象会先转原始值,通常先执行valueOf(),没得到原始值再执行toString()
  5. null == undefined结果为true,但它俩和其他任何值==都为false
1 == '1' // true,'1' 转成 1 0 == false // true,false 转成 0 '' == false // true,'' 转成 0,false 转成 0 null == undefined // true null == 0 // false undefined == '' // false [] == false // true

最后那个[] == false是经典中的经典,推导过程是:false先转成0,然后[]转原始值,[].valueOf()返回[],不是原始值,继续调[].toString()得到''''再转成0,所以0 == 0,结果为true。这个推导过程本身就是一个很好的面试加分项,因为面试官能看到你掌握的是“规则推演”,而不是“背结论”。

实际开发中我的建议是:新代码一律用===,不要依赖==的隐式转换。但你要能看懂老代码里的==,因为线上可能有一堆历史代码还在这么写。隐式转换是语言特性,不是洪水猛兽,理解它反而能帮你快速排查怪问题。

2.3 0.1 + 0.2:必考但很多人讲不清

0.1 + 0.2 !== 0.3这题几乎场场必问。答案是浮点数精度问题:JavaScript 里的 number 采用 IEEE 754 双精度标准,即用 64 位二进制来存储数字。十进制小数的二进制表示可能是无限循环的,比如0.1在二进制里就是无限循环小数,计算机只能存储一个近似值。两个近似值相加,结果自然也是近似值,于是出现0.30000000000000004

这个问题的汇报口径有三个层次:

  • 第一层:说出原因是浮点数的二进制近似存储,这是绝大多数人能答到的。
  • 第二层:给出解决方案,比如parseFloat((0.1 + 0.2).toFixed(10)),或者先乘 100 变成整数再除回来。
  • 第三层:提到Number.EPSILON,判断两个小数是否相等时用Math.abs(a - b) < Number.EPSILON来比较。

面试官追到第三层,基本就满意了。这题在开发里对应的是金额计算、百分比展示、图表坐标轴刻度这类场景。我的经验是,涉及金额千万别用浮点数直接算,要么用整数分单位,要么上专门的库;普通场景里用toFixed做一个展示位的裁剪就已经足够。

3. 闭包:背会“函数里 return 函数”只算入门

闭包这个大题,几乎所有前端面试都会出现。定义层面,闭包就是“函数能够访问其外部作用域中变量的能力”,更严谨点说,当一个函数引用了外部变量,并且这个函数在外部变量所在的作用域之外被执行,就构成了闭包。

3.1 闭包形成的底层条件

面试官会问“为什么函数已经返回了,变量还没被回收”。这得从执行上下文和词法作用域讲起。JavaScript 的函数在创建时,会保存一份定义位置的词法环境引用,这个引用跟着函数走。当外层函数执行完,按常规逻辑它的局部变量应该被销毁,但因为内层函数还持有这个环境的引用,垃圾回收器发现这个环境仍然可达,就不会回收它。

所以闭包形成的两个条件缺一不可:一是有函数嵌套,二是内层函数引用了外层函数的变量。只嵌套不引用,不产生闭包;只引用不嵌套,本来就访问不到外部变量,也不存在闭包。

一个最小例子:

function outer() { let count = 0; return function inner() { count += 1; return count; }; } const counter = outer(); counter(); // 1 counter(); // 2

这里count就长期驻留在内存中,counter每次调用都能访问到并修改它。这种“持久化局部变量”的能力,在真实开发里非常有用,但要注意它同时意味着变量不会被自动回收,如果滥用就会造成内存泄漏。

3.2 真实开发里闭包在哪里出现

背闭包定义只算第一步,面试官更愿意听你讲实际场景。闭包在工程里最常见的几个出现位置:

  • 防抖和节流:用闭包持久化 timer 或 lastTime 变量,不污染全局。这是手写题和项目经验都能聊到的交叉点。
  • 函数柯里化add(1)(2)(3)这种连续调用,每一步都通过闭包记住之前的参数。
  • 模块封装:通过 IIFE 把变量藏在函数内部,只暴露有限的接口,实现类似私有变量的效果。
  • React Hooks 的闭包陷阱useEffect回调里读到旧的 state,本质上就是因为回调函数闭包了旧渲染周期的变量。

聊 React Hook 闭包陷阱这个点,很容易让面试官眼前一亮。比如经典的 Counter 在useEffect里依赖count却只跑一次导致count永远是初始值的问题,这背后就是闭包捕获了旧值。能够从八股跳到框架源码行为,是区分“背题”和“理解”的分水岭。

3.3 循环输出题的典型解法

再来看那道经典的循环题:

for (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 100); }

直接结果是输出 5 个 5。原因是var声明的i是函数级作用域,5 个 setTimeout 回调共享同一个i;等到 100ms 后回调执行时,循环早已结束,i停在了 5。解法有三种:

  • let声明i,每次迭代生成一个新的绑定,这是最简洁的解法,也是 ES6 之后的推荐写法。
  • 用 IIFE 包一层,把每轮的i作为参数传入,形成一个独立闭包。
  • 把打印逻辑抽到单独函数里,接收参数,本质上和 IIFE 一样。
// 解法一:let for (let i = 0; i < 5; i++) { setTimeout(() => console.log(i), 100); } // 解法二:IIFE for (var i = 0; i < 5; i++) { (function (j) { setTimeout(() => console.log(j), 100); })(i); }

面试官追问“解法一的原理是什么”,要能答到:let 在 for 循环的每个迭代里创建了独立的词法环境,回调闭包的是当次迭代的绑定,所以拿到的分别是 0 到 4。这题背后其实就是对“块级作用域 + 闭包捕获时机”两个概念的综合检验。

4. this 指向:四类绑定规则,一题定胜负

this 是 JavaScript 里被误解最深的机制之一。面试问 this,最终考的是你有没有真正理解“this 的值在调用时决定,而不是在定义时决定”这件事。

4.1 四种绑定规则与优先级

  • 默认绑定:函数直接调用,非严格模式下 this 指向全局对象(浏览器里是 window),严格模式下是 undefined。
  • 隐式绑定:通过对象方法调用,this 指向该对象,即obj.fn()里 this 指向 obj。
  • 显式绑定:通过callapplybind手动指定 this。
  • new 绑定:通过new调用时,this 指向新创建的对象。

优先级从低到高是:默认绑定 < 隐式绑定 < 显式绑定 < new 绑定。其中bind绑定过的函数再用call不会覆盖 this,因为 bind 的绑定优先级更高;new 能覆盖 bind 绑定的 this,不过实践里很少这么写。

// 默认绑定 function fn() { console.log(this); } fn(); // window(非严格模式) // 隐式绑定 const obj = { name: 'obj', fn }; obj.fn(); // obj // 显式绑定 fn.call(obj); // obj // 隐式绑定丢失 const f = obj.fn; f(); // window,因为 f 调用时没有对象上下文

4.2 箭头函数如何绕过 this

箭头函数没有自己的 this,它的 this 是定义时外层词法作用域的 this,而且callapplybind都无法改变它。这句话背起来容易,用起来要小心,因为如果定义箭头函数时外层 this 本来就飘了,那箭头函数也跟着飘。

const obj = { name: 'obj', fn: function () { setTimeout(() => { console.log(this.name); }, 100); } }; obj.fn(); // 'obj'

如果 setTimeout 里换成普通函数,this.name就是 undefined 了。这就是为什么很多代码里会在定时器、异步回调内部用箭头函数“锁住”外层的 this。面试官问这个,其实是想看你在实际封装组件或 SDK 时有没有踩过 this 丢失的坑。

4.3 高频场景题与常见丢 this 坑

场景一:定时器里调用对象方法。

const user = { name: 'Alice', greet() { console.log(this.name); } }; setTimeout(user.greet, 100); // undefined setTimeout(() => user.greet(), 100); // 'Alice' setTimeout(user.greet.bind(user), 100); // 'Alice'

第一种写法把函数引用传给 setTimeout,执行时已经脱离对象,变成了默认绑定。第二种用箭头函数包一层,保留对象调用方式。第三种直接 bind 绑定。

场景二的追问点是callapplybind三者的区别:callapply都是立即执行,区别在于传参方式,call用逗号分隔参数,apply用数组;bind返回一个新函数,不立即执行,且支持预置参数。能把三者在底层实现上的关系讲清楚,比如手写一个简易 bind 时用到 apply,就是加分项。

另外,解构赋值和回调传参会丢 this,比如const { greet } = user; greet();这种,本质和前面const f = obj.fn一样,都是隐式绑定丢失。

5. 事件循环:宏任务与微任务的顺序题

事件循环这题,是 JavaScript 面试的“压轴常客”。原因很简单:前端整天跟异步打交道,请求、定时器、渲染、事件回调全都依赖这套机制,不懂事件循环,监控上报的时机、加载顺序、加载性能就都说不清楚。

5.1 队列是怎么排的

先建立一个整体认知:JavaScript 是单线程的,一次只能执行一段代码。V8 引擎执行完同步代码后,会把异步回调按照类型放进不同的队列。宏任务队列包括 script 整体代码、setTimeout、setInterval、I/O 操作、UI 渲染;微任务队列包括 Promise 的 then/catch/finally、queueMicrotask、MutationObserver。Node 环境里还有 process.nextTick,它比 Promise 的微任务优先级更高。

执行规则简单归纳就三句话:

  1. 先执行当前宏任务(第一次就是整段 script)里的同步代码。
  2. 同步代码执行过程中产生的微任务,放到微任务队列。
  3. 当前宏任务执行完毕,一次性清空微任务队列;清空过程中如果又产生了新的微任务,继续执行,直到队列为空;然后再取出下一个宏任务。

这也是为什么 setTimeout 的 0 毫秒不是真正的“立即执行”,它至少要等当前执行栈和微任务队列全部清空。

5.2 两道经典输出题的完整推导

第一道基础题:

console.log(1); setTimeout(() => { console.log(2); }, 0); Promise.resolve().then(() => { console.log(3); }); console.log(4);

推导过程:同步先执行 1 和 4,产生一个宏任务(setTimeout)和一个微任务(Promise.then);同步执行完毕,微任务队列先输出 3;之后宏任务输出 2。最终顺序是1, 4, 3, 2

第二道进阶题,带 async/await:

async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 0); async1(); new Promise((resolve) => { console.log('promise1'); resolve(); }).then(() => { console.log('promise2'); }); console.log('script end');

我来一步步推:

  • 执行 async1 函数体,同步打印async1 start
  • 执行await async2(),async2 函数体同步执行,打印async2,然后返回值给 await。await 后面的代码相当于.then,也就是“暂停”,把async1 end挂到微任务队列。
  • 继续往下走,new Promise 里的执行器是同步的,打印promise1resolve()后它的 then 回调进入微任务队列。
  • 继续走同步代码,打印script end
  • 同步代码结束,清空微任务队列:先有async1 end,后有promise2,按进入顺序输出。
  • 最后执行宏任务 setTimeout,打印setTimeout

最终输出是:

script start async1 start async2 promise1 script end async1 end promise2 setTimeout

这里有个细节要注意:await 右侧的函数会同步执行完,只有 await 之后的代码才是异步的。很多人在async2这里出错,以为整个 async2 都是异步的,实际 async2 的函数体是同步打印的。

5.3 async/await 在队列中的真实位置

追问的深水区来了:await到底把“后续代码”放进了什么队列?标准解释是,await会隐式地把后续代码包成一个 Promise 回调,放入微任务队列。但是 V8 后续版本做了优化,如果 await 的右侧本身就是一个 Promise,会减少一次微任务的包装层数。面试时你不需要给出过于精细的分版本行为,掌握“await 后面的代码会被推迟到微任务阶段执行”这个大原则就够了。

为什么前端工程师必须吃透这件事?因为实际开发里有大量场景依赖它:接口请求完成后要更新状态、多个请求之间的竞态控制、首屏渲染时尽早执行关键脚本、错误上报不能阻塞主流程。凡是涉及“我这段代码到底什么时候执行”的问题,最终都要回到事件循环来解释。

6. 深拷贝:从 JSON.stringify 到递归实现

深拷贝在 JavaScript 面试里的出场率极高,因为它是“讲原理 + 手写代码 + 边界处理”三位一体的好题目。

6.1 JSON 方案:写起来爽,坑也最多

最省事的深拷贝写法是JSON.parse(JSON.stringify(obj))。一行代码完成拷贝,但坑非常多:

  • 对象的函数、undefinedSymbol类型的属性会被直接跳过,结果里丢失这些字段。
  • Date会被转成 ISO 字符串,而不是 Date 对象。
  • RegExp会变成空对象。
  • NaNInfinity会被转成null
  • 循环引用会直接抛错:TypeError: Converting circular structure to JSON
  • 原型链上的属性和不可枚举属性不会被拷贝。

如果只是拷贝一个纯数据的配置对象,JSON 方案足够;但一旦数据结构里混了函数或者出现循环引用,就得换方案。我建议把“JSON 深拷贝的缺陷”作为一道独立的常识题来记,因为它直接反映你对引用类型的理解深度。

6.2 手写递归深拷贝的边界清单

手写实现要兼顾的边界很多,从简单到完整,一般分三档。

第一档,能递归拷贝普通对象和数组:

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; }

第二档,处理循环引用,用 WeakMap 记录已拷贝的源对象,遇到重复引用时直接返回缓存:

function deepClone(target, map = new WeakMap()) { if (typeof target !== 'object' || target === null) { 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); } const clone = Array.isArray(target) ? [] : {}; map.set(target, clone); for (const key of Object.keys(target)) { clone[key] = deepClone(target[key], map); } return clone; }

第三档,用Reflect.ownKeys替代Object.keys,把 Symbol 属性也考虑进去,再处理 Map、Set 等类型。一般面试答到第二档已经超过大部分候选人了,第三档可以在追问时展示深度。WeakMap 的选型也值得说一句:用它做缓存不会阻止垃圾回收,所以比普通 Map 更适合这种场景。

6.3 和现代 API structuredClone 的对比

structuredClone是浏览器后来提供的原生深拷贝 API,支持循环引用、Date、RegExp、Map、Set、TypedArray 等类型,拷贝行为比 JSON 方案健壮得多。但它有两个限制:一是函数仍然是不可克隆的,会抛DataCloneError;二是兼容性要求相对较新的浏览器环境。

面试时提到它,能体现“我在跟进新特性而不是只会背旧方案”。不过要让面试官知道,手写递归方案的价值在于理解原理,工程上能用原生 API 就用原生 API,就像Array.isArray和手写类型判断的关系一样。

7. 手写题冲刺:防抖、节流与 Promise.all

手写题是八股面试里最能拉开分差的部分,因为背不背得出来,一写便知。面试官并不指望你写出完美无缺的库级源码,他要看的是:代码结构是否清晰、有没有处理 this 绑定、会不会处理返回值、步进式思考是否严密。

7.1 防抖节流:从代码到场景一次讲透

先理清概念。防抖 debounce:事件触发后延迟执行,如果延迟期间再次触发,就重新计时。核心用途是“只看最后一次动作”,比如输入框搜索、窗口 resize 结束后再计算。节流 throttle:固定时间间隔内只执行一次,即使触发频率再高也严格限频。核心用途是“控制执行频率”,比如滚动事件中加载更多、拖动时更新位置。

防抖实现:

function debounce(fn, delay = 300) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }

节流实现(时间戳版本):

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); } }; }

两个点要跟面试官讲明白:第一,返回函数里必须用fn.apply(this, args),这样外面对象调用包装函数时 this 能正确传递;第二,防抖和节流都是靠闭包在“记住”上次的状态,一个记 timer,一个记 last 时间戳。把这两点和前面闭包章节呼应起来,整个答案的连贯性会非常强。

进阶追问“防抖和节流能否结合”,比如输入框既需要延迟防抖又需要保证一定时间内必然执行一次,可以手写一个“防抖 + 节流”的混合版本,用 timer 做防抖、用 last 做节流,双重判断。这种题没有标准答案,但考察的就是你灵活运用基础能力解决实际问题的水平。

7.2 Promise.all 的实现与常见坑

Promise.all 也是高频手写题。它的特性是:接收一个可迭代对象,返回一个新的 Promise;所有输入 Promise 都成功时,返回按原始顺序排列的结果数组;任何一个失败,直接走 reject,而且是“快速失败”。

function promiseAll(promises) { return new Promise((resolve, reject) => { if (!Array.isArray(promises)) { throw new TypeError('promises must be an array'); } const results = []; let count = 0; if (promises.length === 0) { resolve([]); return; } promises.forEach((item, index) => { Promise.resolve(item).then((value) => { results[index] = value; count += 1; if (count === promises.length) { resolve(results); } }).catch(reject); }); }); }

写这个题有几个容易忽略的细节:

  • 必须用Promise.resolve(item)包一层,因为输入可能是普通值,不一定是 Promise。
  • 结果用results[index] = value而非push,保证输出顺序与输入顺序一致,避免某个 Promise 先完成导致顺序错乱。
  • count计数判断是否全部完成,不能用results.length,因为数组中间可能存在空槽,length 不可靠。
  • 空数组直接resolve([]),这是一个需要显式处理的边界。

追问还可能包括Promise.allSettledPromise.race的区别。allSettled等所有请求结束并返回每个结果的状态,适合批量操作里不能因为一个失败就整体的场景;race是竞速,取第一个完成的结果。这三个 API 背熟后,手写题和项目场景题都能接住。

7.3 附带一道数组扁平化的送分题

有时面试官会顺手加一道数组扁平化:

function flatten(arr, depth = Infinity) { return arr.flat(depth); }

这是 API 直接调用版。手写递归版:

function flatten(arr, depth = 1) { return arr.reduce((acc, item) => { return acc.concat( Array.isArray(item) && depth > 0 ? flatten(item, depth - 1) : item ); }, []); }

手写版考察的是 reduce、递归和 depth 控制。工程上直接用flat就好,但知道底层实现能让你在遇到“别用 flat、实现一个扁平化”这种限制题时稳住。

8. 除了答案,面试官还想听到什么

最后这部分不是八股,是我自己面别人和被人面之后的复盘。很多候选人知识点都会,但分不高,原因往往不在技术上,而在表达方式上。

8.1 怎么答才不像“背八股”

第一,用“先结论、再展开、后举例”的结构。面试官问“闭包是什么”,不要上来就背定义,可以这样说:“闭包是函数捕获外部变量的机制,核心是函数定义时的词法环境会被保留。我举个实际例子,写防抖函数时我们就是通过闭包保存 timer 的。”三句话覆盖定义、原理、场景,这比背诵官方式定义质感好得多。

第二,答完一个点主动往外延展。讲完 it 循环经典题,可以主动说“这道题其实考察的是 var 和 let 的作用域差异,跟实际开发里用 let 替代 var 的规范也是一致的”。这种关联能力比多背十个知识点更有用。

第三,允许自己说“我不太确定”,但要补一句“我准备的验证方式是这样的”。面试官反感的是不懂装懂,不反感诚实加思路。比如不确定数组扁平化的边界条件,可以说“我记得 flat 还有 depth 参数可以控制深度,具体默认值我需要查一下文档,但递归实现的核心逻辑我清楚”。这种回答会让人觉得工程素养在线。

8.2 我复盘时发现的三个高性价比知识点

第一个是高阶类型的判断组合拳:typeofinstanceofObject.prototype.toString三者的适用边界。这题能串起类型系统一整个专题,性价比极高,强烈建议优先复习。

第二个是 this 和闭包的联动题。实际面试里它们经常被放在同一道题里考察,比如“这个 setTimeout 回调为什么访问不到 this.xxx”和“闭包里怎么缓存 this”。把这两块放一起练,比单独背概念效率高得多。

第三个是手写题里的 this 还原。很多人在手写 debounce 或 Promise.all 时,会用箭头函数或者普通函数,连 this 怎么传给原函数都没考虑。一个很简单的判断标准:凡是你写的工具函数,内部调用回调时都应该用fn.apply(this, args)把上下文传回去,因为调用方很可能是在对象方法里使用你的工具函数。

根据我个人带人和面试的经验,前端八股看起来零散,但背后其实有一张网:类型相关的问题连到判断和转换,函数相关的问题连到作用域和闭包,异步相关的问题连到事件循环和 Promise 体系。你如果能把每一道小题都跟这张网里的两三个节点挂上钩,面试时就会明显感觉“有话说”,而不是“背了忘,忘了背”。这期整理里大部分题我都给了追问方向,建议你把这些追问都自己写一遍代码验证一遍,费不了多少时间,但效果比干看强很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 4:44:14

胶粘剂行业研究报告:口径、Python指标与交叉验证实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 4:42:57

x86电脑为什么能编译ARM程序?交叉编译原理与实践全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 4:39:51

从零搭建我的世界Java版服务器:局域网联机与公网访问全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 4:38:20

Jitsi Colibri 控制信令、统计监控与故障排查实战

Colibri 这个名字&#xff0c;我第一次见到是在一台 Jitsi Videobridge 的日志里——一行 WebSocket 握手记录&#xff0c;后面跟着一串看不出含义的 ID。当时我以为是某个新出的媒体库&#xff0c;翻了一圈才发现&#xff0c;它是自建会议系统里最关键、也最容易被跳过的一层&…

作者头像 李华
网站建设 2026/9/18 4:38:03

Python协程与asyncio入门:从事件循环到并发编程实战

先聊点实际的。学 Python 绕不过并发&#xff0c;而 Python 并发绕不过协程。只要你写过爬虫、做过接口轮询、处理过大量 I/O 操作&#xff0c;一定体会过“程序卡在等待上”的那种无力感。比如你用 requests 下载几十个文件&#xff0c;一个请求没回来&#xff0c;后面全堵着&…

作者头像 李华