本轮要点: 本轮面试考察了前端基础能力、网络协议、Vue响应式原理、构建工具底层实现,以及手写能力与业务建模能力,重点在于深入理解底层原理和综合解决问题的能力。
本轮共 11 道题。答案默认折叠,便于先自行作答。
1. 讲一下你的前端模块化的理解
题目要点
模块化发展历程、ES Module 和 CommonJS 区别、模块加载机制、Tree-shaking原理。
参考答案
原理说明
模块化的目的:为了提高代码的可维护性、复用性和依赖管理能力,将代码组织为独立模块。
发展历程:
- 无模块化(全局变量)
- IIFE 模式(立即执行函数)
- CommonJS(Node.js 标准,同步加载)
- AMD / CMD(异步模块定义)
- ES Module(ES6 标准模块系统,浏览器原生支持)
CommonJS vs ES Module:
- CommonJS 是同步加载,适用于服务端;ES Module 是静态分析+异步加载,适用于浏览器。
- ES Module 支持 Tree-shaking,CommonJS 不支持。
核心用法 + 示例代码
// ES Module
export const add = (a, b) => a + b;
import { add } from './utils.js';
常见误区
- 将 ES Module 和 CommonJS 混用,导致打包失败。
- 忽视 ES Module 是静态结构,不能在运行时动态导入变量名模块(需使用
import())。
2. HTTP/2 解决 HTTP/1.1什么问题?
题目要点
HTTP/2 优化点、HTTP/1.1 的瓶颈、性能原理
参考答案
原理说明
HTTP/2 的主要改进点:
- 多路复用:HTTP/1.1 只能并行发起多个连接,HTTP/2 允许多个请求复用一个 TCP 连接。
- 头部压缩:通过 HPACK 算法压缩重复的请求头。
- 服务端推送:可以在客户端请求之前主动推送资源。
- 二进制分帧:改为帧的形式传输数据,提升解析效率。
核心用法 + 示例
- 开启 HTTP/2:
- Nginx 开启
http2支持。 - 使用 CDN 或 HTTPS 时启用
http/2协议。
- Nginx 开启
常见误区
- 认为 HTTP/2 就是更快的 HTTP/1.1,其实它改变了传输结构。
- 忽略了浏览器是否支持或服务端配置不当。
3. 在你的项目中,你是如何设计埋点方案统计用户点击“加入购物车”的行为?
题目要点
- 字段设计:覆盖业务核心维度,避免过度采集;
- 技术选型:根据场景选择无痕/手动埋点,优先保证可靠性;
- 数据质量:通过验证、监控、采样确保数据可信;
- 长期维护:通过SDK收敛和文档化降低协作成本。
参考答案
可以参考以下设计思路进行回答:
1. 数据建模:明确埋点字段
核心字段需覆盖基础上下文,例如:
- 行为标识:统一命名规则(如
cart_add),避免魔法字符串 - 商品信息:商品ID、SKU、价格、类目(需考虑敏感字段脱敏)
- 用户上下文:匿名ID(设备指纹或Cookie)、登录用户ID(若存在)
- 环境信息:页面URL、referrer、时间戳、设备类型(User Agent解析)
- 业务扩展字段:促销ID、库存状态、AB测试分组
关键点:字段设计需与后端数据团队对齐,确保数仓能解析。例如价格单位统一为「分」以避免浮点误差。
2. 技术实现:平衡侵入性与可靠性
- 无痕埋点:通过事件代理(Event Delegation)监听购物车按钮的父容器,减少对业务代码侵入。例如:
document.addEventListener('click', (e) => { if (e.target.closest('[data-track="cart-add"]')) { const product = e.target.dataset.product; // 通过dataset传递参数 tracker.send('cart_add', JSON.parse(product)); } }); - 手动埋点:对于动态生成的内容(如SPA路由跳转),在业务逻辑层显式调用埋点:
const handleAddToCart = (product) => { cartService.add(product); tracker.send('cart_add', { product_id: product.id, // 避免直接透传整个product对象,按需选取字段 }); };
权衡点:无痕埋点维护成本低但灵活性差,手动埋点更精准但需代码耦合。
3. 数据上报策略
- 实时性要求:采用Navigator.sendBeacon()(页面关闭时仍可靠发送)或优先调用API,降级方案为localStorage暂存+下次上报
- 防重复提交:通过唯一请求ID(如UUID)或前端生成的事件指纹(商品ID+时间戳哈希)去重
- 采样率控制:大流量场景下按用户ID哈希采样(如10%),避免数据爆炸
4. 验证与监控
- 开发阶段:通过Mock服务拦截埋点请求,验证字段完整性
- 线上监控:
- 日志抽样检查:定期抽查原始日志是否包含必填字段
- 数据一致性校验:对比埋点数据与购物车服务调用量差异(如±5%阈值告警)
- 用户行为路径分析:检查“加入购物车”后无下单的异常流失节点
- 日志抽样检查:定期抽查原始日志是否包含必填字段
5. 可维护性优化
- 统一SDK封装:收敛埋点方法,禁止业务代码直接调用第三方统计平台(如Google Analytics)
- 版本化兼容:埋点字段随业务迭代时,通过Schema校验工具(如JSON Schema)保证历史数据可解析
- 文档自动化:利用JSDoc或Swagger生成埋点字段字典,与数据团队共享
4. Hash 模式和历史模式(History API)的路由跳转差异,如何实现服务端兼容。
题目要点
前端路由原理、Hash vs History 模式、服务端配置要点。
参考答案
原理说明
- Hash 模式:使用
#符号改变 URL,变化不会触发服务端请求。兼容性好、部署简单。 - History 模式:使用 HTML5 的
pushState/replaceState修改路径,用户访问某个路径可能触发真实的服务端访问,需配置服务端 fallback 到index.html。
核心用法 + 示例
// Vue Router
const router = new VueRouter({
mode: 'history', // 或 hash
});
- History 模式下 Nginx 示例:
location / {
try_files $uri $uri/ /index.html;
}
常见误区
- 忽略 History 模式下的服务端 fallback 导致路径刷新 404。
- 使用 Hash 模式时不美观、对 SEO 不友好。
5. Vue 2 的 Object.defineProperty 和 Vue 3 的 Proxy 在响应式处理数组时的区别是什么?
题目要点
Vue 响应式系统、数组监听原理、defineProperty 和 Proxy 区别
参考答案
原理说明
- **Vue 2 使用 **``:
- 只能拦截已有属性,对新增或删除属性无法监听。
- 对数组进行了方法重写(push/pop/shift…)来手动触发更新。
- **Vue 3 使用 **``:
- 能完全代理对象和数组,包括属性的添加、删除、访问等。
- 对数组索引访问、length 修改等也能被监听。
示例对比
// Vue 2
this.arr[1] = 10; // 无法监听索引变化
// Vue 3
reactiveArr[1] = 10; // 可以被追踪
常见误区
- 认为 Proxy 性能一定更好,其实需要考虑浏览器兼容性。
- 忽略 Vue 2 中数组原型方法的重写带来的维护成本。
6. 讲一下Vue 的异步更新策略,以及 nextTick 的实现原理。
题目要点
异步更新策略
- Vue 通过
queueWatcher将Watcher更新推入异步队列,避免同步频繁更新 DOM。 - 默认使用 Microtask(微任务) 执行队列,确保数据稳定后再渲染。
- Vue 通过
nextTick 核心流程
- 回调推入队列 → 异步任务调度(
Promise.then等)→ 执行回调(flushCallbacks)。 - 保证回调在 DOM 更新后 执行,适用于依赖更新后 DOM 的操作(如获取渲染后的元素尺寸)。
- 回调推入队列 → 异步任务调度(
降级兼容
- 根据浏览器支持情况选择最优异步 API,保证高版本性能优先,低版本功能可用。
- 根据浏览器支持情况选择最优异步 API,保证高版本性能优先,低版本功能可用。
典型应用场景:
- 修改数据后立即获取更新后的 DOM(如
this.$nextTick(() => { /* 操作 DOM */ }))。 - 在自定义指令或插件中确保操作发生在渲染完成后。
参考答案
Vue 的异步更新机制是其高效渲染的核心之一,主要目的是减少不必要的 DOM 操作,通过批量更新优化性能。具体策略如下:
响应式数据变更触发依赖更新
- 当数据被修改时,Vue 的响应式系统会触发
dep.notify(),通知所有订阅该数据的Watcher进行更新。 - 但 Vue 并不会立即执行 DOM 更新,而是将
Watcher推入一个异步队列(queueWatcher)。
- 当数据被修改时,Vue 的响应式系统会触发
异步队列去重
- 同一个
Watcher在同一个事件循环内多次触发(如在一个方法中多次修改数据),Vue 会去重,确保它仅被推入队列一次,避免重复计算。
- 同一个
批量更新(Microtask / Macrotask)
- Vue 默认使用 Microtask(微任务) 来执行异步队列,优先选择
Promise.then(现代浏览器),降级到MutationObserver或setImmediate,最终回退到setTimeout(Macrotask)。 - 这样做的目的是让所有同步数据变更在同一个事件循环内完成,DOM 更新在微任务阶段执行,保证视图更新前所有数据已稳定。
- Vue 默认使用 Microtask(微任务) 来执行异步队列,优先选择
nextTick 的实现原理
nextTick 是 Vue 提供的一个 API,用于在 DOM 更新完成后 执行回调,其核心逻辑与异步更新队列紧密相关:
回调队列管理
- 每次调用
nextTick(callback),Vue 不会立即执行回调,而是将其推入一个 回调队列(callbacks)。 - 如果当前没有刷新回调队列的任务(
pending = false),则启动一个异步任务(timerFunc)。
- 每次调用
异步任务调度
- Vue 会根据运行环境选择最优的异步 API(优先级:
Promise.then>MutationObserver>setImmediate>setTimeout)。 - 例如,在现代浏览器中,
timerFunc可能是:timerFunc = () => Promise.resolve().then(flushCallbacks);<br> ```<br>
- Vue 会根据运行环境选择最优的异步 API(优先级:
执行回调(flushCallbacks)
- 当异步任务触发时,Vue 会依次执行回调队列中的所有回调,并清空队列。
- 由于 Vue 的 DOM 更新也使用相同的异步机制,
nextTick能确保回调在 DOM 更新后 执行。
- 当异步任务触发时,Vue 会依次执行回调队列中的所有回调,并清空队列。
降级策略
- 在不支持
Promise的旧浏览器(如 IE11),Vue 会降级到MutationObserver或setTimeout,但 Macrotask 可能比 Microtask 稍慢,导致nextTick延迟稍高。
- 在不支持
7. Loader 和 Plugin 在 Webpack 中的作用有何不同?举例说明常见应用场景。
题目要点
Loader
- 专注单个文件的转换,需声明文件类型(
test正则)。 - 典型场景:转译(Babel)、样式处理、文件加载。
- 专注单个文件的转换,需声明文件类型(
Plugin
- 干预整个构建生命周期,通过钩子实现高级功能。
- 典型场景:代码压缩、HTML 生成、环境变量注入。
- 干预整个构建生命周期,通过钩子实现高级功能。
参考答案
Webpack 的核心编译流程可以抽象为:资源输入 → 模块转换(Loader)→ 代码优化与输出(Plugin)。两者的分工和定位完全不同:
| 维度 | Loader | Plugin |
|---|---|---|
| 作用阶段 | 模块加载阶段(Module Resolution) | 整个构建流程(Compilation Hooks) |
| 功能目标 | 转换单个文件(如转译、编译) | 干预打包过程(如优化、资源管理) |
| 运行机制 | 链式调用,按规则处理文件内容 | 监听 Webpack 生命周期钩子执行任务 |
| 配置方式 | module.rules 数组 | plugins 数组 |
Loader 的作用与典型场景
核心能力:将非 JavaScript 模块转换为 Webpack 可识别的格式(通常输出 JavaScript 或 Asset 资源)。
常见 Loader 示例
转译类 Loader
babel-loader:将 ES6+/TypeScript 转译为 ES5。module: {<br> rules: [{<br> test: /\.js$/,<br> use: 'babel-loader' // 调用 Babel 转译<br> }]<br> }<br> ```<br>ts-loader:处理 TypeScript 文件。
样式处理 Loader
css-loader:解析@import和url()依赖。style-loader:将 CSS 注入 DOM(通过<style>标签)。{<br> test: /\.css$/,<br> use: ['style-loader', 'css-loader'] // 从右到左执行<br> }<br> ```<br>
文件资源 Loader
file-loader:将文件(如图片)输出到构建目录并返回 URL。url-loader:小文件转 Base64 内联,大文件回退到file-loader。
关键点:Loader 是单向管道,每个文件匹配后依次通过多个 Loader 处理(如 sass-loader → css-loader → style-loader)。
Plugin 的作用与典型场景
核心能力:通过 Webpack 的 Tapable 事件流 扩展构建流程,实现资源优化、环境注入等高级功能。
常见 Plugin 示例
代码优化类
TerserPlugin:压缩 JavaScript(替代 UglifyJS)。const TerserPlugin = require('terser-webpack-plugin');<br> module.exports = {<br> optimization: {<br> minimizer: [new TerserPlugin()]<br> }<br> };<br> ```<br>SplitChunksPlugin:拆分公共代码(默认已集成)。
资源管理类
HtmlWebpackPlugin:自动生成 HTML 并注入打包后的资源。new HtmlWebpackPlugin({<br> template: './src/index.html'<br> });<br> ```<br>CopyWebpackPlugin:复制静态文件(如public/favicon.ico)。
环境增强类
DefinePlugin:定义全局常量(如process.env.NODE_ENV)。HotModuleReplacementPlugin:启用 HMR 热更新。
关键点:Plugin 通过监听 Compiler Hooks(如 emit、done)干预构建流程。例如:
compiler.hooks.emit.tap('MyPlugin', (compilation) => {
// 在生成资源到输出目录前修改内容
});
8. Webpack 热更新(HMR)的实现原理,包括客户端和服务端的协作流程。
题目要点
- 服务端:
- 监听文件 → 增量编译 → 推送 WebSocket 消息 → 提供补丁文件。
- 监听文件 → 增量编译 → 推送 WebSocket 消息 → 提供补丁文件。
- 客户端:
- 接收通知 → 拉取补丁 → 移除旧模块 → 执行新代码 → 触发
accept回调。
- 接收通知 → 拉取补丁 → 移除旧模块 → 执行新代码 → 触发
- 关键依赖:
- WebSocket(实时通信) +
hot-update.js(新模块代码) + HMR Runtime(替换逻辑)。
- WebSocket(实时通信) +
- 适用场景:
- 开发环境保留应用状态(如表单输入、UI 组件状态)。
参考答案
Webpack 的 Hot Module Replacement (HMR) 允许在运行时替换、添加或删除模块,而无需完全刷新页面。其核心依赖于 客户端(Browser) 和 服务端(Webpack Dev Server) 的协同工作,整体流程可分为以下几个阶段:
1. 服务端(Webpack Dev Server)的职责
1.1 启动阶段:建立 WebSocket 连接
- Webpack Dev Server (WDS) 启动时,会创建一个 WebSocket 服务,用于与客户端实时通信。
- 同时,WDS 会监听文件变化(通过
chokidar等库),并触发重新编译。
1.2 文件变更后的编译流程
增量编译:
- 当文件修改后,Webpack 触发 增量构建(Incremental Build),生成新的 模块依赖图(Module Graph)。
- 对比新旧依赖图,找出 变动的模块(changed modules)。
- 当文件修改后,Webpack 触发 增量构建(Incremental Build),生成新的 模块依赖图(Module Graph)。
生成补丁文件(HMR Update):
- Webpack 生成一个 JSON 格式的补丁文件(hot-update.json),包含:
{ "c": {"main": "newHash"}, // 变更的 chunk ID 及新 hash<br> "h": "newHotUpdateHash" // 本次 HMR 的唯一标识<br> }<br> ```<br> - 同时生成 JS 补丁文件(hot-update.js),包含变更模块的新代码。
- Webpack 生成一个 JSON 格式的补丁文件(hot-update.json),包含:
推送更新通知:
- WDS 通过 WebSocket 向客户端发送消息:
{ "type": "hash", // 通知客户端新的编译 hash<br> "data": "newHash" }<br> ```<br> 随后发送:<br> ```json<br> { "type": "ok", // 表示编译完成<br> "data": { /* 可能携带警告信息 */ }<br> }<br> ```<br>
- WDS 通过 WebSocket 向客户端发送消息:
2. 客户端(Browser)的职责
2.1 初始化阶段:注册 HMR 运行时
- Webpack 在打包时注入 HMR Runtime 代码(
webpack-hot-middleware/client或webpack-dev-server/client)。 - 客户端初始化时,会:
- 建立 WebSocket 连接(用于接收服务端通知)。
- 向服务端发送
"hot"请求,表明支持 HMR。
- 建立 WebSocket 连接(用于接收服务端通知)。
2.2 接收更新并执行热替换
监听 WebSocket 消息:
- 当收到
"hash"消息时,客户端存储新的编译 hash。 - 当收到
"ok"消息时,客户端触发 检查更新(checkForUpdates)。
- 当收到
请求补丁文件:
- 客户端通过
JSONP或fetch请求hot-update.json和hot-update.js。 - 示例请求 URL:
http://localhost:8080/main.newHash.hot-update.js ```<br>
- 客户端通过
执行模块替换:
hotApply阶段:HMR Runtime 执行关键操作:- 从缓存中移除旧模块。
- 执行新模块代码(
hot-update.js)。 - 调用 模块的
module.hot.accept回调(如果有)。
- 从缓存中移除旧模块。
UI 更新(可选):
- 如果模块定义了
module.hot.accept,开发者可以手动处理更新逻辑(如保留 React/Vue 组件状态)。 - 示例(React + HMR):
if (module.hot) { module.hot.accept('./App', () => { // 重新渲染组件,保留状态<br> render(<App />, document.getElementById('root')); }); } ```<br>
- 如果模块定义了
3. 关键协作流程总结
| 步骤 | 服务端(WDS) | 客户端(Browser) |
|---|---|---|
| 1. 初始化 | 启动 WebSocket 服务 | 连接 WebSocket,注册 HMR |
| 2. 文件变更 | 监听文件 → 增量编译 → 生成补丁文件 | - |
| 3. 通知客户端 | 推送 "hash" 和 "ok" 消息 | 接收消息,触发更新检查 |
| 4. 获取补丁 | 提供 hot-update.json 和 .js | 请求补丁文件 |
| 5. 热替换 | - | 移除旧模块 → 执行新代码 → 触发 accept |
4. 核心实现细节
4.1 模块热替换的边界条件
- 未处理
accept的模块:会向上冒泡,直到找到父模块的accept,否则刷新页面。 - CSS 热更新:通过
style-loader或MiniCssExtractPlugin自动处理,无需手动accept。
4.2 降级策略
- 如果 HMR 失败(如模块报错),客户端会回退到
window.location.reload()。
4.3 性能优化
- 增量构建:仅编译变动的文件(依赖
webpack的缓存机制)。 - 懒加载模块:动态导入(
import())的模块也支持 HMR。
9. 手写一个支持异步链式调用的 Promise 基础版本,需包含 resolve 和 then 方法。
题目要点
- 状态机模型:
pending/fulfilled/rejected三态转换。 - 异步队列:通过回调数组和
setTimeout实现异步执行。 - 链式调用:
then返回新 Promise,递归解析返回值。 - 兼容性:处理
then参数可选和值穿透。
参考答案
实现代码
class MyPromise {
constructor(executor) {
this.state = 'pending'; // 状态:pending, fulfilled, rejected
this.value = undefined; // 成功时的值
this.reason = undefined; // 失败时的原因
this.onFulfilledCallbacks = []; // 成功回调队列
this.onRejectedCallbacks = []; // 失败回调队列
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
// 异步执行所有成功回调
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
// 异步执行所有失败回调
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject); // 立即执行执行器函数
} catch (err) {
reject(err); // 捕获同步错误
}
}
then(onFulfilled, onRejected) {
// 处理 then 的参数可选(值穿透)
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
// 异步执行 onFulfilled(模拟微任务)
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else if (this.state === 'rejected') {
// 异步执行 onRejected
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else if (this.state === 'pending') {
// 订阅回调(支持异步操作)
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2; // 返回新 Promise 实现链式调用
}
}
// 处理 then 的返回值(支持 Promise 链)
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'));
}
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else {
resolve(x);
}
}
关键实现解析
状态管理
pending→fulfilled(成功)或rejected(失败),状态不可逆。- 通过
resolve/reject修改状态并触发回调。
异步支持
- 使用
setTimeout模拟微任务队列(实际 Promise 使用微任务)。 - 在
pending时订阅回调,resolve/reject后异步执行。
- 使用
链式调用
then返回新 Promise(promise2)。resolvePromise处理返回值:- 普通值 → 直接
resolve。 - Promise → 等待其状态变更。
- 普通值 → 直接
错误处理
- 执行器(
executor)和then回调中的错误会被捕获并reject。
- 执行器(
使用示例
const p = new MyPromise((resolve) => {
setTimeout(() => resolve('success'), 1000);
});
p.then((res) => {
console.log(res); // 1秒后输出 "success"
return 'chain';
}).then((res) => {
console.log(res); // 输出 "chain"
});
10. 给定数组 nums,判断是否存在三个元素 a, b, c 使得 a + b + c = 0
题目要点
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 排序 + 双指针 | O(n²) | O(1) | 一般情况(最优解) |
| 哈希表 | O(n²) | O(n) | 需快速查找(但去重较麻烦) |
参考答案
方法一:排序 + 双指针(最优解)
时间复杂度:O(n²)
空间复杂度:O(1)(不考虑结果存储空间)
算法步骤
- 排序数组(方便去重和双指针移动)。
- 遍历数组,固定一个数
nums[i],然后在剩余部分用双指针找nums[left] + nums[right] = -nums[i]。 - 去重处理:
- 如果
nums[i] === nums[i-1],跳过(避免重复三元组)。 - 在双指针移动时,跳过重复的
left和right。
- 如果
代码实现
function threeSum(nums) {
const result = [];
nums.sort((a, b) => a - b); // 排序
for (let i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue; // 跳过重复的 nums[i]
let left = i + 1;
let right = nums.length - 1;
const target = -nums[i];
while (left < right) {
const sum = nums[left] + nums[right];
if (sum === target) {
result.push([nums[i], nums[left], nums[right]]);
// 跳过重复的 left 和 right
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;
left++;
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
return result;
}
方法二:哈希表(辅助优化)
时间复杂度:O(n²)
空间复杂度:O(n)(哈希表存储)
算法步骤
- 排序数组(方便去重)。
- 双层循环:
- 外层遍历
nums[i]。 - 内层用哈希表记录
target = -nums[i] - nums[j]是否出现过。
- 外层遍历
代码实现
function threeSum(nums) {
const result = [];
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue;
const seen = new Set();
const target = -nums[i];
for (let j = i + 1; j < nums.length; j++) {
const complement = target - nums[j];
if (seen.has(complement)) {
result.push([nums[i], complement, nums[j]]);
while (j + 1 < nums.length && nums[j] === nums[j + 1]) j++; // 去重
}
seen.add(nums[j]);
}
}
return result;
}
边界情况处理
- 数组长度 < 3:直接返回
[]。 - 全零数组:如
[0, 0, 0],返回[[0, 0, 0]]。 - 无解情况:如
[1, 2, 3],返回[]。
11. 设计一个地图搜索框的自动补全功能
题目要点
| 模块 | 技术选型 | 优化点 |
|---|---|---|
| 前端交互 | 防抖 + 键盘导航 | 减少请求、无障碍支持 |
| 后端 API | Elasticsearch + Redis | 快速检索、结果缓存 |
| 数据排序 | 热度 + 距离 + 个性化 | 提升结果准确性 |
| 扩展能力 | 语音/多源搜索 | 增强用户体验 |
参考答案
1. 核心功能需求
- 输入联想:根据用户输入实时匹配候选结果(如地点、POI)。
- 结果排序:按相关性(如搜索热度、距离)排序。
- 异步加载:高效处理高并发请求,避免频繁触发搜索。
- 防抖优化:减少无效请求,提升性能。
- 键盘导航:支持
↑/↓选择、Enter确认。
2. 技术实现方案
2.1 前端实现
UI 组件结构:
<div class="search-container">
<input
type="text"
id="search-input"
placeholder="搜索地点..."
autocomplete="off"
/>
<ul id="suggestions-list" class="suggestions-hidden"></ul>
</div>
关键逻辑:
输入监听与防抖:
const input = document.getElementById('search-input'); let debounceTimer; input.addEventListener('input', () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { if (input.value.trim()) fetchSuggestions(input.value); }, 300); // 防抖 300ms });结果渲染:
function renderSuggestions(results) { const list = document.getElementById('suggestions-list'); list.innerHTML = results.map(item => ` <li data-id="${item.id}">${item.name} <small>${item.address}</small></li> `).join(''); list.classList.remove('suggestions-hidden'); }键盘导航:
input.addEventListener('keydown', (e) => { const items = document.querySelectorAll('#suggestions-list li'); let current = document.querySelector('.suggestion-active'); switch (e.key) { case 'ArrowDown': e.preventDefault(); current?.classList.remove('suggestion-active'); const next = current?.nextElementSibling || items[0]; next?.classList.add('suggestion-active'); break; case 'Enter': if (current) { input.value = current.textContent.trim(); list.classList.add('suggestions-hidden'); } break; } });
2.2 后端 API 设计
请求示例:
GET /api/suggestions?query=北京&limit=5&lat=39.9&lng=116.4
响应格式:
{
"results": [
{
"id": "poi-123",
"name": "北京故宫",
"address": "北京市东城区景山前街4号",
"distance": "2.5km"
}
]
}
优化策略:
- 缓存:使用 Redis 缓存高频查询结果。
- 分词优化:结合 Elasticsearch 或地理编码服务(如高德/Google Places API)。
- 个性化排序:结合用户历史搜索数据调整权重。
3. 性能与体验优化
- 预加载热点数据:首页加载时缓存热门地点。
- 懒加载图片:候选结果的图标延迟加载。
- 离线支持:本地存储历史记录,无网络时展示。
- 错误降级:API 失败时显示本地历史记录。
4. 高级功能扩展
- 多源搜索:同时搜索地点、公交线路、商家。
- 语音输入:集成 Web Speech API。
- 地理围栏提示:输入时自动提示附近地点。