面试题集合
web worker里面 可以发送接口请求吗?单文件上传在web worker里面主要做什么逻辑?
在 Web Worker 中可以发送接口请求,因为 Web Worker 环境支持 XMLHttpRequest 和 fetch API(现代浏览器中),这两种方式都能用于发起 HTTP/HTTPS 请求。
具体说明:
-
支持的 API:
fetch:现代浏览器的 Web Worker 环境原生支持fetch(基于 Promise),可以直接使用异步请求逻辑。XMLHttpRequest:传统的请求方式,在 Web Worker 中同样可用,适合需要兼容旧环境的场景。
示例(使用
fetch):// worker.js self.onmessage = async (e) => { const url = e.data; try { const response = await fetch(url); const data = await response.json(); self.postMessage(data); // 将结果发送给主线程 } catch (error) { self.postMessage({ error: error.message }); } }; -
限制:
- Web Worker 中无法操作 DOM、
window对象或主线程的全局变量,因此请求的结果需要通过postMessage传递给主线程,由主线程处理渲染等操作。 - 受浏览器同源策略限制,请求同样需要遵守 CORS 规则,跨域请求需服务器配合设置响应头。
- 部分环境(如某些旧版浏览器)可能对
fetch支持不完善,此时可降级使用XMLHttpRequest。
- Web Worker 中无法操作 DOM、
-
适用场景:
处理大量数据的请求(如下载大文件、批量接口调用),避免阻塞主线程导致页面卡顿。复杂的数据解析(如请求后需要处理大量 JSON 或 CSV 数据),将计算逻辑放在 Worker 中。
总结:Web Worker 完全支持接口请求,且是优化主线程性能的常用手段,只需注意结果需通过消息机制与主线程通信。
说一说前端设计模式
前端设计模式是解决高频开发问题的标准化思路,核心价值是解耦代码、提升复用性与可维护性,以下是最常用的 8 种模式(含核心逻辑 + 实战场景),适配 Vue/React 等主流框架:
1. 单例模式(Singleton)
- 核心:确保一个类 / 对象仅创建一次实例,全局可访问(避免重复初始化资源)。
- 场景:全局状态管理(Vuex/Pinia 实例)、工具类(如封装的 Axios 实例、日志工具)。
- 极简示例:Vue 中封装 Axios 单例,统一配置请求拦截器。
2. 工厂模式(Factory)
- 核心:通过 “工厂函数 / 类” 统一创建同类对象,隐藏创建细节,支持批量初始化。
- 场景:批量生成相似组件(如弹窗、表单输入框)、封装第三方插件实例(如地图组件)。
3. 观察者模式(Observer)
- 核心:“发布者 - 订阅者” 模式,发布者状态变化时自动通知所有订阅者更新(解耦依赖)。
- 场景:组件通信(Vue 的
$on/$emit、React 的 Context)、状态管理(Vuex 的响应式机制)。
4. 代理模式(Proxy)
- 核心:通过代理对象拦截目标对象的访问,实现数据监听、权限控制、日志记录等附加功能。
- 场景:Vue 3 响应式系统(
Proxy拦截对象读写)、表单输入验证、接口请求拦截。
5. 策略模式(Strategy)
- 核心:定义多个独立策略(算法 / 逻辑),根据场景动态切换,替代大量
if-else。 - 场景:表单验证(不同字段用不同校验规则)、动态渲染组件(根据角色切换显示逻辑)。
6. 装饰器模式(Decorator)
- 核心:不修改原对象,通过装饰器动态添加新功能(扩展逻辑而非修改原有逻辑)。
- 场景:组件功能扩展(如按钮添加加载状态)、TypeScript/React 中的
@decorator语法(如@connect)。
7. 适配器模式(Adapter)
- 核心:转换接口格式,解决 “接口不兼容” 问题(适配旧代码或第三方库)。
- 场景:第三方组件适配(如 Element UI 组件封装)、旧接口数据格式转换。
8. 命令模式(Command)
- 核心:将 “请求” 封装为命令对象,支持请求的排队、撤销 / 重做(如编辑器操作)。
- 场景:复杂交互(如编辑器撤销 / 重做、表单提交队列)。
这些模式并非孤立使用,常结合框架特性(如 Vue 组件化、React Hooks)灵活组合,比如 Vuex 同时用到单例 + 观察者模式。
你个人或者团队开发中,有用过哪些提升效率的方法
-
工程化配置复用:维护团队统一的
webpack/vite基础配置模板、ESLint/Prettier 规则集,规范化提交信息:用commitlint + husky约束提交信息格式 -
基础功能复用 axios、router、vuex、工具函数。
-
快捷键与编辑器配置:熟练使用 VS Code 快捷键(如代码格式化、快速跳转、批量修改);安装必备插件(如 ESLint、Prettier、Volar、Path Intellisense),自定义代码片段(如快速生成组件模板)。
-
碎片化学习:积累常用代码片段到备忘录或 GitHub Gist,遇到重复问题直接复制
canva的实现原理
前端流式渲染怎么实现
前端流式渲染核心是「数据分块接收、逐步渲染」,精简实现方案如下:
1. 纯前端(客户端流式处理)
- 核心 API:
Fetch + ReadableStream - 流程:请求流式 API → 逐块读取数据 → 解码处理 → 实时更新 DOM
- 示例(打字机效果):
async function streamRender() {
const res = await fetch('/api/stream');
const reader = res.body.getReader();
const decoder = new TextDecoder();
const output = document.getElementById('output');
while (true) {
const { done, value } = await reader.read();
if (done) break;
output.textContent += decoder.decode(value, { stream: true });
}
}
2. 框架流式 SSR(React 18+/Vue 3+)
- 核心:
Suspense + 服务端流式输出 - 流程:服务端先渲染同步组件(首屏内容)→ 异步组件用
Suspense占位 → 数据就绪后流式发送组件 HTML → 客户端激活 - 关键 API:React
renderToPipeableStream、VuecreateSSRApp+ 流式渲染适配器
3. 传统后端流式输出
- 核心:HTTP 分块传输(
Transfer-Encoding: chunked) - 流程:后端模板引擎逐块生成 HTML → 依次发送给浏览器 → 浏览器渐进式渲染
SSE流式对话的实现过程,怎么建立连接的
SSE(Server-Sent Events,服务器发送事件)实现流式对话的核心是建立一次 HTTP 长连接,由服务器单向持续向客户端推送数据,适用于聊天机器人、实时日志等场景。其实现过程和连接建立方式如下:
一、连接建立过程
-
客户端发起连接请求客户端通过创建
EventSource对象,向服务器发送一个 HTTP GET 请求,指定 SSE 连接的端点(如/stream),并在请求头中声明接受 SSE 格式:// 客户端代码 const eventSource = new EventSource('/stream'); // 建立SSE连接请求头会自动携带
Accept: text/event-stream,告知服务器需要持续的事件流。 -
服务器确认长连接服务器收到请求后,返回 HTTP 200 响应,并通过响应头声明:
Content-Type: text/event-stream(指定数据格式为 SSE)Cache-Control: no-cache(禁止缓存)Connection: keep-alive(保持长连接)此时,客户端与服务器的 TCP 连接被保持,不会像普通 HTTP 请求那样关闭。
二、流式对话实现流程
-
服务器持续推送数据服务器通过已建立的长连接,按照 SSE 格式(每行以
data:开头,空行分隔)分段发送数据。例如,聊天机器人生成回复时,每生成一段文字就推送一次:// 服务器推送的SSE格式数据 data: 你好,我是AI助手 data: 请问有什么可以帮你?(注意:每个数据块末尾需用两个换行符
\n\n结束) -
客户端接收并处理数据客户端通过
EventSource的onmessage事件监听服务器推送的数据,实时更新 UI(如拼接对话内容):eventSource.onmessage = (event) => { const chunk = event.data; // 接收服务器推送的片段 document.getElementById('chat').innerText += chunk; // 拼接显示 }; -
连接关闭
- 客户端:调用
eventSource.close()主动关闭连接。 - 服务器:若长时间无数据推送,可主动断开连接;客户端会自动重试(默认间隔 3 秒),可通过
retry字段自定义重试时间。
- 客户端:调用
核心特点
- 单向通信:仅服务器向客户端推送,客户端如需发送消息需另建 HTTP 请求(如 POST)。
- 自动重连:连接中断时,
EventSource会自动重试,无需手动处理。 - 轻量协议:基于 HTTP,无需像 WebSocket 那样握手升级,实现简单。
适用于对话生成、实时通知等 “服务器主动推送” 场景,相比 WebSocket 更简单,适合单向流式数据传输。
大文件上传切多大
核心结论:主流推荐切片大小 1-4MB,优先选 2MB(兼顾兼容性、速度和稳定性)。
关键选择依据:
-
网络环境:
- 弱网 / 移动端:1-2MB(减少单次请求失败概率,重传成本低);
- 高速网络 / PC 端:2-4MB(减少请求次数,提升整体效率)。
-
业务场景:
- 需断点续传 / 秒传:切片不宜过大(如 1MB,便于记录已上传分片);
- 大文件(10GB+):可适当增大至 4-8MB(减少请求量,降低服务器压力)。
-
兼容性:
- 避免小于 512KB(请求过多,占用连接池);
- 避免大于 10MB(部分浏览器 / 服务器对单请求体大小有限制,易超时)。
总结:
- 通用场景直接选 2MB;
- 弱网 / 移动端选 1MB;
- 高速网络 / 超大文件选 4MB。
实现私有属性的方法
| 方法 | 私有性 | 兼容性 | 推荐场景 |
|---|---|---|---|
| 命名约定 | 差 | 好 | 内部项目,不敏感数据 |
| 闭包 | 好 | 好 | ES5 环境,需要严格私有 |
| Symbol | 较好 | 较好 | 希望隐藏属性,但可接受绕过 |
| ES2022 私有字段 | 最好 | 一般 | 现代环境,需要强私有性 |
| WeakMap | 好 | 较好 | 类库开发,避免污染实例 |
try catch可以捕获promise的报错吗?
try/catch 不能直接捕获 Promise 内部的错误,因为 Promise 的错误是异步的,而 try/catch 只能捕获同步代码中的异常。
为什么不能直接捕获?
JavaScript 中,try/catch 用于捕获同步执行过程中抛出的错误。而 Promise 的 .then() 或 .catch() 中的代码是异步执行的,当 Promise 状态变为 rejected 时,错误会被放入异步任务队列,此时 try/catch 所在的同步代码已经执行完毕,无法捕获到后续异步抛出的错误。
示例:
try {
new Promise((resolve, reject) => {
reject(new Error("Promise 错误")); // 异步错误
});
} catch (error) {
console.log("捕获到错误:", error); // 不会执行
}
上面的代码中,try/catch 无法捕获 Promise 内部的 reject 错误。
正确的捕获方式
有以下几种方式可以捕获 Promise 的错误:
1. 使用 .catch() 方法
这是最常用的方式,在 Promise 链的末尾添加 .catch() 来捕获前面的所有错误。
new Promise((resolve, reject) => {
reject(new Error("Promise 错误"));
})
.then(result => {
console.log(result);
})
.catch(error => {
console.log("捕获到错误:", error); // 会执行
});
2. 使用 async/await 配合 try/catch
如果你在 async 函数中使用 await,就可以用 try/catch 来捕获 Promise 的错误。
async function fetchData() {
try {
const response = await new Promise((resolve, reject) => {
reject(new Error("网络错误"));
});
console.log(response);
} catch (error) {
console.log("捕获到错误:", error); // 会执行
}
}
fetchData();
这种方式看起来更像同步代码的错误处理,可读性更高。
3. 全局捕获
可以使用 window.addEventListener('unhandledrejection') 来捕获所有未被处理的 Promise 错误。这通常用于全局错误监控或日志记录。
window.addEventListener('unhandledrejection', event => {
console.log("捕获到未处理的 Promise 错误:", event.reason);
event.preventDefault(); // 阻止浏览器默认行为
});
new Promise((resolve, reject) => {
reject(new Error("未处理的错误"));
});
总结
try/catch只能捕获同步错误。- Promise 错误属于异步错误,必须用
.catch()或async/await + try/catch来捕获。 - 对于全局未处理的 Promise 错误,可以使用
unhandledrejection事件监听。
影响首屏最重要的因素是什么?
影响首屏加载的核心因素是 关键资源的加载与渲染效率,最关键的是以下 3 点(按优先级排序):
- 关键资源体积:首屏必需的 HTML、核心 CSS、关键 JS 体积过大(如未压缩、冗余代码、大图片),会直接拉长下载时间,是最主要瓶颈。
- 网络传输效率:服务器响应速度(TTFB)、网络延迟、带宽,以及是否启用 CDN、HTTP/2、缓存策略(如强缓存),直接影响资源到达浏览器的速度。
- 渲染阻塞:关键 CSS 未内联(导致渲染阻塞)、关键 JS 未用
defer/async(导致解析阻塞),或首屏 JS 执行耗时过长,会让浏览器无法及时绘制页面。
简单说:首屏快慢的核心,是「首屏必需资源能不能小、能不能快传到浏览器、能不能不阻塞渲染」。
给了一道promise.allsettled,我不会,换成了promise.all,写出来了,等结果了
Vue 更注重开发者体验和易用性,适合快速开发、中小项目或团队;React 强调函数式思想和灵活性,更适配大型复杂应用、团队协作场景。
大前端
大前端是以 Web 技术为核心,覆盖多终端、全链路的技术体系,核心是 “一套技术栈适配多平台”,打破传统前端仅局限于浏览器的边界,延伸到移动端、桌面端、小程序等场景,同时整合工程化、跨端、服务端渲染等能力。
核心特点
- 多终端覆盖:用前端技术(HTML/CSS/JS/TS)开发 Web 端、App(React Native/Flutter)、小程序、桌面应用(Electron)、车载 / 智能设备界面,实现 “一次开发,多端部署”。
- 全链路参与:前端工程师不仅做 UI 开发,还涉及工程化(构建、部署)、接口联调、服务端渲染(SSR)、跨端适配、性能优化等全流程。
关键技术方向
- 跨端开发:React Native、Flutter、Taro、UniApp;
- 工程化:Webpack/Vite、CI/CD、模块化 / 组件化、TS 类型约束;
- 端侧扩展:小程序、Electron(桌面)、PWA(渐进式 Web App);
- 性能与体验:SSR/SSG、懒加载、缓存策略、大文件处理;
- 生态延伸:Node.js 全栈(服务端接口、BFF 层)、低代码平台。
Promise静态方法,all、allsettled、race的区别,讲一下finally
线程在 JS 底层的使用
JavaScript 本身是单线程执行模型,但底层通过浏览器 / Node.js 的多线程架构和线程池实现异步操作(如网络请求、定时器、I/O 等),JS 线程仅负责执行主线程逻辑(如代码解析、DOM 操作),耗时操作则交由底层线程处理,最终通过事件循环回调到 JS 主线程。
一、JS 底层线程模型
-
JS 主线程负责执行同步代码、处理事件回调(如定时器、网络响应)、DOM 操作等,遵循 “单线程执行栈” 规则,同一时间只能执行一个任务。
-
浏览器底层线程(以 Chrome 为例)
- 渲染线程:处理 HTML/CSS 解析、页面布局与绘制,与 JS 主线程互斥(避免 DOM 冲突);
- 网络线程:处理 HTTP/HTTPS 请求,多线程并行(可同时发起多个请求);
- 定时器线程:管理
setTimeout/setInterval,计时完成后将回调推入事件队列; - Worker 线程:包括 Web Worker(主线程外的 JS 线程)、Service Worker(离线缓存线程);
- GPU 线程:处理图形渲染(如 Canvas、WebGL)。
-
Node.js 底层线程
- V8 主线程:执行 JS 代码;
- libuv 线程池:默认 4 线程,处理文件 I/O、DNS 解析等耗时操作;
- 事件循环线程:调度异步任务回调。
二、关键线程交互场景
-
异步任务的线程协作
// 网络请求由浏览器网络线程处理 fetch('https://api.example.com') .then(res => res.json()) // 回调在 JS 主线程执行 .catch(err => console.error(err)); // 定时器由定时器线程计时,计时结束后回调入队 setTimeout(() => console.log('timeout'), 1000);- 耗时操作(网络 / 定时器)由底层线程处理,完成后将回调推入事件队列;
- JS 主线程执行完同步代码后,通过事件循环从队列中取出回调执行。
-
Web Worker 独立线程为解决主线程阻塞问题,Web Worker 允许创建独立 JS 线程:
-
Node.js 线程池调度文件 I/O 等操作由 libuv 线程池处理,避免阻塞 V8 主线程:
const fs = require('fs'); // 异步读取文件由线程池处理 fs.readFile('file.txt', (err, data) => { if (err) throw err; console.log(data); });
三、核心结论
- JS 语言层面单线程,但底层依赖多线程架构实现异步;
- 耗时操作(I/O、网络、计时)由专用线程处理,JS 主线程仅负责回调执行;
- Web Worker/Node.js Worker 可创建额外 JS 线程,实现并行计算(但无法访问 DOM)。
简单说,JS 的 “单线程” 是指业务逻辑执行单线程,底层通过多线程协作支撑异步能力,这也是 JS 能高效处理并发请求的关键。
跨标签页通信
跨标签页通信是指浏览器中多个同源标签页 / 窗口之间的数据交互,核心依赖浏览器的存储机制或广播 API,以下是常用方案及原理:
1. LocalStorage + storage 事件
-
原理:同源标签页共享
localStorage,当一个标签页修改localStorage时,其他标签页会触发storage事件。 -
用法:
// 发送消息的标签页 localStorage.setItem('msg', 'Hello from Tab1'); // 接收消息的标签页 window.addEventListener('storage', (e) => { if (e.key === 'msg') console.log('收到消息:', e.newValue); }); -
特点:简单易用,但仅支持字符串数据,且需同源。
2. Broadcast Channel API
-
原理:专门用于同源标签页通信的 API,创建频道后可直接广播消息。
-
用法:
// 所有标签页创建同一频道 const channel = new BroadcastChannel('my-channel'); // 发送消息 channel.postMessage({ text: 'Hello' }); // 接收消息 channel.onmessage = (e) => console.log('收到:', e.data); -
特点:支持任意类型数据(对象、数组),语义清晰,是推荐方案。
3. SharedWorker
- 原理:通过共享的 Web Worker 作为中间层,多个标签页连接同一个 Worker 实现通信。
- 特点:适合复杂场景,但实现稍复杂,需处理连接管理。
4. Cookie + 轮询
- 原理:修改 Cookie 后,其他标签页定时读取 Cookie 变化。
- 特点:兼容性好,但 Cookie 容量小(4KB),轮询耗性能,仅适合简单场景。
核心要点
- 同源限制:所有方案均要求标签页同源(协议、域名、端口一致);
- 常用方案:优先用
Broadcast Channel(简洁高效)或LocalStorage + storage事件(兼容性好); - 跨域场景:需借助后端中转(如 WebSocket、接口转发)。
简单说,跨标签页通信本质是利用浏览器的共享存储或专用广播机制,实现同源页面间的数据同步。
在编写代码过程中的开发经验,是如何减少错误的
-
- 答题要点:分享编写代码过程中的开发经验,如代码审查、单元测试、代码规范等,并说明这些经验如何减少错误。
介绍一下你们的监控系统是如何构建的
-
- 答题要点:介绍监控系统的构建过程,包括数据源、数据存储、数据分析、告警通知等方面。
WebSocket是如何建立连接的?对于丢包和延迟问题,是如何处理的
请列举一些Node.js的流行框架,并简要说明它们的作用
-
- 答题要点:列举Node.js的流行框架,如Express、Koa等,并简要说明它们的作用和适用场景。
Express:偏向 “全能型框架”,上手快、生态成熟,适合中小型项目或快速开发;
Koa:偏向 “轻量内核 + 插件扩展”,异步处理更优雅,适合复杂异步场景或需高度定制的项目。
简单说,Express 是 “开箱即用的工具集”,Koa 是 “灵活定制的内核”。
是否有使用过低代码平台?如果有,请描述其工作原理
低代码平台的核心工作原理是 “可视化拖拽 + 配置化生成 + 底层代码自动化” ,本质是用图形化界面替代传统手写代码,通过封装通用组件和逻辑,让用户快速搭建应用,核心流程可拆解为 3 步:
1. 组件与逻辑封装(底层基础)
平台预先封装大量通用模块:
- UI 组件:按钮、表单、表格等可视化元素(支持自定义样式 / 属性);
- 业务逻辑模块:数据请求、表单校验、权限控制等通用功能;
- 数据源适配:内置数据库(如 MySQL)、API 接口、第三方系统(如钉钉 / 企业微信)的连接能力。
这些模块被标准化为 “可拖拽、可配置” 的原子单元,隐藏底层复杂代码。
2. 可视化设计与配置(用户操作层)
用户通过拖拽组件、配置属性实现应用搭建:
- 页面设计:拖拽组件到画布,调整布局、样式(如颜色、尺寸);
- 逻辑配置:通过可视化流程图(如条件判断、循环)或表单配置,定义组件交互(如点击按钮提交表单)、数据流转(如从接口获取数据渲染表格);
- 权限与规则配置:设置用户角色、访问权限、数据校验规则等。
3. 自动化代码生成与运行(底层执行层)
用户配置完成后,平台自动完成:
- 代码生成:将可视化配置转换为可执行代码(如 Vue/React 前端代码、Node.js 后端接口、SQL 脚本);
- 编译部署:自动编译代码,部署到服务器 / 云环境(支持一键发布);
- 运行时解析:部分平台不直接生成源码,而是运行时解析配置文件(如 JSON),动态渲染页面和执行逻辑(牺牲部分灵活性换效率)。
核心特点
- 无需 / 少量手写代码,依赖 “拖拽 + 配置”;
- 复用通用模块,降低开发门槛;
- 支持快速迭代、一键部署,适合中小应用或原型开发。
简单说,低代码平台是 “预制乐高积木(组件 / 逻辑)+ 可视化拼搭(拖拽配置)+ 自动组装成型(代码生成 / 运行)”,核心是通过标准化和自动化,缩短应用开发周期。
service worker和强缓存相比,有哪些优势?
强缓存是 “被动式、简单化” 的缓存方案,适合基础静态资源优化(追求高性能、无需频繁更新);
Service Worker 是 “主动式、可编程” 的缓存方案,核心优势是离线可用、缓存可控、动态更新,适合需要提升用户体验(如离线访问、无感更新)或复杂缓存逻辑的场景(如 PWA 应用、高频更新的动态页面)。
bind有什么用?连续多个bind,最后this指向是什么?
- 固定
this指向:避免函数调用时this丢失(如回调函数、事件处理),并返回一个新函数(不会立即执行) - 预设参数:可提前传入部分参数(柯里化),新函数调用时补充剩余参数。
- 连续
bind时,this以第一次绑定为准,后续bind无法覆盖。
Promise 值穿透
Promise “值穿透” 是指Promise 链式调用中,若中间某步传入非函数(如 null/ 普通值),会跳过该步并直接传递上一步的结果,本质是 then/catch 对参数的容错处理。
核心表现
then(onFulfilled, onRejected)或catch(onRejected)中,若传入的不是函数,会被忽略,直接将结果传递给下一个处理函数;- 比如
Promise.resolve(1).then(null).then(res => console.log(res)),最终输出1(null被忽略,值直接穿透到下一步)。
本质原因
Promise 规范规定:若 then 的回调不是函数,则视为 “透传”,等价于 then(v => v)(成功态)或 catch(e => Promise.reject(e))(失败态)。
总结
值穿透是 Promise 的容错机制,确保链式调用中即使传入非函数参数,也不会中断流程,而是直接传递结果。
单点登录
单点登录(SSO)的核心是 “一处登录,多处访问”,通过统一认证中心管理用户身份,实现多系统间的身份互通,核心实现步骤如下:
1. 统一认证中心(核心)
搭建独立的认证服务器(如 OAuth2.0/OpenID Connect 服务),负责用户登录验证、颁发身份凭证(如 Token)。
2. 身份凭证传递
- 用户在认证中心登录后,生成加密的身份凭证(如 JWT 令牌、票据 Ticket);
- 其他系统访问时,跳转至认证中心验证凭证,确认有效后允许访问。
3. 跨域 / 跨系统验证
- Cookie+Session:认证中心登录后写入 Cookie,子系统通过 Cookie 向认证中心校验(需同根域名);
- Token 机制:认证中心颁发 Token,子系统携带 Token 请求认证中心验证有效性;
- 票据机制(如 CAS) :子系统跳转认证中心获取票据,再用票据向认证中心换身份信息。
4. 会话同步
用户登出时,认证中心通知所有子系统销毁会话,实现全局登出。
核心原理
通过统一的身份凭证和认证流程,让多系统共享用户身份状态,避免重复登录,本质是 “集中式身份验证 + 分布式凭证校验”。
多个项目共用组件时,如何保证版本一致性?
多个项目共用组件时保证版本一致性,核心是统一组件依赖管理 + 版本管控机制,具体方案如下:
1. 抽离为独立 npm 包
将共用组件封装成私有 / 公共 npm 包,所有项目通过安装该包引入组件,通过锁定版本号(如package.json中写死1.2.3而非^1.2.3)或使用package-lock.json/yarn.lock固定依赖版本,确保所有项目使用同一版本。
2. 使用 Monorepo 管理
通过pnpm workspaces、lerna等工具将多个项目和共用组件放在同一仓库,组件作为内部包被项目依赖,利用工作区机制强制项目使用本地组件最新版本(或指定版本),避免版本不一致。
3. 建立版本发布规范
遵循语义化版本(SemVer),组件更新时明确标注版本变更(如patch修复、minor新增功能、major Breaking Change),并通过 Changelog 同步变更内容,项目升级时按需选择版本,避免跨版本差异。
4. 依赖锁定工具
使用npm ci(替代npm install)或yarn install --frozen-lockfile,强制以lock文件中的版本安装依赖,防止因依赖解析规则差异导致版本不一致。
5. 组件库文档与版本校验
在组件库文档中标注各版本特性,项目中可通过脚本校验组件版本是否符合要求(如低于指定版本则报错),或在 CI/CD 流程中加入版本一致性检查。
简言之,核心是 “统一依赖源 + 版本锁定 + 规范管控”,通过工具和流程确保所有项目使用相同版本的共用组件。
虚拟滚动如何动态计算可视区域?无限滚动如何结合Intersection Observer优化性能?
如果一个功能既可以使用小程序实现,也可以使用H5实现,你会如何选择?
用户体验
如果功能对加载速度、动画流畅度、系统权限调用(如微信支付、扫一扫、蓝牙、地理位置等)有较高要求,且需要提供接近原生应用的体验,那么小程序会是更优的选择。小程序的预加载、离线缓存机制以及更接近原生渲染的能力,通常能带来更流畅的用户体验。例如,如果是一个需要频繁使用相机或地图的服务,小程序的原生能力接入会更加便捷和高效。
开发成本与周期
无需平台审核,修改后可即时上线。如果功能需求相对简单,对性能和原生能力要求不高,且追求快速迭代和低成本,H5会更有优势。例如,一个简单的营销活动页或者信息展示页面,H5就能很好地满足需求。
推广与分发能力
小程序依托于微信、支付宝等超级App的巨大用户基础,可以通过扫码、分享、搜索等多种方式快速触达用户,获客成本相对较低,传播效率高。而H5主要通过URL链接传播,在某些场景下推广效率可能不如小程序。如果功能需要借助社交裂变或快速分享来获取用户,小程序会更具优势。
功能和权限限制
小程序有严格的包体大小限制和平台审核机制,同时不能直接操作DOM,这在某些复杂的前端场景下可能会带来限制。而H5则没有这些限制,可以承载更复杂、更大的应用。但另一方面,小程序的统一规范和组件化开发模式,在一定程度上也降低了长期维护的复杂度。
综上所述,如果功能偏向于工具性、服务性,需要深度整合系统能力、追求极致用户体验和便捷的分享传播,我会倾向于选择小程序。
如果功能更偏向于内容展示、营销活动,对性能要求不高,且更看重开发灵活性、低成本和快速迭代,那么H5会是更合适的选择。最终的决策是基于业务需求、目标用户、资源投入和长期发展策略的综合权衡。
小程序的架构原理
1.1 原理说明
-
宿主环境与运行机制: 小程序并非运行在浏览器中,而是运行在微信、支付宝等宿主App的内置环境中。这个环境提供了一套沙箱机制,隔离了小程序与原生系统,确保安全和稳定。宿主App会内置一个JS引擎(如微信小程序的JSCore)来运行小程序的逻辑层,以及一个渲染层(如WebView)来渲染视图。
-
双线程架构: 这是小程序的核心架构特点。
- 逻辑层(App Service) : 运行在独立的JS线程中,负责处理业务逻辑、数据请求。开发者编写的JavaScript代码在这里运行。逻辑层不具备渲染能力,不能直接操作DOM。
- 视图层(View) : 运行在独立的渲染线程中,负责UI的渲染。它根据WXML描述文件和WXSS样式文件来绘制页面。视图层可以是一个WebView,也可以是更轻量的原生渲染组件。
-
数据通信与渲染: 逻辑层和视图层之间无法直接通信,它们通过一套统一的JSBridge进行消息传递。
- 数据下发: 当逻辑层数据发生变化(如
setData),会将数据通过JSBridge序列化后发送给视图层。视图层接收到数据后,进行Diff算法比较,只更新需要变化的部分,然后渲染到屏幕。 - 事件上报: 视图层的用户交互事件(如点击、滑动)会通过JSBridge传递给逻辑层,由逻辑层进行处理。
- 数据下发: 当逻辑层数据发生变化(如
-
组件化: 小程序提供了丰富的内置组件(
<view>,<text>,<image>等),这些组件是原生组件的封装,性能更好。开发者也可以基于这些基础组件进行自定义组件开发,实现代码复用和模块化。 -
生命周期管理: 小程序提供了应用生命周期(
onLaunch,onShow,onHide)和页面生命周期(onLoad,onShow,onReady,onHide,onUnload),开发者可以利用这些生命周期钩子进行业务逻辑的控制。 -
虚拟DOM (非严格意义) : 虽然小程序没有浏览器的完整DOM树,但在数据更新时,宿主环境会进行类似Virtual DOM的Diff算法,对比新旧数据,最小化地更新视图,以提高渲染效率。
1.2 核心用法 + 示例代码
-
数据更新流程示例: 当调用
this.setData时,逻辑层会收集待更新的数据,将其序列化并通过JSBridge发送到视图层。视图层收到数据后,会与当前数据进行比较,找出差异,然后只对有差异的视图节点进行更新。 -
这种双线程架构和通信机制保证了小程序的运行效率和安全性,避免了前端H5页面常见的性能瓶颈和安全风险。
1.3 常见误区或面试陷阱
-
陷阱: 不清楚JSBridge的具体作用和通信方式。
JSBridge是逻辑层和视图层之间的桥梁,负责序列化和反序列化数据,实现跨线程通信。 -
陷阱: 忽略了小程序安全性考量。
双线程隔离和平台审核机制,都旨在提高小程序应用的安全性。 -
误区: 认为小程序不能操作DOM是因为技术实现不足。实际上,这是平台为了实现高性能、高安全和统一体验而做出的设计权衡。
-
陷阱: 不清楚
setData的异步性。setData是一个异步操作,修改data后,视图的更新并不会立即完成。 -
陷阱: 将小程序与React/Vue等框架的"虚拟DOM"混为一谈。虽然都有数据驱动和Diff更新,但底层机制和运行环境不同。小程序是在Native层进行视图更新,而Web框架是在浏览器DOM上进行模拟更新。
使用rpx进行尺寸适配:
- 设计稿如果以iPhone 6/7/8为基准(物理宽度375px,DPR=2,屏幕逻辑像素宽度为750rpx),那么在设计稿中量取的1px,在小程序中可以直接写成2rpx。例如,如果设计稿中有一个元素宽度是100px,那么在WXSS中就可以写成
width: 200rpx;。
小程序无法直接操作DOM,是其核心架构设计决定的
-
双线程模型: 小程序的逻辑层(JavaScript运行环境)和视图层(渲染环境,如WebView)是分离的,运行在不同的线程中。JavaScript在逻辑层中执行,而DOM存在于视图层。如果允许逻辑层直接操作DOM,会涉及到跨线程操作,这会带来复杂的同步问题和巨大的性能开销。
-
性能考量: 直接操作DOM是前端性能瓶颈之一。小程序通过数据驱动的方式,将数据变化发送给视图层,视图层再进行高效的局部更新(通常会进行Diff计算),从而避免了频繁的、低效的DOM操作,提升了渲染性能和用户体验。
-
安全与管控: 宿主环境(如微信)对小程序有严格的沙箱限制和安全管理。如果开发者可以随意操作DOM,可能引入XSS攻击等安全漏洞,或者进行一些不符合平台规范的操作。限制DOM操作有助于平台对小程序进行统一的管控和优化。
-
统一的用户体验: 为了保证小程序在不同设备和环境下都能提供一致且流畅的用户体验,平台对UI渲染和交互进行了封装和优化。直接操作DOM会打破这种统一性,增加适配成本,也可能导致用户体验的差异。
小程序为何禁止部分css选择器
-
禁用或受限的选择器示例:
- 全局选择器
*: WXSS不支持,防止全局污染。 - 属性选择器
[attr]: 一般不支持,或支持有限。 - 父子选择器
>: 支持,但使用时要注意作用域。 - 兄弟选择器
+、~: 通常不支持。 - 部分伪类和伪元素: 例如,
:before,:after等伪元素是不支持的,因为小程序没有完全的DOM概念。
- 全局选择器
1.3 常见误区或面试陷阱
- 误区: 认为禁用选择器是小程序"功能不足"的表现。实际上,这是为了性能、安全和组件化而做的策略性限制。
- 陷阱: 尝试使用被禁用的CSS选择器,导致样式不生效或报错。
- 陷阱: 不了解小程序组件的样式隔离特性(默认隔离),导致样式互相影响。
答题要点
- 安全性: 理解禁用选择器是为了防止恶意代码注入或样式污染。
- 性能优化: 掌握限制选择器复杂度,以提高样式解析和渲染效率。
- 组件化封装: 了解如何通过限制选择器来更好地支持组件的独立性和复用性。
- 开发规范: 熟悉小程序对样式的限制,以规范开发行为。
频繁调用setData数据的卡顿
-
频繁调用
setData是小程序性能卡顿的常见原因,这主要与小程序的双线程架构和数据通信机制紧密相关。-
双线程通信开销: 小程序的逻辑层(JavaScript线程)和视图层(渲染线程,可能是WebView或原生渲染)是分离的。当逻辑层通过
setData更新数据时,这些数据需要经过序列化,然后通过JSBridge跨线程传输到视图层。视图层接收到数据后,需要进行反序列化,并与旧数据进行Diff比较,找出变化的部分,最后再通知渲染引擎进行更新。这个过程涉及数据的序列化/反序列化、跨线程通信以及Diff计算,每一步都有一定的性能开销。如果频繁调用setData,就会频繁触发这些操作,导致性能损耗累积。 -
渲染层重绘/重排: 视图层在接收到数据变化后,会根据Diff结果进行视图更新。如果数据变化涉及大量的DOM节点(或小程序组件树节点),即使是局部更新,也可能导致视图层的大范围重绘(Repaint)甚至重排(Reflow),这些操作都是非常耗费性能的。
-
不必要的Diff计算: 即使数据没有实际变化,只要调用了
setData,小程序内部依然会触发数据比较和潜在的渲染更新流程。无效的setData调用会白白消耗性能。
-
1.2 核心用法 + 示例代码
-
优化策略:
-
合并
setData调用: 将多次对不同数据或同一数据不同部分的setData调用合并为一次。例如,不要在循环中频繁调用setData,而是在循环结束后一次性更新。 -
减少
setData的数据量: 只更新需要变化的数据,避免传输整个大的data对象。如果只是修改data对象中某个深层属性,只传递该属性的路径和新值。 -
节流防抖: 对于用户频繁触发的事件(如滚动、输入),使用节流(throttle)或防抖(debounce)来减少
setData的调用频率。 -
使用数据绑定优化: 避免在
wxml中进行复杂计算,将计算结果提前在js中处理好并绑定到data。 -
利用自定义组件的数据监听器: 对于组件内部的数据更新,可以考虑使用自定义组件的
observers来监听数据变化,只在必要时进行组件内部的渲染更新。
-
在移动端开发中,如何判断用户使用的是安卓、iOS还是微信平台
- 微信客户端和微信小程序 User-Agent 不一样
- 微信小程序推荐使用
wx.getSystemInfoSync()获取平台信息 - Safari 浏览器可能会模拟 iOS 标识,注意与真实设备区别
2.3 常见误区或陷阱
- ❌ 将所有 iOS 判断仅用
iPhone,可能漏掉 iPad - ❌ 忽略微信内嵌 WebView 的兼容性差异
- ❌ 不做前缀判断,可能误匹配(如 UC 浏览器也带 Android)
- ❌ 忽视 UA 会被劫持/修改,不能作为安全验证手段
function detectPlatform() {
const ua = navigator.userAgent;
return {
isAndroid: /Android/i.test(ua),
isIOS: /iPhone|iPad|iPod/i.test(ua),
isWeChat: /MicroMessenger/i.test(ua),
isWeCom: /wxwork/i.test(ua), // 判断是否企业微信
isMiniProgram: /miniprogram/i.test(ua), // 微信小程序环境
};
}
const platform = detectPlatform();
console.log(platform.isAndroid, platform.isIOS, platform.isWeChat);
进程和线程
一、考察点
- 理解进程与线程的基本定义和区别
- 掌握进程和线程的工作机制及资源管理差异
- 理解多进程、多线程编程模型及应用场景
- 理解进程与线程的调度和通信方式
二、参考答案
2.1 进程的概念
- 进程是操作系统分配资源的基本单位,是正在运行的程序实例
- 拥有独立的地址空间、内存、文件句柄等资源
- 一个操作系统可以同时运行多个进程,彼此间内存独立
- 进程之间通信复杂,一般通过进程间通信(IPC)机制实现
2.2 线程的概念
- 线程是进程内的执行单位,是程序执行的最小调度单位
- 同一进程内的多个线程共享进程资源(内存空间、文件句柄等)
- 线程之间切换开销较小,适合并发执行任务
- 多线程提高程序执行效率,但需注意同步和资源竞争问题
2.3 进程与线程的主要区别
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 拥有独立资源(内存空间等) | 共享进程资源 |
| 调度单位 | 操作系统调度的基本单位 | 线程调度的基本单位 |
| 通信方式 | 进程间通信(IPC),复杂且开销大 | 线程间通信直接共享内存,速度快 |
| 创建销毁开销 | 较大 | 较小 |
| 稳定性 | 进程崩溃不影响其他进程 | 一个线程异常可能影响整个进程 |
2.4 应用场景
- 多进程适用于资源隔离、安全性高的场景,如 Web 服务器多进程架构
- 多线程适合计算密集型或 IO 并发任务,提高执行效率
- 现代系统常结合使用多进程和多线程实现高效、稳定的并发
三、常见误区或面试陷阱
- ❌ 混淆进程和线程的资源隔离和共享机制
- ❌ 忽视线程安全问题,误认为线程间无竞争
- ❌ 认为多线程一定比多进程更高效,忽略上下文切换开销
- ❌ 不理解进程和线程的调度开销差异,错误设计并发方案
答题要点
- 进程是资源分配的独立单位,线程是执行的基本单位
- 线程共享进程资源,进程资源相互隔离
- 多线程切换开销小,适合轻量级并发
- 多进程提供更好隔离性和稳定性
- 并发编程中需根据实际需求权衡使用进程或线程
一个进程可能会有几个栈和几个堆
一、考察点
- 理解进程内存结构中堆和栈的定义
- 掌握堆和栈的数量及其对应关系
- 了解多线程环境下堆和栈的变化
- 理解堆和栈的作用及管理机制
二、参考答案
2.1 堆(Heap)的数量
- 一个进程通常只有 一个堆
- 堆是进程内存中用于动态分配内存的区域
- 所有线程共享同一个堆,线程可以动态分配和释放内存
- 堆上的内存管理由程序或语言运行时负责,如
malloc、new
2.2 栈(Stack)的数量
- 一个进程中,
每个线程都有自己的独立栈 - 栈用于存储函数调用时的局部变量、参数、返回地址等
- 因为每个线程执行独立的调用栈,所以每个线程都有一个独立的栈空间
- 主线程有主栈,子线程各自拥有各自的栈
2.3 其他补充
- 虽然堆是共享的,但多线程访问堆时需做好同步,防止竞态条件
- 栈大小一般固定,由系统或线程创建时分配
- 线程的栈空间相对较小,堆空间可以动态增长
三、常见误区或面试陷阱
- ❌ 认为每个线程也有独立的堆(实际上堆是共享的)
- ❌ 误以为进程只有一个栈(多线程时栈的数量等于线程数)
- ❌ 忽略多线程访问堆的同步问题
- ❌ 混淆栈和堆的作用及生命周期
答题要点
- 一个进程通常只有一个堆,供所有线程共享使用
- 每个线程都有独立的栈,用于存储局部变量和函数调用信息
- 堆负责动态内存分配,栈负责函数调用管理
- 多线程程序中堆是共享资源,栈是线程私有资源
- 理解堆和栈的区别及数量,有助于设计安全高效的并发程序
为什么会有堆内存和栈内存的区分?他们各自有什么特点?
一、考察点
- 理解堆内存和栈内存设计的初衷与区别
- 掌握堆和栈的内存管理方式及生命周期
- 了解两者的性能差异和使用场景
- 理解程序执行时堆栈协作的机制
二、参考答案
2.1 为什么区分堆和栈?
- 目的在于优化内存管理与程序执行效率
- 栈内存结构简单,分配释放快,适合存储局部变量和函数调用信息
- 堆内存灵活,支持动态分配和释放,适合存储生命周期不确定的数据
- 这种区分帮助操作系统和运行时高效管理内存资源,保证程序稳定和高效执行
2.2 栈内存的特点
- 内存分配方式:连续且自动,系统通过调整栈指针完成分配和释放
- 生命周期:函数调用开始时分配,结束时自动释放
- 访问速度:极快,CPU 直接支持栈操作
- 大小限制:较小,固定大小(通常几 MB),栈溢出会导致程序崩溃
- 用途:存储函数参数、局部变量、返回地址等临时数据
- 线程私有:每个线程有独立栈,互不影响
2.3 堆内存的特点
- 内存分配方式:动态分配,程序员或运行时手动管理(如 malloc/free 或 GC)
- 生命周期:灵活,可跨函数调用,直到显式释放或垃圾回收
- 访问速度:比栈慢,因需维护内存分配数据结构,存在碎片化问题
- 大小限制:较大,受限于系统内存和进程地址空间
- 用途:存储对象、动态数据结构、全局或共享数据等
- 线程共享:堆是所有线程共享的内存区域,需要同步机制保证安全
2.4 区分的意义总结
- 栈适合频繁分配、生命周期短、大小确定的数据,保证高效执行
- 堆适合生命周期不确定、大小动态变化的数据,提供灵活性
- 两者协同工作,使程序既高效又灵活,满足不同需求
三、常见误区或面试陷阱
- ❌ 认为堆内存比栈内存“更好”,忽视其分配开销和管理复杂性
- ❌ 混淆栈和堆的生命周期和访问权限
- ❌ 忽略栈大小有限制,导致栈溢出问题
- ❌ 忽视多线程环境下堆内存的同步和安全问题
答题要点
- 栈和堆内存设计目的是优化内存分配和程序性能
- 栈内存自动管理,速度快但空间有限,适合局部数据
- 堆内存动态管理,灵活但分配和释放开销较大
- 两者协同保证程序运行的高效和灵活
- 理解堆栈区分,有助于合理设计程序和避免内存错误
JavaScript是单线程语言,这个特点有什么好处?为什么设计成单线程的
一、考察点
- 理解 JavaScript 单线程模型的含义
- 掌握单线程设计带来的优势及限制
- 理解事件循环(Event Loop)与异步机制如何配合单线程
- 认识单线程设计背后的历史和实际应用考量
二、参考答案
2.1 JavaScript 单线程的含义
- JavaScript 运行在单个主线程上,意味着同一时刻只执行一个任务
- 所有代码执行、事件处理、UI 渲染等都在这个线程内顺序进行
- 避免了多线程环境下复杂的线程同步和竞争问题
2.2 单线程的好处
-
简化并发控制
- 无需显式锁、死锁等多线程问题,降低程序复杂度
-
避免数据竞争和状态不一致
- 由于只有一个执行线程,访问共享数据时无需担心竞态条件
-
一致的执行顺序
- 代码执行按顺序进行,易于理解和调试
-
适合浏览器环境设计
- 浏览器需要同时处理 UI 渲染和 JS 执行,单线程避免渲染阻塞和状态混乱
2.3 为什么设计成单线程
-
历史原因
- JavaScript 最初设计用于网页浏览器,主要处理用户交互和DOM操作
- 浏览器环境中,UI 渲染必须同步,线程切换和并发会带来巨大复杂性
-
保证执行环境安全与简洁
- 单线程避免了复杂的同步机制,提高性能和稳定性
-
通过事件循环和异步机制补充
- 虽然是单线程,JS 通过 事件循环(Event Loop) 实现异步任务和并发效果
- 使得单线程下仍能高效处理 IO、定时器、网络请求等任务
2.4 现实中的应用和演变
- 现代浏览器引入 Web Worker 支持多线程处理,但 JS 主线程仍保持单线程
- 单线程设计保证了主线程的安全和 UI 的流畅
- 异步机制和回调函数、Promise、async/await 等使得编程更灵活
三、常见误区或面试陷阱
- ❌ 误以为单线程意味着 JS 只能同步执行,忽视异步机制
- ❌ 认为单线程不能实现并发或异步操作
- ❌ 忽略事件循环机制对单线程性能和并发的支撑作用
- ❌ 混淆主线程与浏览器底层多线程(如渲染线程、网络线程)
答题要点
- JavaScript 运行在单线程环境,保证执行顺序和状态一致性
- 单线程简化并发控制,避免复杂的同步和竞态问题
- 设计初衷是适应浏览器环境需求,保证 UI 渲染和脚本执行协调
- 事件循环和异步机制弥补了单线程的局限,提升性能和体验
- 现代环境通过Web Worker 等支持多线程,但核心执行仍单线程
Node.js相比其他服务端语言有什么优势
一、考察点
- 理解 Node.js 的架构特点及设计理念
- 掌握 Node.js 在性能、扩展性和开发效率方面的优势
- 了解 Node.js 与传统服务端语言(如 Java、PHP、Python 等)的区别
- 掌握 Node.js 适用的典型应用场景
二、参考答案
2.1 单线程事件驱动架构
- Node.js 基于单线程事件循环(Event Loop),采用异步非阻塞 I/O 模型
- 能高效处理大量并发请求,避免线程切换和资源消耗
- 适合 I/O 密集型应用(网络请求、文件操作等),性能表现优异
2.2 JavaScript 全栈统一语言
- 前端和后端均使用 JavaScript,降低团队学习成本
- 代码复用性高,前后端共享部分逻辑(如校验、数据结构)
2.3 丰富的生态系统和模块化
- npm 拥有海量开源包,覆盖各种功能需求
- 模块化设计方便组件拆分和复用
- 支持微服务、Serverless 等现代架构模式
2.4 高扩展性与灵活性
- 支持事件驱动、高并发场景,适合实时应用(聊天、推送等)
- 灵活的异步编程模型(Promise、async/await)易于编写高效代码
- 支持多进程/集群模式,解决单线程瓶颈
2.5 快速启动和轻量级
- 启动快,资源占用低
- 适合构建轻量级服务和微服务架构
- 易于部署和运维
三、常见误区或面试陷阱
- ❌ 误认为 Node.js 适合所有场景,忽视 CPU 密集型任务瓶颈
- ❌ 忽略单线程可能导致阻塞的问题,需合理使用异步和多进程
- ❌ 认为 JavaScript 不适合后端开发,忽略其生态和性能优势
- ❌ 忽视 Node.js 在安全和稳定性方面的最佳实践
答题要点
- Node.js 采用单线程异步非阻塞 I/O,适合高并发 I/O 密集型应用
- 统一使用 JavaScript,提升开发效率和团队协作
- 拥有丰富生态系统和灵活模块化设计,支持现代架构
- 启动快、资源占用低,适合轻量级微服务
- 需要注意单线程瓶颈和异步编程技巧,合理设计应用结构
闭包
1. 外部函数调用 10 次产生的闭包数量
会产生 10 个闭包。因为外部函数每次调用都会创建独立的执行上下文,内部函数会捕获当前执行上下文中的变量,每次调用都会生成一个独立的闭包(变量作用域相互隔离)。
2. 修改一个闭包的变量是否影响其他闭包
不会影响。每个闭包捕获的是外部函数每次调用时创建的独立变量副本(不同执行上下文的变量相互独立),修改其中一个闭包的变量,仅影响该闭包对应的作用域,与其他闭包无关。
3. 生成闭包的其他写法
除了 “外部函数返回内部函数”,还有以下常见写法:
-
内部函数作为参数传递给外部函数:
function outer() { let num = 10; function inner() { console.log(num); } // 内部函数作为参数传给其他函数 otherFn(inner); } function otherFn(fn) { fn(); } // 执行时形成闭包 -
内部函数赋值给全局变量:
let innerFn; function outer() { let num = 20; innerFn = function() { console.log(num); }; // 内部函数被全局引用 } outer(); innerFn(); // 执行时形成闭包 -
立即执行函数(IIFE)结合内部函数:
const closureFn = (function() { let num = 30; return function() { console.log(num); }; })(); // IIFE执行后返回内部函数,形成闭包
递归的定义是什么?它通常用在哪些场景
递归的定义
递归是一种编程思想,指函数直接或间接调用自身的过程,通过将复杂问题拆解为与原问题结构相似的子问题,逐步简化问题直至达到可直接解决的 “终止条件”(避免无限循环)。
核心要素:
- 递归调用:函数自身调用;
- 终止条件:当子问题简化到某个程度时,直接返回结果,不再递归。
常见应用场景
-
数学问题:
- 阶乘计算(
n! = n × (n-1)!)、斐波那契数列(F(n) = F(n-1) + F(n-2)); - 数论中的质数判断、最大公约数求解(欧几里得算法)。
- 阶乘计算(
-
数据结构操作:
- 树形结构遍历(二叉树的前 / 中 / 后序遍历、深度优先搜索 DFS);
- 链表相关操作(如反转链表、求链表深度)。
-
算法与逻辑处理:
- 回溯算法(如全排列、子集问题、迷宫求解);
- 分治算法(如快速排序、归并排序的递归实现);
- 嵌套结构解析(如 JSON 数据解析、多层级菜单处理)。
-
实际业务场景:
- 多级评论 / 回复展示(递归遍历嵌套的评论结构);
- 文件夹目录遍历(递归访问子文件夹)。
递归的优势是代码简洁、逻辑清晰,适合处理具有 “自相似” 结构的问题;但需注意控制递归深度,避免栈溢出(可通过尾递归优化或转为迭代实现改进)。
浏览器有哪几种缓存,各种缓存的优先级是什么样的?
-
Service Worker 缓存:由于其可以完全控制网络请求,因此具有最高的优先级,即使是强制缓存也可以被它所覆盖。
-
强制缓存:如果存在强制缓存,并且缓存没有过期,则直接使用缓存,不需要向服务器发送请求。
-
协商缓存:如果强制缓存未命中,但协商缓存可用,则会向服务器发送条件请求,询问资源是否更新。如果服务器返回 304 Not Modified 响应,则直接使用缓存。
-
Web Storage 缓存:Web Storage 缓存的优先级最低,只有在网络不可用或者其他缓存都未命中时才会生效。
若要求正方形内嵌图片保持比例且不拉伸,如何实现
一、考察点
- 理解图片的默认渲染行为及尺寸适配
- 掌握 CSS 中图片等媒体对象的缩放控制属性 (object-fit)
- 熟悉容器布局,确保图片在固定宽高正方形内自适应显示
- 了解响应式设计和兼容性考虑
二、参考答案
2.1 核心概念说明
- 图片默认行为:图片按原始宽高比显示,若强制设置宽高可能导致变形
object-fit属性:用于控制替换元素(如 img、video)内容的填充方式object-fit: contain保持图片比例,完整显示,可能留空白object-fit: cover保持比例,填满容器,超出部分裁剪- 结合容器尺寸和
object-fit可灵活控制图片展示效果
2.2 具体实现示例
<div class="square">
<img src="your-image.jpg" alt="示例图片" />
</div>
<style>
.square {
width: 50vw;
padding-bottom: 50vw; /* 自适应正方形 */
position: relative;
overflow: hidden; /* 防止图片溢出 */
}
.square img {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
object-fit: contain; /* 保持比例且不拉伸 */
transform: translate(-50%, -50%);
}
</style>
2.3 说明
- 使用
padding-bottom创建正方形容器 position: relative+position: absolute定位图片,方便居中- 图片宽高设为 100%,配合
object-fit: contain保持原始比例且不被拉伸 overflow: hidden防止图片超出容器边界显示- 可根据需求改为
object-fit: cover实现填充并裁剪效果
三、常见误区或面试陷阱
- 不设置
object-fit,直接用width和height100%,图片可能被拉伸变形 - 忽略容器定位,导致图片无法居中或布局错乱
- 未设置
overflow: hidden,图片溢出影响页面布局 - 误用
background-image和background-size,与 img 标签区别不清
答题要点
- 容器用 padding-bottom 创建自适应正方形
- 图片使用
object-fit: contain保持比例且不拉伸 - 绝对定位图片居中显示,宽高100%填充容器
- 设置
overflow: hidden避免溢出
对象不可变性实现,对比三种方案:
一、考察点
- 理解前端实现对象不可变性的不同方案
- 掌握 Object.freeze、Proxy 和 Object.defineProperty 的特点及限制
- 分析三种方案在深度冻结、性能、使用体验上的差异
- 理解不可变对象在状态管理、函数式编程中的应用价值
二、参考答案
2.1 方案介绍及原理
方案1:Object.freeze
- 原理:冻结对象,使其属性不可新增、删除、修改
- 对象变为不可扩展,属性变为不可写不可配置
- 注意:只冻结一层(浅冻结),嵌套对象仍可修改
示例:
const obj1 = Object.freeze({ a: 1 });
obj1.a = 2; // 无效,严格模式下会报错
方案2:Proxy
- 原理:通过代理对象拦截操作,自定义行为,如禁止修改
- 可以深度拦截(结合递归代理实现深度不可变)
- 灵活性高,可定制化错误提示或日志
- 性能相对较高,但部分老浏览器不支持
示例:
const obj2 = new Proxy({ a: 1 }, {
set(target, prop, value) {
console.warn(`禁止修改属性${prop}`);
return false; // 拦截写操作
}
});
obj2.a = 2; // 拦截失败,无法修改
方案3:Object.defineProperty
- 原理:将属性设置为不可写(writable: false),禁止修改该属性
- 只能逐个属性设置,不方便批量操作
- 同样是浅层限制
示例:
const obj3 = {};
Object.defineProperty(obj3, "a", {
value: 1, writable: false, configurable: false
});
obj3.a = 2; // 无效,无法修改
2.3 适用场景及选择建议
- Object.freeze 简单冻结浅层对象,防止误修改,适合小规模数据冻结
- Proxy 复杂场景需深度不可变,需捕获所有修改操作,推荐使用,适合现代环境
- Object.defineProperty 需对部分关键属性单独保护时使用,适合局部控制,不适合整体冻结
三、常见误区或面试陷阱
- 误以为 Object.freeze 是深冻结(需手动递归冻结)
- 忽略 Proxy 兼容性问题,盲目使用导致兼容性缺陷
- 认为 Object.defineProperty 可以阻止所有修改(无法阻止属性新增和删除)
- 低估 Proxy 对性能的潜在影响
答题要点
- Object.freeze:浅冻结,禁止新增、删除、修改属性
- Proxy:灵活拦截,支持深度不可变,实现复杂业务需求
- Object.defineProperty:逐属性控制,可设为不可写,适合部分属性保护
- 根据需求和兼容性权衡选择方案
为什么WeakMap的键必须是对象
一、考察点
- 理解 WeakMap 的设计初衷和底层实现机制
- 掌握 WeakMap 与普通 Map 的区别
- 了解 JavaScript 垃圾回收机制与弱引用的关系
- 能说明 WeakMap 键限制为对象的技术原因及应用场景
二、参考答案
1.1 WeakMap 的基本概念
- WeakMap 是一种键为对象,值任意的数据结构
- 与普通 Map 不同,WeakMap 对键对象的引用是“弱引用”
- 当键对象不存在其它引用时,能被垃圾回收器回收,避免内存泄漏
1.2 键必须是对象的原因
1)弱引用必须指向对象
- JavaScript 的弱引用机制只能对对象类型生效
- 基本类型(如字符串、数字)是值类型,无法被弱引用追踪和回收
2)垃圾回收机制要求
- 只有对象才存在垃圾回收标记和生命周期管理
- WeakMap 通过弱引用允许键对象在无外部引用时自动回收
- 如果允许基本类型作为键,则无法做到弱引用,违背 WeakMap 设计初衷
3)设计意图和使用场景
- 用于存储与对象相关联的私有数据,避免内存泄漏
- 例如框架中保存某个 DOM 节点的元数据,DOM 节点销毁时对应数据自动释放
1.3 WeakMap 与 Map 的区别
| 特性 | Map | WeakMap |
|---|---|---|
| 键类型 | 任意类型 | 只能是对象 |
| 是否可枚举 | 可遍历所有键值 | 不可遍历 |
| 引用强度 | 对键保持强引用,阻止垃圾回收 | 对键保持弱引用,不阻止垃圾回收 |
| 适用场景 | 普通键值映射 | 关联对象私有数据,自动回收 |
1.4 WeakMap 常见误区或面试陷阱
❌ 误区一:认为 WeakMap 可以用基本类型作为键
- 会导致运行时错误,WeakMap 只能接受对象作为键
❌ 误区二:误用 WeakMap 导致内存泄漏
- 忽略 WeakMap 不可遍历特性,误判数据存在与否
答题要点
- WeakMap 键必须是对象,因为弱引用机制只能作用于对象
- 对基本类型无法进行弱引用,无法参与垃圾回收机制
- WeakMap 设计用于保存对象私有数据,避免内存泄漏
- 与普通 Map 区别在于弱引用和不可枚举特性
1.5 Map常见误区或面试陷阱
❌ 误区二:忽略数字与字符串键的区别
'1'与1是不同的键,避免混淆
答题要点
- Map 支持所有基本类型和对象作为键
- 包括字符串、数字、布尔值、Symbol、null、undefined
- 与 WeakMap 只能用对象键不同,Map 更灵活
- 与普通对象不同,Map 不会将数字键转换为字符串类型,它保持键的原始类型
Symbol作为键可以保证唯一性,适用于需要唯一标识的场景
状态码302
302 Found(临时重定向)
- 表示客户端请求的资源临时被移动到另一个 URL,客户端应使用
Location响应头中的地址重新发起请求 - 适用于服务器临时重定向请求目标,例如跳转登录页、活动页等
- 浏览器会自动跳转到新地址,用户无感知
HTTP/1.1 302 Found
Location: https://example.com/new-url`
跨域场景下如何解决Cookie作用域问题
一、考察点
- 是否理解 Cookie 的作用域限制机制(Domain / Path / SameSite)
- 是否了解跨域请求时 Cookie 的携带条件
- 是否掌握前后端联调中解决跨域 Cookie 失效的实践方法
- 是否能结合真实场景说明如何设置、传递、验证 Cookie
二、参考答案
1.1 原理说明
✅ Cookie 的作用域控制
- Domain:决定 Cookie 可被哪些子域访问,默认仅当前域名可访问,可通过
Set-Cookie: Domain=xxx.com设置共享 - Path:限制 Cookie 作用的路径,默认当前路径及子路径生效
- SameSite:控制跨站请求时 Cookie 是否会自动发送:
| SameSite 属性 | 描述 |
|---|---|
Strict | 完全禁止跨站点发送 Cookie |
Lax | 部分允许(GET 导航类请求可携带) |
None | 允许跨站发送,但必须设置 Secure |
✅ 跨域请求 Cookie 不生效的原因
- 前端未设置
withCredentials: true - 服务端未返回
Access-Control-Allow-Credentials: true - 服务端未设置
Access-Control-Allow-Origin为具体源(不能是*) - Cookie 设置了 SameSite=Strict 或未指定导致默认限制
1.2 前后端联调中 Cookie 跨域解决方案
✅ 前端配置
fetch('https://api.example.com/userinfo', {
method: 'GET',
credentials: 'include' // 关键点:允许携带 Cookie
});
XMLHttpRequest同样需要设置xhr.withCredentials = true
✅ 服务端配置(以 Node.js/Express 为例)
res.setHeader('Access-Control-Allow-Origin', https://frontend.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
✅ Cookie 设置示例
Set-Cookie: token=abc123; Domain=.example.com; Path=/; SameSite=None; Secure
- 跨域必须使用:
SameSite=None; Secure - Secure 要求 HTTPS 协议才能生效
1.3 应用场景举例
场景一:子域共享登录态(SSO)
- 主域名:
example.com - 系统1:
admin.example.com - 系统2:
user.example.com
使用
Set-Cookie: token=...; Domain=.example.com可让两个子域共享登录态
场景二:前端在 localhost:3000,后端在 api.example.com
- 典型开发环境跨域情况
- 需设置:前端
withCredentials、后端Access-Control-Allow-Credentials: true - 后端设置 CORS
origin精确匹配,而非通配符
1.4 常见误区或面试陷阱
❌ 误区一:服务端已设置 Cookie,前端就能收到
- 如果跨域不配置
credentials: include+Allow-Credentials,浏览器不会保存 Cookie
❌ 误区二:CORS 中 Access-Control-Allow-Origin: * 能解决所有跨域问题
- 携带 Cookie 时不能使用
*,必须设置明确来源
❌ 误区三:SameSite=None 不生效
- 忽略了必须搭配
Secure,在非 HTTPS 环境中会被浏览器拒绝
答题要点
- Cookie 默认无法跨域,需通过配置 Domain、SameSite、Secure 控制作用域
- 跨域请求携带 Cookie:前端设置
credentials: include,服务端设置Access-Control-Allow-Credentials: true - Set-Cookie 需带上
SameSite=None; Secure才允许跨域传递 Access-Control-Allow-Origin不可为通配符- 常用于登录态共享、开发联调等跨源通信场景
git rebase与git merge的区别
答题要点
merge:保留历史,产生合并提交,适合多人协作rebase:线性历史,重写提交,适合个人开发、保持整洁rebase后可用--ff-only合并,避免 merge commit- 公共分支避免使用
rebase,以防历史冲突 - 冲突处理:rebase 更频繁,merge 集中发生
- 实际项目中常见模式:本地 rebase 整理,远程 merge 提交
为什么微任务在冒泡阶段立即执行而非进入任务队列
- 在事件回调中产生的微任务,会被加入微任务队列,并在当前宏任务执行完毕后立即执行要保证该宏任务内的所有同步代码和微任务都能连续执行完毕,防止中间被其他宏任务打断,保证状态一致。 如果微任务延迟到下一个宏任务队列,可能导致 UI 更新不及时或状态不一致
- 微任务是当前宏任务的“补充”,执行时机在宏任务结束后立即执行
- 事件冒泡阶段的回调属于当前宏任务的同步部分
- 事件回调内产生的微任务会在宏任务结束时立刻执行,保证状态和 UI 一致
- 这种设计保证了事件处理的原子性、性能和响应速度
如何定位到LCP(最大内容渲染)瓶颈
一、考察点
- 理解 LCP 指标的定义及重要性
- 掌握定位性能瓶颈的工具和方法
- 能够分析资源加载、渲染及网络等多维度原因
- 具备结合具体数据进行优化方向判断的能力
二、参考答案
1.1 LCP(Largest Contentful Paint)简介
- 表示页面主要内容(最大可见内容块)完成渲染的时间
- LCP 可能涉及图片、文本块、视频等元素的渲染
1.2 定位 LCP 瓶颈的具体步骤
1)使用性能分析工具抓取 LCP 时间点
-
Chrome DevTools Performance 面板
- 录制页面加载过程,查看 LCP 标记及对应渲染事件
-
Lighthouse
- 自动生成性能报告,定位慢资源、长任务
-
WebPageTest
- 真实网络环境下抓取加载数据,观察 LCP 相关指标
-
Google PageSpeed Insights
- 在线工具,给出 LCP 评分及瓶颈建议
2)分析 LCP 相关资源和任务
-
查找页面中被视为 LCP 元素的 DOM 节点(DevTools Performance → Summary → Largest Contentful Paint)
-
重点关注:
- 图片是否未优化、过大、未使用懒加载
- 关键字体是否阻塞渲染(字体加载时间长)
- 首屏 JS 和 CSS 资源加载是否缓慢或阻塞渲染
- 长任务(如大量 JS 执行)是否影响渲染
3)分析网络请求
- 查看关键资源(图片、CSS、JS)请求时间,检查是否存在请求阻塞、重定向、DNS 解析慢等问题
- 使用 Chrome DevTools Network 面板,关注“关键请求链”与“水平方向的请求阻塞”
- 分析是否存在多个关键请求串行执行,造成延迟
4)检查渲染阻塞因素
- CSS 及字体文件加载阻塞渲染,影响首次绘制及 LCP
- 大型 JS 任务阻塞主线程,延迟渲染
- DOM 复杂度高,布局计算耗时长
1.3 优化方向举例
- 图片压缩、合理尺寸、使用 WebP 格式、懒加载非首屏图片
- 关键 CSS 内联,减少阻塞加载
- 异步加载非关键 JS,避免阻塞主线程
- 使用字体显示策略(如
font-display: swap)避免字体阻塞 - 服务器开启压缩和缓存,加速资源传输
1.4 常见误区或面试陷阱
❌ 误区一:只关注单个资源,忽视整体加载流程
- LCP 受到多资源、多任务协同影响
❌ 误区二:误将 First Paint 或 FCP 当成 LCP
- LCP 关注的是最大可见内容渲染完成,区别明显
❌ 误区三:忽略网络环境影响,未在真实场景测试
- 实际用户网络状况对 LCP 有重大影响
答题要点
- 使用 Chrome DevTools Performance 面板定位 LCP 时间点及元素
- 重点分析图片、字体、CSS 和 JS 加载与执行时间
- 关注长任务阻塞和渲染阻塞因素
- 结合网络面板分析关键请求链
- 综合运用多种工具(Lighthouse、WebPageTest、PageSpeed Insights)获取数据支持
错误监控如何区分可恢复错误(如网络超时)与不可恢复错误(如前端代码崩溃)
一、考察点
- 理解不同类型错误的分类标准和业务影响
- 掌握前端错误捕获技术及其分类方法
- 能结合具体场景设计错误分级与处理策略
- 了解如何利用监控系统做错误报警和自动化响应
二、参考答案
1.1 错误分类定义
| 类型 | 特征描述 | 示例 | 处理策略 |
|---|---|---|---|
| 可恢复错误 | 影响局部功能或临时异常,用户操作可重试或继续使用 | 网络超时、接口返回异常 | 重试机制、降级处理、提示用户 |
| 不可恢复错误 | 导致应用整体功能崩溃或不可用 | JS 运行时异常、内存泄漏 | 错误上报、告警、快速修复 |
1.2 错误监控区分方法
1)基于错误类型区分
-
网络请求错误(Fetch/XHR 失败、超时)
- 可判断为可恢复错误
- 通过请求状态码、超时事件捕获
-
JS 运行时错误
- 通过
window.onerror或window.addEventListener('error')捕获 - 通常属于不可恢复错误
- 通过
-
Promise 未捕获异常
- 通过
window.onunhandledrejection捕获 - 多为不可恢复错误
- 通过
2)基于错误上下文信息
- 收集错误堆栈、请求参数、环境信息
- 判断错误影响范围(局部组件、全局应用)
- 结合业务判断是否可恢复
3)基于错误频率和影响度
- 高频错误且影响用户操作为不可恢复
- 偶发或短暂错误,且有重试机制为可恢复
1.3 具体实现策略
A. 网络错误处理
fetch(url, options)
.then(res => {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
return res.json();
})
.catch(err => {
if (err.message.includes('timeout') || err.message.includes('NetworkError')) {
// 标记为可恢复错误,进行重试或提示
console.warn('网络超时,尝试重试');
} else {
// 其他异常,判断为不可恢复
reportError(err);
}
});
B. 监听全局错误事件
window.onerror = function (message, source, lineno, colno, error) {
// 标记为不可恢复错误
reportError({ message, source, lineno, colno, error, type: 'js_runtime' });
};
window.onunhandledrejection = function (event) {
// 标记为不可恢复错误
reportError({ reason: event.reason, type: 'promise_rejection' });
};
C. 业务层错误分级
- 设计错误码体系,区分错误等级(INFO、WARN、ERROR、FATAL)
- 结合用户反馈和日志分析不断调整分类规则
1.4 常见误区或面试陷阱
❌ 误区一:所有错误都一视同仁处理
- 忽视错误对业务影响的差异,导致报警泛滥或漏报
❌ 误区二:只捕获 JS 错误,忽略网络和资源错误
- 网络错误同样会影响用户体验,必须区分对待
❌ 误区三:错误分类完全依赖前端,忽略后端协同
- 后端日志与状态码对判断错误类型很重要
答题要点
- 利用全局错误监听和请求状态码区分错误类型
- 网络错误(超时、断网)一般为可恢复,支持重试
- JS 运行时错误和未捕获 Promise 异常为不可恢复
- 结合错误上下文和业务影响确定分类
- 建立多层次错误分级和报警机制,减少误报和漏报
实现一个深拷贝函数,要求能够处理循环引用
当对象存在循环引用(例如 A -> B -> A)时,普通递归会导致死循环或栈溢出。
为了解决这一问题,可以使用 WeakMap(或 Map)记录已经拷贝过的对象,在遇到重复对象时直接返回缓存值,避免重复拷贝。
注意事项与常见误区
-
不能直接用
JSON.parse(JSON.stringify(obj):- 会丢失函数、
undefined、Symbol、循环引用等
- 会丢失函数、
-
递归拷贝时要判断是否为对象,不能对
null递归 -
循环引用时如果没有缓存机制会导致死循环
-
WeakMap相比Map更适合缓存对象引用,避免内存泄漏 -
还可以扩展支持:Map、Set、Function、Symbol 等更复杂场景
答题要点
- 使用
typeof判断基本类型,直接返回 - 引用类型递归处理,创建新对象/数组
- 使用
WeakMap缓存已处理对象,解决循环引用问题 - 特殊对象如 Date、RegExp 要单独判断并处理
- 注意保留原型链、避免
null和undefined误处理
如何实现一个对象继承另一个对象的功能
- JS 原型继承机制及其本质理解
- 不同继承方式的优缺点(
Object.create、构造函数、class) - ES5 与 ES6 的继承语法及底层逻辑
- 原型链与原型对象(
__proto__和prototype)的概念辨析
参考答案
一、原理说明
JavaScript 是基于原型链(Prototype Chain) 实现继承的,所有对象都可以通过其原型访问父对象的属性。
实现继承的几种方式:
1. 使用 Object.create(推荐方式)
const parent = { name: 'Parent' };
const child = Object.create(parent);
child.age = 10;
console.log(child.name); // 'Parent'
Object.create(proto)返回一个以proto为原型的新对象。- 不会拷贝属性,而是共享原型,适合简单对象继承。
2. 构造函数继承(伪类继承)
function Parent() {
this.name = 'Parent';
}
function Child() {
Parent.call(this); // 继承属性
this.age = 10;
}
const child = new Child();
console.log(child.name); // 'Parent'
- 只能继承构造函数中的属性,无法继承原型上的方法。
3. 原型链继承(经典)
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayHello = function() {
console.log('Hello');
};
function Child() {}
Child.prototype = new Parent();
const child = new Child();
child.sayHello(); // Hello
- 缺点:引用类型会被所有实例共享。
4. 组合继承(构造 + 原型)
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHi = function() {
console.log('Hi, ' + this.name);
};
function Child(name, age) {
Parent.call(this, name); // 第一次调用
this.age = age;
}
Child.prototype = new Parent(); // 第二次调用
Child.prototype.constructor = Child;
- 实现了属性和方法的继承,但调用了两次构造函数,存在性能浪费。
5. 寄生组合继承(最佳实践)
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHi = function() {
console.log('Hi');
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 只继承原型,不调用构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
- 优点:避免了两次构造函数调用,继承了属性和方法,推荐使用。
6. ES6 class extends
class Parent {
constructor(name) {
this.name = name;
}
greet() {
console.log('Hello');
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
- 语法糖,本质仍是基于原型链的继承。
super()必须在constructor里第一个调用。
二、常见误区与陷阱
-
混淆
__proto__和prototype:obj.__proto__是对象实例的原型指针;Func.prototype是函数构造器的原型对象。
-
对象的继承不是拷贝,而是引用或链式访问
-
原型链继承中,所有实例共享引用类型属性,可能产生副作用
-
忘记设置
constructor会导致类型判断异常
答题要点
- 掌握
Object.create的使用方式,适合简单继承 - 了解构造函数与原型链继承的优缺点
- 推荐使用寄生组合继承或 ES6 class 实现完整继承
- 理解继承的本质是原型链指向关系
- 注意原型共享导致的数据污染问题
在遍历数组时,为什么推荐使用for.of而不是for...in
一、考察点
-
是否理解
for...in和for...of在数组遍历中的本质差异for...in遍历的是属性名(索引)for...of遍历的是元素值
-
能否识别数组结构中常见的风险点,如稀疏数组、原型污染、自定义属性等
- 了解
for...in可能访问到非索引属性,遍历顺序不确定
- 了解
-
是否有实际项目中选择合适遍历方式的经验
- 面试官希望看到你在实践中有意识地区分并选择合适工具,确保语义清晰、行为安全
二、参考答案
1.1 原理说明
✅ for...in 是为对象设计的
for...in会枚举对象的所有可枚举属性,包括从原型链继承而来的属性- 对数组来说,
for...in遍历的是数组的键名(即索引) ,返回的是字符串形式的索引
const arr = ['a', 'b', 'c']; for (let i in arr) { console.log(typeof i); // "string" }
✅ for...of 是为可迭代结构设计的
for...of使用数组的[Symbol.iterator]方法,返回数组中的元素值- 语义更清晰,行为更符合“遍历数组元素”的直觉
const arr = ['a', 'b', 'c']; for (let val of arr) { console.log(val); // "a", "b", "c" }
1.2 核心用法 + 示例代码
🎯 for...in 的问题:遍历顺序不可控 + 原型链属性污染 + 类型非数字
Array.prototype.extra = 'polluted'; const arr = ['x', 'y']; arr.custom = 'meta'; for (let i in arr) { console.log(i); // "0", "1", "custom", "extra" }
- 不仅遍历数组索引,还遍历自定义属性和原型属性
- 返回的索引是字符串类型(需额外转换)
✅ for...of 的推荐方式:语义更清晰、只遍历数组值
const arr = ['x', 'y']; arr.custom = 'meta'; for (let val of arr) { console.log(val); // "x", "y" }
- 忽略自定义属性和原型链
- 简洁直接,行为符合预期
✅ 如果需要索引 + 值,搭配 arr.entries() 更合理
for (let [index, value] of arr.entries()) { console.log(index, value); }
entries()提供[索引, 值]的迭代器- 与
Object.entries(obj)一致,便于统一理解
1.3 常见误区或面试陷阱
❌ 误区一:误以为 for...in 和 for...of 效果一致
-
实际上:
for...in遍历的是属性名(可能包括非数组内容)for...of遍历的是元素值(严格只对可迭代结构)
❌ 误区二:忽视原型链污染带来的问题
Array.prototype.extra = 123; const arr = ['a', 'b']; for (let i in arr) { console.log(i); // "0", "1", "extra" }
- 非常容易造成代码逻辑异常
❌ 误区三:误用 for...of 遍历普通对象
const obj = { a: 1, b: 2 }; for (let val of obj) { // ❌ TypeError: obj is not iterable }
for...of仅适用于实现了迭代器接口的结构(数组、Set、Map、字符串、类数组)
答题要点
for...in遍历索引(字符串),还会遍历自定义属性和原型链for...of直接遍历元素值,语义清晰,行为可控for...in不保证遍历顺序 /for...of按插入顺序迭代- 推荐数组使用
for...of/ 需要索引时可用arr.entries() - 面试时务必指出
for...in存在原型污染风险,强调可维护性和语义性
哪些地方不能使用箭头函数
-
箭头函数的设计初衷及其与普通函数的区别
- 了解箭头函数的核心特性:没有自己的
this、arguments、super和new.target
- 了解箭头函数的核心特性:没有自己的
-
能够识别适合与不适合使用箭头函数的具体场景
- 能否结合项目需求正确选择函数定义方式,避免运行时错误
-
理解箭头函数在
构造函数、事件绑定、对象方法中的限制- 知道不同调用场景下
this绑定差异,避免错误引用
- 知道不同调用场景下
二、参考答案
1.1 原理说明
✅ 箭头函数的核心特点
- 箭头函数没有自己的
this,它的this是继承自外层最近的非箭头函数的this - 没有自己的
arguments对象,使用时会访问外层函数的arguments - 不能用作构造函数,无法使用
new关键字实例化 - 没有
prototype属性 - 不能绑定
this,即call、apply、bind不会改变箭头函数的this
🔍 这导致了箭头函数在以下场景中不适用或容易出错
1.2 核心用法 + 示例代码
🚫 不能作为构造函数使用
const Foo = () => {}; const obj = new Foo(); // TypeError: Foo is not a constructor
- 普通函数可用作构造函数
- 箭头函数没有
prototype,无法实例化
🚫 不能用作对象的方法(若依赖 this 指向调用对象)
const obj = { value: 42, getValue: () => { return this.value; } }; console.log(obj.getValue()); // undefined,this指向外层上下文(通常是window/global)
- 这里
this并非指向obj - 如果需要方法内部访问对象本身,应使用普通函数或简写方法
const obj = { value: 42, getValue() { return this.value; } }; console.log(obj.getValue()); // 42
🚫 不适合用作事件处理函数(尤其是依赖事件绑定的 this)
button.addEventListener('click', () => { console.log(this); // this指向定义时的外层作用域,而非按钮元素 });
- 传统函数中的
this指向事件源(DOM元素) - 箭头函数继承了外层
this,失去了事件绑定的语义
🚫 不能使用 arguments 对象
const fn = () => { console.log(arguments); // ReferenceError: arguments is not defined }; fn(1, 2, 3);
- 箭头函数没有自己的
arguments,若需使用参数列表,可以用剩余参数...args
const fn = (...args) => { console.log(args); };
1.3 常见误区或面试陷阱
❌ 误区一:误用箭头函数作为类的方法或构造器
- 以为箭头函数写法可以代替类方法,导致无法访问实例属性或产生意外的
this
❌ 误区二:错误绑定事件监听中的 this
- 使用箭头函数作为事件回调,
this不指向事件元素,引发代码逻辑异常
❌ 误区三:依赖 arguments 的函数误用箭头函数
- 代码中仍旧直接用
arguments,导致错误或不符合预期
答题要点
- 箭头函数无自身
this,继承外层上下文 - 不能作为构造函数使用,无法用
new - 对象方法中如果需要
this指向自身,不应使用箭头函数 - 事件处理函数依赖
this时不适合箭头函数 - 箭头函数无
arguments,用剩余参数代替 - 不支持
prototype,不能绑定this
创建一个对象的过程有哪些方式
一、考察点
-
了解 JavaScript 中创建对象的多种方式及其底层原理
- 能够区分字面量、构造函数、工厂函数、类、
Object.create等方法
- 能够区分字面量、构造函数、工厂函数、类、
-
掌握每种创建方式的优缺点及适用场景
- 包括内存使用、继承机制、属性和方法定义等方面
-
理解原型链与继承关系在对象创建中的体现
- 认识
prototype的作用和对象之间的联系
- 认识
-
能够根据需求灵活选择创建对象的方式
二、参考答案
1.1 原理说明
✅ 1. 对象字面量
- 直接使用
{}创建对象 - 该对象的原型是
Object.prototype - 语法简单,性能优良,适合快速创建单个对象
const obj = { name: 'Tom', age: 20 };
✅ 2. 构造函数(函数 + new)
-
通过定义构造函数,使用
new关键字实例化对象 -
new操作符执行步骤:- 创建一个新空对象
- 将新对象的
__proto__指向构造函数的prototype - 执行构造函数内部代码,
this指向新对象 - 如果构造函数没有返回对象,则返回新对象
function Person(name, age) { this.name = name; this.age = age; } const p = new Person('Tom', 20);
✅ 3. 工厂函数
- 普通函数返回一个新对象
- 不依赖
new,灵活且易于理解 - 但无法实现对象间的继承关系(除非手动设置)
function createPerson(name, age) { return { name, age }; } const p = createPerson('Tom', 20);
✅ 4. Object.create(proto)
- 通过指定原型对象
proto创建新对象 - 新对象直接继承
proto - 常用于实现原型式继承
const proto = { greet() { console.log('Hello'); } }; const obj = Object.create(proto); obj.greet(); // Hello
✅ 5. ES6 类(Class)
- 语法糖,底层仍基于原型继承
- 更接近传统面向对象的语法,代码结构清晰
- 类内部的构造函数使用
constructor定义
class Person { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log('Hi, ' + this.name); } } const p = new Person('Tom', 20);
1.2 核心用法 + 示例代码
| 方式 | 代码示例 | 说明 |
|---|---|---|
| 对象字面量 | const obj = { a: 1 }; | 快速创建,无继承复杂度 |
| 构造函数 | function A() { this.x = 10; } new A(); | 支持原型链,使用广泛 |
| 工厂函数 | function f() { return {x: 10}; } f(); | 简单,灵活,不支持继承 |
| Object.create | const o = Object.create(proto); | 直接指定原型 |
| ES6 类 | class C { constructor() {} } new C(); | 语法简洁,支持继承和静态方法 |
1.3 常见误区或面试陷阱
❌ 误区一:混淆工厂函数和构造函数的区别
- 工厂函数不支持
instanceof判断 - 构造函数需配合
new使用,否则this指向全局(严格模式下为undefined)
❌ 误区二:忽略 Object.create 与构造函数创建的原型链差异
Object.create(null)创建的对象没有原型,无法使用toString等方法
❌ 误区三:误以为 ES6 类是新的继承机制
- 实际是基于原型链的语法糖,底层仍是构造函数和原型链机制
答题要点
- 对象字面量:简单,快速,原型是
Object.prototype - 构造函数 +
new:创建实例,支持继承 - 工厂函数:灵活,返回新对象,不支持继承
Object.create(proto):指定原型,原型式继承- ES6 类:语法糖,面向对象语法,继承基于原型链
- 区分使用场景,理解原型链及内存效率
在浏览器里,如何自定义一个事件
一、考察点
-
掌握浏览器事件模型及自定义事件的实现原理
- 理解事件的类型(原生事件、自定义事件)
- 理解事件的派发(dispatch)和监听机制
-
熟悉 CustomEvent API 的使用方法
- 能够创建和初始化自定义事件,携带数据
- 理解事件的冒泡、取消默认行为等配置项
-
能够合理使用自定义事件解决组件间通信或解耦需求
二、参考答案
1.1 原理说明
- 浏览器的事件系统支持自定义事件,允许开发者创建并派发非标准的事件类型
- 自定义事件基于
Event或更常用的CustomEvent构造函数 CustomEvent允许传递附加信息(detail),增强事件表达能力- 事件的传播(捕获、冒泡)机制依然适用,开发者可控制事件是否冒泡和是否可取消
1.2 核心用法 + 示例代码
🎯 创建并派发自定义事件
// 1. 创建自定义事件,携带数据
const myEvent = new CustomEvent('my-event', {
detail: { message: 'Hello, custom event!' },
bubbles: true, // 事件是否冒泡
cancelable: true // 事件是否可取消默认行为
});
// 2. 监听该事件
document.addEventListener('my-event', (e) => {
console.log('Received:', e.detail.message);
});
// 3. 派发事件
document.dispatchEvent(myEvent);
new CustomEvent(eventType, eventInit):创建事件,eventInit可配置detail、bubbles、cancelable- 使用
addEventListener监听,使用dispatchEvent派发 - 事件可以冒泡,便于组件间层级传播
🎯 自定义事件在组件通信中的典型应用
- 当子组件需通知父组件某事件发生时,可使用自定义事件
- 适合解耦、无须借助全局状态管理的简单场景
// 子元素触发
const child = document.querySelector('#child');
const event = new CustomEvent('child-action', { detail: { data: 123 }, bubbles: true });
child.dispatchEvent(event);
// 父元素监听
const parent = document.querySelector('#parent');
parent.addEventListener('child-action', (e) => {
console.log('Child triggered with data:', e.detail.data);
});
1.3 常见误区或面试陷阱
❌ 误区一:使用 new Event() 代替 CustomEvent() 传递数据
Event构造函数不能携带detail数据,不能传递额外信息- 需用
CustomEvent来携带自定义数据
❌ 误区二:忽略事件是否冒泡和取消默认行为配置
- 自定义事件默认不冒泡,需显式设置
bubbles: true才能向上层节点传播 - 取消默认行为相关设置通常用于表单事件,普通自定义事件较少用到
❌ 误区三:错误理解事件派发范围
dispatchEvent派发事件只能在绑定的 DOM 节点及其祖先节点中捕获,不能跨 DOM 树传播
答题要点
- 使用
new CustomEvent(eventName, { detail, bubbles, cancelable })创建事件 - 使用
element.addEventListener监听,element.dispatchEvent派发 detail用于携带自定义数据- 需根据需求设置
bubbles和cancelable Event不支持携带数据,需用CustomEvent- 事件传播遵循 DOM 事件模型,派发节点及其祖先能接收到事件
如何获取页面中出现最多的元素
- 遍历 DOM 树,递归或使用
getElementsByTagName('*') - 统计元素标签出现次数,使用对象或 Map 保存计数
- 返回出现次数最多的标签及次数
- 过滤非元素节点(
nodeType === 1) - 标签名统一大小写处理
如果商家频繁操作导致性能卡顿
商家频繁操作(如快速点击按钮、批量操作数据等)导致的性能卡顿,核心是减少高频操作对主线程的阻塞,通过 “限制操作频率、优化执行效率、拆分任务” 三大方向优化,具体方案如下:
-
限制操作频率(防抖 / 节流)
-
对高频触发的事件(如按钮点击、输入框搜索)使用防抖(
debounce)或节流(throttle):- 防抖:等待操作停止后延迟执行(如连续点击按钮,只在最后一次点击后执行);
- 节流:固定时间内只执行一次(如每秒最多处理 1 次批量操作),避免短时间内大量重复请求 / 计算。
-
-
优化同步任务,避免阻塞主线程
- 将复杂计算(如数据过滤、格式化)迁移到Web Worker,利用子线程处理,不阻塞 UI 渲染;
- 简化 DOM 操作:减少高频 DOM 增删改(如批量更新时先离线构建 DOM 片段,再一次性插入),避免重排重绘。
-
拆分任务,分批执行
- 对大量数据处理(如批量导入商品、更新库存)采用任务分片:用
requestIdleCallback或定时器(setTimeout)将大任务拆分成小步骤,每步执行后让出主线程,避免长时间占用 JS 引擎。
- 对大量数据处理(如批量导入商品、更新库存)采用任务分片:用
-
缓存
- 缓存频繁使用的数据(如商品基础信息)到内存或
localStorage,减少重复请求 / 计算;
- 缓存频繁使用的数据(如商品基础信息)到内存或
-
UI 反馈
- 操作时立即给出 loading 状态或反馈(如按钮置灰、显示加载动画),避免用户重复触发;
在移动端开发中,你们是如何处理不同设备的适配问题的
考察点
- 是否了解常见的移动端适配方案及原理
- 是否能结合业务场景选择合理的适配策略
- 是否关注兼容性、用户体验和性能平衡
- 是否能系统性总结适配技术手段的优缺点
参考答案
一、适配问题的来源
移动端设备种类繁多,存在如下适配挑战:
- 屏幕尺寸不同(如 iPhone SE 到 iPad Pro)
- 分辨率与像素密度不同(如1x 到 4x)
- 不同平台(iOS、Android)行为差异
- 浏览器内核差异(微信内嵌、Safari、Chrome 等)
二、常见适配方案及实践
1. 使用视口 meta 标签设置
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
- 保证页面以设备宽度为基准渲染,避免缩放失真
- 是一切移动端适配的基础
2. flex 弹性布局 + 百分比布局
- 使用
flex实现自适应布局、对齐方式 - 使用
%控制元素宽高随父元素变化自动适应
3. rem 适配(结合 lib-flexible 或 postcss-pxtorem)
- 基于
html { font-size }动态设置根字体大小 - 所有长度用
rem表示,实现随屏幕宽度缩放
示例代码:
document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px';
- 优点:适配思路清晰,开发体验好
- 缺点:对第三方组件兼容性较差,需要统一处理单位转换
4. vw/vh 适配
- 使用视口单位(vw = 1%视口宽度,vh = 1%视口高度)作为长度单位
- 优点:原生CSS支持,无需额外JS计算
- 缺点:iOS下 Safari 某些版本 vh 不稳定;不利于对字体的精确控制
5. 媒体查询 Media Query
@media screen and (max-width: 768px) { /* 针对窄屏设备样式 */ }
- 适合做断点响应式布局(如 PC/H5 两端共用一套代码)
- 可配合 CSS Modules 或 Sass 编写模块化响应样式
6. 多套 UI 组件或主题切换
- 对于分辨率特别极端或平台风格差异大的情况(如横屏、Pad、TV)
- 采用不同主题、样式、交互进行适配处理
三、其他补充策略
- 高清方案:针对高DPR(devicePixelRatio)设备使用多倍图或 SVG 图形
- viewport-fit=cover:适配 iPhone X 等刘海屏安全区域
- 字体大小限制:Android 浏览器允许用户放大字体,需设置
user-scalable=no或动态缩放限制
要点
- 了解并使用 viewport 设置布局基础
- 灵活组合 rem/vw/flex/media query 等单位和布局方案
- 使用 postcss 工具链统一适配流程
- 考虑高DPR设备图像清晰度适配
- 针对横屏、Pad 等极端设备考虑额外样式方案
- 注重兼容性测试,处理浏览器差异
typeof null === 'object'是历史遗留的设计问题null instanceof Object === false因为 null 没有原型链- 类型判断建议优先使用
=== null、Object.prototype.toString.call(val)等更准确方式
typeof null 和 null instanceof Object的结果是什么
typeof null === 'object'是历史遗留的设计问题null instanceof Object === false因为 null 没有原型链- 类型判断建议优先使用
=== null、Object.prototype.toString.call(val)等更准确方式
如何用WeakMap:重构避免内存泄漏?闭包中变量存储在堆的哪个区域
考察点
- 理解闭包变量的内存存储位置及生命周期
- 掌握使用
WeakMap管理闭包数据,防止内存泄漏的原理 - 理解JavaScript垃圾回收机制及弱引用的作用
- 能结合实际场景说明
WeakMap替代闭包变量存储的优化方案
参考答案
一、闭包中变量的内存存储位置
- JavaScript中,函数执行时创建执行上下文,局部变量存储在堆内存中(
在 JavaScript 中,闭包中被引用的局部变量(外部函数的变量)会脱离原本的执行上下文(栈内存),被转移到堆内存中存储。) - 闭包使函数持续持有对外层作用域变量的引用,导致这些变量无法被垃圾回收
- 堆内存(Heap)是动态分配区域,用于存储对象和闭包捕获的变量,生命周期取决于引用关系
- 只要闭包存在,对应的变量就存活在堆中
二、WeakMap避免内存泄漏的原理
WeakMap的键是弱引用对象,不计入垃圾回收的根引用- 当键对象不再被引用时,
WeakMap中的键值对会自动被GC回收 - 适合存储和管理闭包私有数据,避免闭包对象持有过长生命周期
- 通过将闭包中状态或数据存储在
WeakMap,利用外部对象作为键,实现自动释放
三、用 WeakMap 重构闭包示例
传统闭包写法(存在闭包持久引用风险):
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
使用 WeakMap 存储私有状态:
const countMap = new WeakMap();
function createCounter() {
const obj = {}; // 用作WeakMap的键
countMap.set(obj, 0);
return function() {
let count = countMap.get(obj) || 0;
count++;
countMap.set(obj, count);
console.log(count);
};
}
- 这里状态
count不直接存在闭包内,而是存在WeakMap中,绑定到外部对象obj - 当闭包不再引用
obj时,该键值对会被自动回收,防止内存泄漏
四、应用场景及优势
- 适合管理大量实例私有状态,避免闭包链过长导致内存长驻
- 在类或函数组件中,存储不方便直接挂载的私有数据
- 减少因闭包持有大量变量导致的内存占用,提升性能
答题要点
- 闭包变量存储在堆内存,受引用关系影响生命周期
WeakMap利用弱引用特性,键对象无引用时自动回收- 通过
WeakMap存储闭包私有数据,避免闭包导致的内存泄漏 - 用外部对象作为键,数据与闭包解耦,方便管理和释放
- 适合复杂状态管理,提升内存效率和应用性能
new操作符内部执行步骤
考察点
- 理解JavaScript中
new操作符的底层执行流程 - 掌握构造函数实例化对象的原理
- 理解
new与函数调用的区别及其对this的绑定机制 - 能解释构造函数返回值对
new结果的影响
参考答案
一、new操作符的内部执行步骤
-
创建一个空对象
- 创建一个全新的空对象,作为将要返回的实例对象
-
设置原型链
- 将新对象的内部
[[Prototype]](即__proto__)指向构造函数的prototype属性 - 实现继承构造函数原型上的属性和方法
- 将新对象的内部
-
绑定函数执行上下文
- 将构造函数内部的
this指向新创建的对象 - 执行构造函数代码,给新对象添加属性或方法
- 将构造函数内部的
-
返回对象
- 如果构造函数返回了一个对象类型(非基本类型),则返回该对象
- 否则,返回步骤1创建的新对象
二、举例说明
function Person(name) {
this.name = name;
return { custom: 'object' }; // 返回对象,则new结果是此对象
}
const p = new Person('Tom');
console.log(p); // { custom: 'object' }
function Animal(name) {
this.name = name;
return 123; // 返回基本类型,忽略返回值
}
const a = new Animal('Cat');
console.log(a.name); // 'Cat'
三、new的关键点总结
- 创建新对象且继承构造函数原型链
this绑定新对象执行构造函数- 返回值规则(对象类型覆盖默认返回)
答题要点
new创建空对象并设置原型链- 构造函数内部
this指向该对象执行初始化 - 返回非对象类型则返回新对象,返回对象类型则以返回对象为准
new实现了构造函数实例化和继承机制的结合
讲一下你在项目中如何使用Chrome DevTools Performance面板分析FPS卡顿的
考察点
- 熟悉 Chrome DevTools Performance 面板的使用方法
- 理解浏览器渲染流程及影响 FPS 的关键环节
- 能分析帧率卡顿的具体原因(如长任务、重排重绘等)
- 掌握性能瓶颈定位和优化思路
参考答案
一、Chrome DevTools Performance 面板简介
- Performance 面板用于捕获网页的性能数据,展示 CPU 时间线、帧率(FPS)、事件分布等
- 能分析脚本执行、样式计算、布局、绘制、合成等阶段耗时情况
- 通过录制一段操作过程,捕获渲染性能快照
二、使用流程
-
开启性能录制
- 打开 DevTools,切换到 Performance 面板
- 点击“Record”按钮,模拟或复现页面卡顿场景
- 结束录制,生成性能分析报告
-
查看 FPS 曲线
- 观察顶部 FPS 图,理想帧率是 60 FPS(每帧 ~16.6ms)
- 低帧率区域标红,重点关注这些区域
-
分析主线程耗时
- 在 Main Thread 时间轴中查看长任务(黄色条)
- 重点检查 JavaScript 执行、样式计算(Recalculate Style)、布局(Layout)、绘制(Paint)时间
- 长任务导致浏览器无法及时刷新,FPS 下降
-
检查渲染阻塞原因
- 通过 Flame Chart(火焰图)查看调用栈,定位具体函数或操作
- 识别是否存在大量同步DOM操作、频繁触发重排或重绘
-
查看合成层与GPU活动
- 关注 Composite Layers 阶段,判断是否合理使用了 GPU 加速
- 查看是否存在不必要的图层合成或频繁触发合成导致开销大
-
捕获事件与帧边界
- 对比用户输入事件(如滚动、点击)与帧渲染时间,判断响应延迟
- 确认是否有任务阻塞帧边界导致卡顿
三、项目中定位FPS卡顿的实例步骤
- 录制一段页面滚动或动画的过程
- 观察 FPS 下降时间点,锁定主线程长任务
- 分析火焰图定位耗时函数,找到性能瓶颈(如大量 DOM 操作或复杂计算)
- 配合代码审查,确认是否存在同步阻塞操作
- 针对重排重绘优化,如减少DOM修改频率、使用transform代替位移等
- 优化 JS 执行效率,异步拆分大任务
- 使用 GPU 加速合理拆分图层,减少合成压力
requestAnimationFrame优化动画?对比CSS动入画的GPU加速原理
考察点
- 理解
requestAnimationFrame(RAF)的工作机制及优势 - 掌握如何使用 RAF 优化动画性能
- 了解 CSS 动画的 GPU 加速原理及其区别
- 能比较 JS 动画与 CSS 动画的优劣及使用场景
参考答案
一、requestAnimationFrame 优化动画原理
-
requestAnimationFrame是浏览器提供的接口,用于在浏览器下一次重绘前执行回调函数。 -
它能保证动画回调在浏览器的刷新周期(一般60FPS,即16.6ms)内执行,避免不必要的多余帧更新。
-
使用 RAF 可以让动画与浏览器渲染同步,减少掉帧、卡顿现象。
-
RAF 具有自动节流功能:当标签页不可见时,RAF 会暂停执行,节省资源。
-
典型用法:
function animate() {
// 更新动画状态,比如元素位置
updateElementPosition();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
二、CSS动画的GPU加速原理
- CSS 动画(如
transform,opacity)通常会触发 GPU 加速。 - 浏览器会将这类动画单独放到合成层(Composite Layer)进行处理,利用 GPU 进行图层合成,避免触发主线程中的重排(reflow)和重绘(repaint)。
- 这样,动画只需合成图层,不改变布局或绘制,性能开销小,流畅度高。
- 典型GPU加速的CSS属性有:
transform,opacity,filter等。
四、使用场景
-
requestAnimationFrame适合:- 需要复杂动画逻辑、物理模拟、动画与业务逻辑高度耦合的场景
- 动态计算位置、动画帧控制等
-
CSS动画适合:
- 简单的 UI 交互动效,如按钮点击反馈、元素进出场动画
- 需要高性能、低主线程占用的动画
答题要点
- RAF 是浏览器刷新同步的动画回调,能减少丢帧和卡顿
- CSS动画借助合成层实现GPU加速,避免重排重绘,提高性能
- RAF灵活但依赖JS执行,CSS动画性能更优但灵活性有限
- 两者根据需求场景合理选择与配合使用
- 现代动画优化中,推荐用CSS动画做轻量效果,用RAF做复杂动画控制
虚拟列表实现中如何计算渲染区间?滚动时如何避免布局抖动
考察点
- 理解虚拟列表(Virtual List)的核心思想及其性能优势
- 掌握如何根据滚动位置和容器高度计算渲染区间(startIndex、endIndex)
- 了解滚动过程中布局抖动(闪烁、跳动)产生的原因
- 掌握避免布局抖动的技术手段和优化策略
参考答案
一、虚拟列表核心原理
- 虚拟列表通过只渲染视口内及附近一定范围内的列表项,避免渲染全部数据,极大降低DOM节点数量,提升性能。
- 通过动态计算“渲染区间”,即当前需要渲染的起始和结束索引,实现按需渲染。
二、渲染区间的计算方法
-
关键数据
scrollTop:当前滚动容器的垂直滚动距离viewportHeight:容器可视高度itemHeight:单个列表项固定高度(或估算平均高度)buffer:预渲染的额外项数,防止快速滚动时白屏
-
基本计算
- 起始索引
startIndex = Math.floor(scrollTop / itemHeight) - buffer,不能小于0 - 结束索引
endIndex = Math.ceil((scrollTop + viewportHeight) / itemHeight) + buffer,不能超过总条数
- 起始索引
-
动态高度处理
- 若列表项高度不固定,需维护每项高度数组,累计计算滚动偏移
- 结合二分查找快速定位当前滚动位置对应的起始项索引
-
渲染时
- 仅渲染
[startIndex, endIndex]区间内的元素 - 通过占位元素(padding 或 margin)撑开总高度,保持滚动条准确
- 仅渲染
三、滚动时布局抖动产生原因
- 由于动态渲染元素高度计算不准确,导致占位高度与实际渲染元素高度不符
- 频繁增减 DOM 节点引起的重排,造成滚动跳动或闪烁
- 直接操作 DOM 或频繁触发同步布局,导致渲染卡顿和界面抖动
四、避免布局抖动的优化方案
-
准确计算占位高度
- 通过缓存每个元素真实高度,动态调整占位容器高度
- 使用占位容器(如上、下 padding)保持滚动高度稳定
-
合理设置缓冲区(buffer)
- 预渲染上下多余项,平滑快速滚动,减少白屏
-
减少同步布局触发
- 避免频繁读取布局属性(如offsetHeight),减少重排
- 使用异步手段(如 requestAnimationFrame)批量处理渲染更新
-
使用transform位移代替修改top/scrollTop
- transform GPU 加速且不触发重排,减少抖动
-
懒加载图片和资源,防止加载时尺寸变化
答题要点
- 计算渲染区间基于 scrollTop、容器高度和单项高度加缓冲区
- 不固定高度需维护高度缓存并结合二分查找定位索引
- 抖动多因占位高度不准确和频繁重排导致
- 通过占位元素撑起正确高度、设置缓冲区和减少同步布局避免抖动
- transform 位移与异步渲染优化性能和体验
你们用的是什么脚手架,脚手架的基本原理了解么
考察点
- 面试官希望了解你是否具备工程化思维
- 是否熟悉当前项目所用的脚手架工具及其底层实现原理
- 是否理解脚手架在项目初始化、规范约定、自动化等方面的作用
- 是否具备定制或维护脚手架的能力
参考答案
一、常用脚手架工具及我们团队的选择
我们项目主要使用的是:
- Vue 项目:使用
@vue/cli、vite或公司自研的内部模板系统 - React 项目:早期使用
create-react-app,后续迁移为Vite + 自定义模板 - 自研脚手架:基于
Plop.js、Yeoman、Commander或create-*实现,通过 Node.js 实现项目初始化、目录结构生成、依赖安装、git 初始化等功能
二、脚手架的核心功能
- 初始化项目结构(生成项目目录和基础文件)
- 注入标准化配置(eslint、prettier、commitlint、husky、tsconfig等)
- 自动安装依赖(npm/yarn/pnpm install)
- 选择模板和功能模块(用户选择功能特性,动态生成对应模块)
- 自定义交互式命令行(CLI) (使用 inquirer 等库实现配置选择)
- 生成代码片段/组件(如
plop generate component)
三、脚手架的实现原理
脚手架本质上是基于 Node.js 脚本,通过命令行交互完成初始化和代码生成,主要原理包括:
- 文件模板引擎:如 EJS、Handlebars,将用户输入注入模板,动态生成文件
- 文件系统操作:如
fs-extra创建文件夹、复制模板、写入内容 - 命令行交互:如
inquirer获取用户输入,决定生成逻辑 - 执行系统命令:如
child_process.spawn()执行git init、npm install - 模块化组织:每种模板或功能模块作为一个插件独立维护,可组合可扩展
示例脚手架代码简化:
const inquirer = require('inquirer');
const fs = require('fs-extra');
const ejs = require('ejs');
const path = require('path');
async function init() {
const answers = await inquirer.prompt([
{ name: 'projectName', message: '项目名称?' },
{ name: 'useTypeScript', type: 'confirm', message: '是否使用 TypeScript?' },
]);
const templateDir = path.resolve(__dirname, 'templates');
const targetDir = path.resolve(process.cwd(), answers.projectName);
await fs.copy(templateDir, targetDir);
// 替换模板变量
const pkgPath = path.join(targetDir, 'package.json');
const content = await fs.readFile(pkgPath, 'utf-8');
const result = ejs.render(content, { projectName: answers.projectName });
await fs.writeFile(pkgPath, result);
}
init();
四、团队为何自研脚手架
- 提高初始化效率,统一开发规范
- 内置企业级配置(如私有 npm 源、CI/CD 脚本、权限管理模板等)
- 避免重复工作,提高开发一致性和可维护性
- 可集成 UI 工程、服务端 Node 脚手架、监控配置等组件
答题要点
- 脚手架是提升工程效率、统一规范的工具
- 底层通过 Node.js 实现文件复制、模板注入、命令执行
- 常用工具:Vue CLI、CRA、Vite、自研 CLI(基于 Plop、Yeoman)
- 企业通常自研脚手架实现快速初始化、统一配置和组件生成
- 掌握核心原理(模板渲染、交互、文件操作)有助于二次开发
4、ESLint做了哪些规范,有没有自定义规则
针对大量数据的渲染,web Workers:是怎么优化的
考察点
- 面试官希望了解你是否掌握前端中 大数据量渲染 的性能瓶颈与优化手段
- 是否理解 Web Worker 的工作原理以及其适合处理的任务类型
- 能否结合项目实际说明 Web Worker 在前端性能优化中的作用
- 能否识别主线程阻塞和 UI 卡顿之间的联系,并给出对应的拆分方案
参考答案
一、为什么大量数据渲染会卡顿?
在前端渲染大数据时(如:渲染10万条表格数据、做复杂图表、处理日志/报表等),主要会出现以下问题:
- 主线程阻塞:浏览器的 UI 渲染、JS 执行、用户交互都在主线程中进行
- 数据处理耗时长:如排序、聚合、过滤、转格式等,JS 处理大数据本身就慢
- 渲染任务重:DOM 大量更新、节点重复创建,触发频繁重排重绘
这些问题最终会导致:
- 页面卡顿、白屏、UI 不响应
- 滚动卡顿,交互延迟
二、Web Worker 是什么?如何优化这些问题?
Web Worker 是浏览器提供的线程机制,允许 JS 脚本在后台线程中运行,从而不阻塞主线程。
适合用于处理:
- 复杂或大数据量的 计算任务
- 异步化 数据转换、排序、预处理
- 与主线程通信以实现数据预处理 + 主线程渲染解耦
使用方式示例:
主线程:
const worker = new Worker('worker.js') worker.postMessage(largeData) // 传入数据 worker.onmessage = (event) => { renderToDOM(event.data) // 主线程负责渲染 }
worker.js:
self.onmessage = (e) => { const processed = heavyCompute(e.data) // 大量计算 self.postMessage(processed) // 发送回主线程 }
三、如何配合虚拟列表、分页渲染等方案使用?
Web Worker 只负责数据处理,不涉及 DOM。主线程可以:
- 接收到 Worker 处理后的数据,再结合虚拟滚动组件渲染(如:vue-virtual-scroller、react-window)
- 处理后的数据分页传入 UI,减少每一帧渲染负担
四、常见项目使用场景
- 渲染万级别以上的数据表格
- 实时图表中的数据预计算
- 地图场景中复杂点位聚合(如 geo clustering)
- 实时日志/报表处理
- Excel 大数据量导入前的数据转换(如 xlsx 文件转 JSON)
五、注意事项与边界问题
- Worker 与主线程之间通信是异步的,通过
postMessage()传值(需序列化) - 不能操作 DOM,只适合数据逻辑计算
- 在高频操作中通信开销不能忽视(特别是传大量数据)
- 主线程仍需合理使用虚拟列表、分批渲染等机制
答题要点
- 大数据渲染容易造成主线程阻塞,影响 FPS、交互响应
- Web Worker 可用于将耗时计算任务放到独立线程中处理
- Worker 不能访问 DOM,适合配合虚拟滚动、分页等前端方案使用
- Worker 与主线程通过 postMessage 通信,有序列化/异步限制
- 常见使用场景包括表格、图表、地图、Excel 解析等数据密集型场景
为什么选用Canvas分层而非SVG
考察点
- 面试官希望了解你对 Canvas 与 SVG 渲染原理和性能差异的理解
- 是否能从“分层渲染”、“性能瓶颈”、“重绘开销”等角度进行技术选型
- 能否结合具体业务场景,说明 Canvas 分层相较 SVG 的优势和适用场景
参考答案
一、Canvas 与 SVG 的基本区别
| 特性 | Canvas | SVG |
|---|---|---|
| 渲染方式 | 基于像素点的位图渲染 | 基于DOM + 矢量图形的结构化渲染 |
| 事件绑定 | 需要手动计算命中区域 | 可以直接给元素绑定事件 |
| 适合场景 | 高性能绘图(游戏、粒子系统、动画) | 图表、图形编辑器、结构化图形展示 |
| 更新成本 | 需要整体重绘 | 只需更新对应 DOM 节点 |
| 复杂场景下性能 | 表现更优,可控性高 | DOM 节点多时性能下降严重 |
二、为什么在复杂渲染中使用 Canvas 分层而不是 SVG?
1. SVG 在复杂场景下性能劣化严重
- SVG 每一个图形都是 DOM 元素,当图形数达到上千级别时,页面存在大量 DOM 节点,操作和重绘性能急剧下降
- CSS 操作、交互绑定、样式计算都会随 DOM 增多而变慢
2. Canvas 的像素级渲染,适合“分层缓存 + 局部刷新”
-
使用多层
<canvas>,将不同图层内容(如背景层、图形层、文字层、交互层)独立渲染在不同 canvas 上:- 背景层:一次绘制静态背景,避免每次重绘
- 图形层:仅在图形变化时更新
- UI层 / 选中态层:只渲染交互时内容
通过这种方式能有效 避免全部重绘,提升渲染性能
3. 粒度可控 + 支持离屏渲染
- Canvas 可以配合 离屏Canvas(OffscreenCanvas) 或 缓存 Canvas,将复杂计算或绘制在后台完成,提高流畅性
- Canvas 可按需渲染 + 手动控制刷新节奏,更适合动态场景(如动画、大图拖拽、实时画布)
三、使用Canvas分层的具体优势
- 减轻主绘制层负担:多个 canvas 独立渲染,互不干扰
- 减少无效重绘区域:如拖动时只重绘动态层,背景层不动
- 提升帧率与性能:避免每帧都操作整个 DOM 或主 canvas
- 增强用户体验:拖动、绘制等更流畅,避免卡顿
四、适合Canvas分层的典型业务场景
- 大屏数据可视化系统
- 地图系统(点位、覆盖物、轨迹等分层)
- 在线白板、绘图系统
- 游戏场景、动态效果控制
- 高密度图形编辑工具(如连线图、流程图)
五、常见误区和注意点
- Canvas 并非总比 SVG 好:结构化图形、需要频繁交互绑定的图表工具,SVG 更方便
- Canvas 缺少 DOM 的语义化、无障碍支持,可访问性较差
- Canvas 分层时需注意层级逻辑、事件穿透处理,可能需手动计算命中区域进行事件响应
答题要点
- SVG 基于 DOM,适合小型静态图形;Canvas 基于像素渲染,适合高性能大图渲染
- Canvas 分层可以减少全图重绘,按需渲染不同内容,提高性能
- 分层结构如:背景层、图形层、交互层,互不影响
- 典型应用场景包括地图、白板、大屏、实时图形渲染等
- SVG 易操作但性能差;Canvas 控制强但复杂,需结合业务场景选型