前端面试高频易忘知识点复盘:闭包、事件循环与原型链精讲
2026/9/15 11:31:30 网站建设 项目流程

1. 这份“易忘清单”是怎么来的

干了几年前端,有个体会特别深:真正难住你的,往往不是那些新框架、新工具,而是你觉得自己“早就懂了”的基础知识。闭包、事件循环、原型链、组件通信、缓存策略……面试被问到时,话到嘴边说不利索;开发时遇到诡异 bug,排查半天发现又是this指向问题。

我把这些反复踩坑的知识点整理成了一份“易忘清单”,不是那种从入门到精通的系统教程,而是更像一个错题本——专治“学过但忘了”“用过但没吃透”的老毛病。如果你正在准备前端面试,或者工作了两三年想系统复盘一下基础,这份清单应该能派上用场。

前端这门技术的特点就是杂而不深,知识点像散落一地的珠子,平时各用各的,真到用的时候才发现串不起来。这篇文章不打算面面俱到,而是挑那些最容易忘、最高频被问、最影响实际开发效率的点来讲。每个点我都尽量把“是什么、为什么、怎么用、坑在哪”说清楚,争取让你看完就能直接去面试现场用。

2. 基础不牢,地动山摇:JS 核心机制里的易忘点

2.1 闭包、执行上下文与 this 指向

闭包这个概念,每个前端都能说两句“函数内部访问外部变量”,但真正考起来就没那么简单了。闭包的底层机制是作用域链:函数的执行会创建自己的执行上下文,同时保留对父级作用域的引用,即使父级函数已经执行完毕,子函数依然能通过作用域链访问父级的变量。

实际开发里闭包最常见的两个场景,一个是防抖节流,一个是在循环中异步操作。先说防抖,最经典的写法:

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

这段代码里的timer就是闭包变量,每次调用返回的函数,都能访问到同一个timer。如果你不理解闭包,很容易在封装通用组件时写出“防抖失效”的代码。

循环中的异步操作是另一个高频易错点,经典题目是打印 0 到 9:

for (var i = 0; i < 10; i++) { setTimeout(() => console.log(i), 1000); // 输出 10 个 10 }

var声明的i是函数作用域的,循环结束后i已经变成 10,所以闭包捕获的是同一个变量。解决方式不外乎三种:改用let声明、用 IIFE 包裹、或者用bind传参。理解了闭包捕获的是变量引用而不是值的快照,这个问题就再也难不倒你。

this指向的易忘程度比闭包还夸张。核心就一句话:this在函数调用时才确定,谁调用了函数,this就指向谁。背后的原理是执行上下文里的ThisBinding,在函数调用时根据调用方式动态绑定。

常见的坑是解构赋值和回调函数里的this丢失。比如:

const obj = { name: '前端', getName() { return this.name; } }; const { getName } = obj; getName(); // undefined,因为这里 this 指向 undefined

很多人在封装组件时,把对象方法单独提取出来用,结果发现this没了,这就是没理解this的运行时绑定规则。解决办法是箭头函数保留词法作用域的this,或者用bind显式绑定。

2.2 事件循环:微任务和宏任务的执行顺序

这个题是我面试别人时的必问题,也是日常工作最容易忽略的。JS 是单线程的,但浏览器通过事件循环机制实现了异步非阻塞。每次执行完同步代码后,会清空微任务队列,然后再从宏任务队列里取下一个任务执行。

微任务包括 Promise 的回调、queueMicrotaskMutationObserver;宏任务包括setTimeoutsetInterval、I/O 操作、UI 渲染。执行顺序可以概括为:同步代码 → 微任务 → 宏任务(其中宏任务执行完后又会产生新的微任务,先清空再进入下一个宏任务)。

来一道经典的组合题:

console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // 输出顺序:1 -> 4 -> 3 -> 2

很多人知道答案是 1、4、3、2,但再深一层问“为什么 Promise 比 setTimeout 先执行”就卡住了。关键在于 Promise 的回调是微任务,在当前同步代码执行完、宏任务开始之前就会清空;而setTimeout即使延时为 0,也至少会被放到下一轮宏任务里。

这个机制在实战里的影响非常大。比如你在组件里写了setTimeout等 DOM 更新后再取值,很可能取到的是旧值,因为布局和绘制发生在宏任务之间。遇到这类“等一等再执行”的需求,优先考虑微任务或者requestAnimationFrame,而不是盲目用setTimeout猜时间。

Node.js 环境下的事件循环还多出process.nextTicksetImmediate,面试时如果被问 Node 和浏览器的事件循环区别,重点答微任务清空时机和定时器阶段的不同。

2.3 原型链与继承的几种写法

原型链的知识点不多,但特别容易忘。每个对象都有一个[[Prototype]],通过__proto__(浏览器实现)或者Object.getPrototypeOf可以访问到。构造函数有prototype属性,实例通过[[Prototype]]指向构造函数的prototype。访问对象属性时,先查自身,查不到就沿着原型链向上找,直到Object.prototype[[Prototype]]null

继承的几种写法,我建议都手写一遍,面试和日常都有用:

ES5 的构造函数继承,需要解决两个问题——调用父类构造函数以及继承原型方法:

function Parent(name) { this.name = name; } Parent.prototype.sayName = function() { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); // 继承实例属性 this.age = age; } Child.prototype = Object.create(Parent.prototype); // 继承原型方法 Child.prototype.constructor = Child;

ES6 的class本质上还是原型链的语法糖:

class Parent { constructor(name) { this.name = name; } sayName() { console.log(this.name); } } class Child extends Parent { constructor(name, age) { super(name); // 必须先调用 super this.age = age; } }

面试常问的super为什么必须先调用,因为子类实例的创建依赖父类的构造函数先执行,这背后是this的初始化顺序问题。ES5 的寄生组合式继承其实已经模拟了这套机制,理解清楚了就不怕被追问。

3. 浏览器与网络:面试和生产里都在考的基础

3.1 事件机制:冒泡、捕获与事件委托

浏览器的事件流三阶段经常被人遗忘,尤其是“先捕获后冒泡”的顺序容易混淆。我做了一个简单的记忆法:事件从window出发向下到达目标元素的过程是捕获阶段,目标元素自己处理事件是目标阶段,再从目标元素向上回到window是冒泡阶段。addEventListener的第三个参数是true,就在捕获阶段触发;默认false是冒泡阶段触发。

实际开发里,事件委托是高频使用的技巧。比如一个列表,有几百个li,你不需要给每个li绑定事件,只需要在父容器上监听一次,通过event.target判断点击的是哪个元素:

document.querySelector('ul').addEventListener('click', (e) => { const li = e.target.closest('li'); if (li) { console.log('点击了', li.textContent); } });

有些面试官会追问:React 的事件系统是天然的事件委托,所有事件都挂载到根容器上,所以动态插入的元素也能响应事件。这个知识点能答出来,会显得你理解得比较深。

易忘的坑有两个。第一个是e.stopPropagation()只能阻止冒泡,不能阻止捕获;第二个是removeEventListener移除监听时,回调函数必须是同一个引用,所以匿名函数无法被移除。很多人写代码从来不移除事件监听,导致组件卸载后依然响应,出现内存泄漏和重复触发。

3.2 跨域方案与缓存策略的取舍

跨域是老生常谈,但每次写还是容易忘配置。同源策略是浏览器的安全机制,协议、域名、端口任一不同就算跨域。常规解决方案里,开发环境最常用代理转发,生产环境最常用 CORS。

CORS 的核心是服务器返回Access-Control-Allow-Origin等响应头。注意区分简单请求和预检请求:GET、POST、HEAD 且没有自定义头,属于简单请求,不会触发 OPTIONS 预检;一旦加了授权头、Content-Typeapplication/json等,就会先发 OPTIONS 请求确认服务端是否允许。

一个很隐蔽的易忘点:跨域请求携带 Cookie,前端要设置withCredentials: true,后端要返回Access-Control-Allow-Credentials: true,并且Access-Control-Allow-Origin不能是*。前后端同时改才能生效,缺一个请求就会出现“明明登录了却拿不到用户信息”的怪现象。

缓存策略同样是容易答不全的点。HTTP 缓存的优先级是:Service Worker > Memory Cache > Disk Cache > 网络请求。强缓存(Cache-Control)和协商缓存(ETag/Last-Modified)要分清。Cache-Control: max-age=3600表示 1 小时内直接走缓存;ETag是资源内容的标识,每次请求都会带上If-None-Match和服务端比对,304 就继续用缓存。

结合前端发布场景,静态资源文件名带 hash 是因为内容变了文件名就变了,可以放心用强缓存;HTML 文件不能长缓存,否则发版后用户访问的还是旧页面。这个逻辑搞清楚了,上线时就不用再因为缓存问题被运维拉去“开会”了。

3.3 回流与重绘:性能优化的第一课

回流(reflow)和重绘(repaint)的区别,一句话说:回流一定会触发重绘,重绘不一定回流。回流需要重新计算元素的几何属性(位置、大小),重绘只是重新绘制像素,比如改变颜色、背景。

实际开发中,减少回流的经典做法有:

  • transform代替top/left动画,因为transform触发的是合成线程,不会引起回流和重绘。

  • 批量操作 DOM,可以先display: none隐藏节点,操作完再显示,或者用DocumentFragment一次性插入。

  • 读取布局属性时,注意强制同步布局的问题。比如循环里先改样式再读offsetWidth,浏览器每轮都会强制回流一次。正确的做法是先把要读的值缓存下来,再批量改样式。

拿一个示例说明,假设你要给列表项设置不同的高度:

// 反面示例:循环里读取 + 写入交替,触发多次回流 items.forEach((item) => { item.style.height = item.dataset.height + 'px'; console.log(item.offsetHeight); // 每次都强制回流 }); // 正面示例:先全部读取,再全部写入 const heights = items.map((item) => item.dataset.height); items.forEach((item, index) => { item.style.height = heights[index] + 'px'; });

这个优化思路在数据可视化、长列表渲染等场景特别重要。有个容易被忽略的点:requestAnimationFrame的回调在下次重绘之前执行,适合做动画和批量 DOM 更新。面试时提到“用 rAF 合并帧”,会显得你有实际性能优化经验。

4. 框架与工程化:组件通信、Hooks 与微前端

4.1 组件通信的几种姿势

组件通信是前端开发的一天听八百遍的话题,但真到用的时候,很多人还是习惯手写一堆props和回调。不同场景适合的通信方式不同:

  • 父传子:props,最简单直白。

  • 子传父:回调函数,父组件把函数传给子组件,子组件调用时带上数据。

  • 兄弟组件:提升状态到共同父级,或者用全局状态库。

  • 跨层级组件:Context(React 的createContext、Vue 的provide/inject)适合做主题、语言包这种全局属性。

  • 全局通信:EventBus 或者状态管理库(Redux、Pinia),适合复杂应用。

这里我想展开说一个容易踩坑的点:React 里用 Context 做全局状态时,如果 provider 的 value 不是记忆化的(用useMemo包裹),会导致所有消费者组件每次都重新渲染。Vue 的provide/inject也有类似注意点——inject 的数据默认不是响应式的,需要在provide时使用reactiveref包装。

组件通信还有个“传参”的细节,很容易被忽视:如果你传给子组件的是一个内联对象字面量,那么每次渲染都会创建新的引用,子组件的 memo 优化就失效了。写成:

<Child config={{ name: 'test' }} /> // 每次渲染都新建对象

不如:

const config = useMemo(() => ({ name: 'test' }), []); <Child config={config} />

对于需要被 memo 优化的组件,这个细节能带来实打实的性能提升。

4.2 Hooks 依赖数组的那些坑

React Hooks 用熟了之后,大家往往只记得写法,忘了背后的“依赖收集”逻辑。useEffect的第二个参数不传、传空数组、传具体依赖,三种行为完全不同。

  • 不传:每次渲染后都执行,如果内部修改了 state,容易死循环。

  • 空数组:只在挂载时执行一次,适合初始化请求。

  • 传依赖:依赖变化时才执行。

真正容易踩的是“闭包陷阱”。下面这段代码:

const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); // 永远打印 0 }, 1000); return () => clearInterval(timer); }, []);

因为空数组导致 effect 只在挂载时执行,闭包捕获的是初始的count,后续count变化了闭包里的值也不会更新。解法有两种:把count加进依赖数组,但会导致定时器被反复清除重建;或者用useRef保存最新的count

const countRef = useRef(count); countRef.current = count; useEffect(() => { const timer = setInterval(() => { console.log(countRef.current); // 永远是最新值 }, 1000); return () => clearInterval(timer); }, []);

useCallback的依赖数组也一样,忘记加依赖会得到旧闭包,加了不相关的依赖又会导致函数频繁重建。我的经验是:只要 useEffect、useCallback、useMemo 里的逻辑引用了外部变量,就别偷懒,变量全部列进依赖数组,然后用useRef去处理“只想拿最新值但不希望触发重新执行”的场景。

4.3 微前端的核心理念与落地要点

微前端是这两年面试的大热门,这个词听起来很高级,本质就是“把一个大应用拆成多个独立部署的子应用”。qiankun 是这个方向最常被提到的方案,核心是基于 single-spa 做了一层封装,让子应用接入成本低很多。

它的原理可以简化为三步:主应用注册子应用并定义激活规则;子应用导出bootstrapmountunmount生命周期;切换路由时,主应用加载对应子应用资源并挂载到指定容器。

实际落地时容易踩的坑基本集中在三块:

  • 样式隔离:qiankun 默认开启样式隔离,但如果有全局样式没有处理好,还是会出现样式污染。子应用尽量把样式作用域控制在应用内,避免写全局选择器。

  • JS 沙箱:子应用里的window会通过代理做隔离,但如果你直接操作window上的全局变量,跨应用共享时还是会有问题。

  • 资源加载:子应用的静态资源需要使用绝对路径或者配置 publicPath,否则在微前端环境里会 404。

如果你只是用 iframe 也能实现“看起来像微前端”的效果,但 iframe 的体验问题很明显:路由不同步、弹窗层层嵌套、状态无法共享。面试官问你“为什么用微前端而不用 iframe”,除非你说清楚 iframe 的缺点和微前端解决的真实场景,不然就会停留在概念层。

5. 性能优化与安全:容易被问爆的话题

5.1 页面加载性能优化清单

性能优化是面试必考题,我总结了一份足够应付大多数面试和实际项目的优化清单:

  • 资源体积优化:JS/CSS 压缩混淆、Tree Shaking、按需加载、代码分割。

  • 图片优化:WebP 格式、srcset适配不同屏幕、懒加载(loading="lazy")和占位图。

  • 网络优化:CDN 加速、HTTP/2 多路复用、开启 Gzip/Brotli 压缩。

  • 渲染优化:减少首屏阻塞的同步脚本、deferasync按需选择。

  • 缓存优化:合理使用强缓存和协商缓存,Service Worker 做离线缓存。

面试官如果追问“首屏优化具体怎么做”,可以从“关键渲染路径”角度回答:拿到 HTML 后浏览器要解析生成 DOM 树和 CSSOM 树,再合成渲染树,布局,绘制。所以 CSS 要尽量精简并尽早加载,JS 会阻止 DOM 解析所以放到最后或者加defer

开发时会用到的一个技巧是构建阶段的代码分割。以 Webpack 为例,入口文件和第三方依赖分开打包,业务代码变化时只加载业务代码,依赖库走缓存。Vite 用的 Rollup 也支持类似能力,配置manualChunks手动分割。

实际项目里最容易被忽视的是字体优化。你可以用font-display: swap避免字体加载时文字不可见,也可以只加载需要的字重,而不是整包引入。

5.2 XSS、CSRF 与前端安全的常见坑

安全主题在面试里出现频率极高,但很多人只记得“XSS 攻击”和“CSRF 攻击”这两个名字,真要说清原理和防御方案,就暴露了。

XSS(跨站脚本攻击)的本质是用户输入被当成代码执行了。存储型 XSS 会把恶意脚本存到数据库,用户访问页面时被加载执行;反射型 XSS 通过 URL 参数注入;DOM 型 XSS 通过操作 DOM 时执行了恶意内容。防御的核心原则是不信任用户输入,转义输出内容、使用textContent而不是innerHTML、设置 CSP(内容安全策略)。

React 和 Vue 默认会对插值内容做转义,所以框架帮你挡了一部分攻击,但如果你用dangerouslySetInnerHTMLv-html,就绕过了这层保护,必须确保内容是安全的。

CSRF(跨站请求伪造)的原理是:用户已经登录了某个网站,攻击者诱导用户访问恶意网站,恶意网站发出的请求会带上用户的 Cookie,导致服务端以为这是用户本人的操作。防御手段包括:校验请求头里的Referer、使用自定义请求头、关键操作使用验证码、表单中加 CSRF Token。前后端分离的项目里,CSRF Token 配合 Cookie 使用非常常见,如果只是用 JWT 存localStorage,CSRF 风险相对低一些,但会面临 XSS 窃取 Token 的风险。

真正的实践经验是:不要把敏感信息放心大胆地写进localStorage。Session 放 Cookie + HttpOnly + SameSite 是目前比较稳妥的方案,SameSite=Lax能阻止大部分跨站请求携带 Cookie,是改动成本最低、效果最好的一道防线。

6. 面试“错题本”实录:几位候选人的高频翻车点

这部分内容来自我参与技术面试的记录,不是网上流传的题目,而是真实发生过的高频翻车点,也最能反映“易忘”的典型场景。

有候选人被问到“Vue 的 nextTick 是在什么时候执行的”,只答出“DOM 更新后”就说完了,深问“微任务还是宏任务”就卡壳。Vue 的 nextTick 内部会优先使用 Promise(微任务),所以它在同一轮事件循环内就能拿到更新后的 DOM,但如果嵌套使用或者在特殊环境下降级为setImmediatesetTimeout,执行时机就会后移。这个细节面试官特别喜欢挖。

还有一次面到“防抖和节流的区别”,候选人把定义背得很熟,但让他写一个带立即执行的防抖函数就写不出来了。立即执行防抖的意思是首次触发立即执行,之后在等待时间内再次触发不执行,等时间过了再触发的下一次又立即执行。这个版本结合了“执行+等待”两个状态,把边界条件理清楚,才算真正掌握了防抖。

另一个高频翻车点是“深拷贝和浅拷贝”。很多人能说出JSON.parse(JSON.stringify(obj))会丢失函数、undefined、循环引用,但对于structuredClone这个现代 API 了解不多。structuredClone能处理大多数数据类型,包括DateMapSet,但仍不能处理函数和 DOM 节点。面试时主动提到这个 API,会让你的答案比“八股文”版本更有含金量。

这里我整理了几个易忘考点的速查表,适合面试前一晚快速过一遍:

知识点核心结论高频追问方向
事件循环同步先执行,微任务清空后再取宏任务Promise 和 setTimeout 的执行顺序
this 指向运行时由调用者决定箭头函数、bind、解构丢失 this
原型链属性查找沿链向上,直到 null实现继承的几种方式
闭包函数捕获父级作用域变量引用防抖节流、循环中使用 var 的陷阱
缓存强缓存优先于协商缓存发版时 HTML 为什么不能缓存
XSS/CSRF转义输入、限制请求来源localStorage 存 Token 的优缺点
微前端子应用导出生命周期,主应用动态挂载样式隔离、沙箱、资源路径
Hooks依赖数组决定 effect 重新执行闭包陷阱、useRef 保存最新值

7. 一些零碎但值得记住的经验

除了上面这些硬核知识点,我还想分享几条在工作里积累的“软经验”,它们没有标准答案,但确实能让你少走弯路。

第一点:写代码的时候顺手写注释,不是为了别人看,是为了三个月后的自己。尤其是那些“为什么要这样做”的注释,比如“这里不能用 useMemo 因为依赖变化太多,改用 ref 保存最新值”,等回头维护时能省很多时间。

第二点:调试时优先用浏览器 DevTools 的 Performance 面板和 Network 面板,而不是到处console.log。Network 里的瀑布图能清楚看到资源加载的瓶颈,Performance 能定位主线程的耗时函数。有一次排查首屏白屏问题,就是在 Performance 里发现某个第三方脚本阻塞了渲染,替换成异步加载后首屏时间下降了一倍。

第三点:面试答题时可以用“是什么 → 为什么 → 怎么用 → 有什么坑”的结构来组织,这条规律对技术问答几乎百试百灵。比如问 CSS 的position: sticky,先说它是相对定位和固定定位的混合体,再说它依赖最近的滚动容器,接着讲应用场景,最后说明父元素 overflow 不是hidden时容易失效。这样一个答案的完整度,明显比背定义要强。

最后,关于前端“易忘”的本质,我个人的体会是:前端知识更新太快,容易让人把注意力全部放在新框架、新工具上,而忽视了那些底层稳定、十年不变的基础原理。浏览器的事件机制、JS 的作用域和闭包、HTTP 的缓存策略,这些不会因为 React 出了新版本就失效。花点时间把基础啃扎实,你写高级业务时才会更从容,面对面试官的连环追问时也不会慌。

这份清单我每隔一段时间都会重新过一遍,每次都会发现有的知识点又有点模糊了。前端这个行业就是这样,入行容易,精通难,保持复盘的习惯比什么都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询