前端模拟面试
作者: github star 和 星星加星 和github 刷星 3人共同主笔
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
面试官:你好,先简单介绍一下自己和最近做的一个项目吧。
在开场介绍环节,建议运用 STAR 法则,清晰阐述项目背景、个人任务、采取行动及最终成果,突出与岗位匹配的技术能力与解决问题的能力。
- 回答示例:您好!我叫 [姓名],从事前端开发工作 [X] 年了。在技术栈方面,我对 React、Vue 都比较熟悉,也掌握 Webpack 等工程化工具。最近我参与了公司的电商平台优化项目,负责商品详情页和购物车模块的重构工作。在商品详情页,原页面加载速度慢,图片显示不流畅。我通过优化图片加载策略,采用 WebP 格式图片结合懒加载技术,将图片加载时间缩短了 30%。同时,利用 React.memo 和 useMemo 对组件进行性能优化,减少不必要的重渲染。在购物车模块,我实现了实时计算总价、优惠策略应用等功能,通过 Redux 管理购物车状态,确保数据在不同页面间的一致性,提升了用户购物体验,该项目上线后,相关页面的转化率提升了 15% 。
面试官:好的,了解了。那我们先聊聊基础吧。你能讲讲 JavaScript 中null
和undefined
的区别吗?实际开发中一般在什么场景下使用它们?
这个问题考察对 JavaScript 基础数据类型的理解,回答时需明确两者定义、区别,结合实际场景说明用途。
- 回答示例:
null
和undefined
在 JavaScript 中都表示 “无值”,但本质上有区别。null
是一个表示 “空值” 的对象,它是有意设置为无值的,意味着此处原本应该有一个对象,但目前为空。例如,当我们初始化一个变量,准备稍后再给它赋值一个对象时,可能会先将其设为null
:let myObject = null;
。undefined
表示变量声明了但未初始化,或者访问对象不存在的属性,函数没有返回值时也会返回undefined
。比如let a; console.log(a);
,这里a
的值就是undefined
。在实际开发中,null
常用于释放对象引用,比如在清除定时器时,将定时器变量设为null
,防止内存泄漏:
javascript
let timer = setInterval(() => { // 执行某些操作}, 1000);// 清除定时器clearInterval(timer);timer = null;
而undefined
常用于判断函数参数是否传递。例如:
javascript
function greet(name) { if (typeof name === \'undefined\') { name = \'Guest\'; } console.log(`Hello, ${name}!`);}greet(); // 输出Hello, Guest!greet(\'Alice\'); // 输出Hello, Alice!
面试官:嗯,不错。那再问一个关于异步的问题:setTimeout
、Promise.then
、async/await
的执行顺序是怎样的?能举个例子说明吗?
这道题聚焦 JavaScript 异步机制,需掌握事件循环中宏任务、微任务概念,通过示例清晰阐述执行顺序。
- 回答示例:在 JavaScript 的事件循环机制中,
setTimeout
属于宏任务,Promise.then
属于微任务,async/await
本质上也是基于 Promise 实现的。执行顺序是先执行同步代码,然后处理微任务队列,最后处理宏任务队列。例如:
javascript
console.log(\'start\');setTimeout(() => { console.log(\'setTimeout\');}, 0);Promise.resolve().then(() => { console.log(\'Promise.then\');});async function asyncFunc() { console.log(\'asyncFunc start\'); await Promise.resolve(); console.log(\'asyncFunc after await\');}asyncFunc();console.log(\'end\');
上述代码执行过程如下:
- 首先输出
start
,这是同步代码。 - 遇到
setTimeout
,将其回调函数放入宏任务队列。 - 遇到
Promise.resolve().then
,将其回调函数放入微任务队列。 - 执行
asyncFunc
,输出asyncFunc start
,遇到await Promise.resolve()
,await
后面的代码暂停执行,Promise.resolve()
返回一个已解决的 Promise,其.then
回调函数放入微任务队列。 - 继续执行同步代码,输出
end
。此时同步代码执行完毕。 - 开始处理微任务队列,先输出
Promise.then
,然后输出asyncFunc after await
。 - 最后处理宏任务队列,输出
setTimeout
。
面试官:了解了。那聊聊框架吧,你用 React 比较多是吧?能讲讲 React 的虚拟 DOM 和 Diff 算法的核心原理吗?为什么虚拟 DOM 能提升性能?
此问题考察对 React 核心原理的理解,回答需阐述虚拟 DOM 和 Diff 算法概念、工作原理,分析提升性能的原因。
- 回答示例:虚拟 DOM(Virtual DOM)是 React 为了高效更新 DOM 而引入的概念。它本质上是 JavaScript 对象,用来描述真实 DOM 树的结构。当组件状态或属性发生变化时,React 会创建一棵新的虚拟 DOM 树,然后与旧的虚拟 DOM 树进行对比,这个对比过程就是 Diff 算法。Diff 算法的核心原理是:
- 只对同级节点进行比较。当 React 发现两棵树的根节点类型不同时,会直接销毁旧树,创建新树。例如,如果原来根节点是,现在变为
- 通过给元素添加唯一的
key
属性来标识节点。在对比列表时,key
能帮助 React 准确判断哪些节点需要更新、删除或插入。比如有一个列表[
,如果数据变化后变为- Item1
,
- Item2
]
[
,React 通过- Item2
,
- Item3
]
key
能快速定位到Item1
需要删除,Item3
需要插入,Item2
位置变化但内容未变,只需移动 DOM 节点。
虚拟 DOM 能提升性能主要原因有:- 减少真实 DOM 操作。传统方式中,只要数据变化就直接操作真实 DOM,而真实 DOM 操作开销很大。虚拟 DOM 通过对比,只有在差异确定后才一次性更新真实 DOM。例如,当一个复杂表单中有多个输入框值变化,如果直接操作真实 DOM,每次值变化都可能触发重排和重绘。而虚拟 DOM 会先计算出所有变化,最后统一更新,大大减少了重排和重绘次数。
- 跨平台能力。由于虚拟 DOM 是 JavaScript 对象,不依赖特定平台的 DOM API,使得 React 可以方便地在不同环境(如浏览器、Node.js、移动端 RN 等)中使用,提高了代码复用性 。
面试官:好的。那在 React 中,
useState
的更新是同步还是异步的?为什么?如果想在状态更新后立即获取最新值,有哪些办法?该问题深入考察对 React Hook 中
useState
机制的理解,要明确更新特性及原因,给出获取最新值的方法。- 回答示例:在 React 中,
useState
的更新在合成事件和生命周期函数中是异步的,在原生 DOM 事件和setTimeout
等宏任务中是同步的。这是因为 React 为了提高性能,在合成事件和生命周期函数中批量处理状态更新。例如:
javascript
import React, { useState } from\'react\';function App() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); console.log(count); // 输出0,因为是异步更新 }; return (
{count}
在
handleClick
函数中,setCount(count + 1)
执行后,立即打印count
还是旧值 0 。
之所以这样设计,是为了避免频繁触发重新渲染。如果每次useState
更新都立即重新渲染,会导致性能问题。比如在一个列表渲染中,有多个元素同时触发状态更新,如果每个更新都立即重新渲染,会造成大量不必要的计算和渲染开销。
如果想在状态更新后立即获取最新值,有以下几种办法:- 使用
useEffect
。useEffect
会在组件渲染和状态更新后执行,通过依赖项数组可以控制其触发时机。例如:
javascript
import React, { useState, useEffect } from\'react\';function App() { const [count, setCount] = useState(0); useEffect(() => { console.log(count); // 每次count更新后会打印最新值 }, [count]); const handleClick = () => { setCount(count + 1); }; return (
{count}
- 在
setState
的回调函数中获取(在类组件中常用,React Hook 中不推荐但也可用)。例如在类组件中:
javascript
import React, { Component } from\'react\';class App extends Component { constructor(props) { super(props); this.state = { count: 0 }; } handleClick = () => { this.setState((prevState) => ({ count: prevState.count + 1 }), () => { console.log(this.state.count); // 输出最新值 }); }; render() { return (
{this.state.count}
- 使用
useReducer
替代useState
。useReducer
可以更精细地控制状态更新逻辑,在某些场景下能方便获取最新状态 。
面试官:嗯。再聊聊工程化,你项目中用过 Webpack 或者 Vite 吧?能说说 Vite 为什么比 Webpack 在开发环境启动更快吗?它们的核心区别是什么?
这道题考查对前端构建工具的理解,要分析 Vite 启动快的原因,对比两者核心差异。
- 回答示例:在项目中我对 Webpack 和 Vite 都有实际使用经验。Vite 在开发环境启动比 Webpack 更快,主要原因如下:
- 基于 ES 模块(ESM)。Vite 利用浏览器原生支持的 ESM,直接将源代码提供给浏览器,无需像 Webpack 那样进行复杂的打包过程。例如,在 Webpack 中,它需要将各种类型的模块(如 CommonJS、ES 模块等)统一转换、打包成浏览器可识别的格式,这个过程涉及大量的解析、编译和代码合并。而 Vite 直接利用浏览器对 ESM 的支持,浏览器可以并行加载模块,大大提高了加载速度。
- 轻量级依赖预构建。Vite 会在启动时对一些较大的依赖包进行预构建,将其转换为 ESM 格式,并利用缓存机制。在后续开发中,如果依赖没有变化,Vite 可以快速复用缓存,减少重复构建工作。相比之下,Webpack 每次启动都需要重新分析和构建整个项目依赖,即使依赖没有变化,也可能因为其构建机制的特性,导致较长的启动时间。
Webpack 和 Vite 的核心区别主要体现在: - 构建理念。Webpack 是基于打包的构建工具,它会将项目中的各种资源(JavaScript、CSS、图片等)都打包成一个或多个 bundle 文件,适合大型复杂项目,通过各种 Loader 和 Plugin 可以实现丰富的功能扩展,如代码压缩、图片优化、热更新等。Vite 则是基于原生 ESM 的开发服务器和构建工具,开发阶段以快速的冷启动和热更新为优势,生产阶段才进行打包优化,更适合现代前端开发注重快速迭代的特点,尤其是在开发小型项目或对启动速度要求极高的场景下表现出色。
- 配置复杂度。Webpack 的配置相对复杂,需要开发者详细配置各种 Loader(如处理 CSS 的
css - loader
、style - loader
,处理图片的file - loader
等)和 Plugin(如html - webpack - plugin
用于生成 HTML 文件,mini - css - extract - plugin
用于提取 CSS 到单独文件等)来满足不同的项目需求。而 Vite 的配置相对简洁,它有一套合理的默认配置,对于大多数常见场景,开发者无需过多配置即可快速上手,只有在项目有特殊需求时才需要进行额外配置 。
面试官:了解了。那性能优化方面,你在项目中做过哪些具体的优化措施?比如首屏加载慢,你是怎么解决的?
本题考查实际项目中的性能优化能力,需结合项目经历,列举优化措施及解决首屏加载慢的方法。
- 回答示例:在项目中,针对性能优化我采取了多方面措施。
- 资源优化:
- 图片优化。将图片转换为 WebP 格式,这种格式在保证图片质量的同时,文件大小比 JPEG、PNG 等传统格式更小。例如,项目中的产品展示图片,转换为 WebP 格式后,平均文件大小减少了 30% - 40% 。同时,采用图片懒加载技术,只有当图片进入视口时才进行加载。在 React 项目中,使用
react - lazyload
库实现图片懒加载,避免页面初始加载时大量图片同时请求,减少带宽占用。 - 代码压缩与合并。使用 Webpack 的
terser - webpack - plugin
对 JavaScript 代码进行压缩,去除冗余代码和注释,减小文件体积。同时,将多个 CSS 文件合并为一个,减少 HTTP 请求次数。例如,原本项目中有 5 个 CSS 文件,合并后 HTTP 请求减少了 4 次。
- 图片优化。将图片转换为 WebP 格式,这种格式在保证图片质量的同时,文件大小比 JPEG、PNG 等传统格式更小。例如,项目中的产品展示图片,转换为 WebP 格式后,平均文件大小减少了 30% - 40% 。同时,采用图片懒加载技术,只有当图片进入视口时才进行加载。在 React 项目中,使用
- 网络优化:
- 启用 CDN。将项目中的静态资源(如图片、CSS、JavaScript 文件)部署到 CDN 节点上,利用 CDN 的全球分布式节点,根据用户地理位置就近提供资源,加快资源加载速度。例如,将项目部署到阿里云 CDN 后,海外用户访问速度提升明显。
- 优化 HTTP 缓存策略。设置合理的缓存过期时间,对于不常更新的静态资源(如字体文件、一些基础 CSS 和 JavaScript 库),设置较长的缓存过期时间(如一年),减少重复请求。通过在服务器端配置
Cache - Control
和Expires
头信息实现。
- 针对首屏加载慢的问题,除了上述通用优化措施外,还采取了以下方法:
- 服务器端渲染(SSR)。在 React 项目中,使用 Next.js 框架实现 SSR。通过 SSR,服务器将渲染好的 HTML 页面直接返回给浏览器,浏览器无需等待 JavaScript 下载和执行后再渲染页面,大大加快了首屏显示速度。例如,采用 SSR 后,首屏加载时间从原来的 3 秒缩短到 1.5 秒。
- 骨架屏。在页面数据未加载完成前,展示一个骨架屏占位。在 Vue 项目中,使用
vue - skeleton - webpack - plugin
生成骨架屏,让用户在等待数据加载过程中有更好的体验,避免页面长时间空白 。
面试官:最后来道编程题吧。请手写一个防抖函数(debounce),并说明它的应用场景。
这是一道编程实践题,需要现场手写代码实现防抖函数,并阐述其应用场景。
- 回答示例:防抖函数的作用是在一定时间内多次触发同一事件,只执行最后一次。实现如下:
javascript
function debounce(func, delay) { let timer; return function() { const context = this; const args = arguments; clearTimeout(timer); timer = setTimeout(() => { func.apply(context, args); }, delay); };}
解释一下代码:首先定义一个
debounce
函数,它接收两个参数,一个是需要防抖的函数func
,另一个是延迟时间delay
。在debounce
函数内部,定义一个timer
变量用于存储定时器。然后返回一个新的函数,在这个新函数中,首先获取当前的上下文this
和参数arguments
,接着清除之前设置的定时器(如果有),防止之前的定时器还未触发就又触发了新的事件。最后重新设置一个定时器,在delay
时间后执行原始函数func
,并将上下文和参数传递给它。
防抖函数的应用场景很广泛,比如:- 搜索框输入联想。用户在搜索框中不断输入内容时,如果每次输入都触发搜索请求,会造成大量不必要的网络请求。使用防抖函数,当用户停止输入一段时间(如 300 毫秒)后,再触发搜索请求,既能满足用户实时搜索的需求,又能减少网络压力。
- 按钮点击提交。防止用户在短时间内多次点击按钮,造成重复提交表单。例如在登录按钮上使用防抖函数,设置延迟时间为 1 秒,在这 1 秒内多次点击按钮,只会执行一次登录请求 。
- 通过给元素添加唯一的