前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
小程序题库
  • 每日一题每天一道,攒手感
  • 原理篇React / Vue 源码拆解
  • 知识卡片NEW碎片时间过考点
  • 历年面经按年份追踪真实考点
  • 算法题库在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
小程序题库
  • 每日一题每天一道,攒手感
  • 原理篇React / Vue 源码拆解
  • 知识卡片NEW碎片时间过考点
  • 历年面经按年份追踪真实考点
  • 算法题库在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • 前端业务场景题汇总
    • JavaScript 业务逻辑
      • 购物车金额相加出现很多小数,怎样算得准确?
      • 接口里的长订单号末尾变了,为什么转字符串也恢复不了?
      • 用户选了某一天,页面为什么显示成前一天?
      • 评论截断后表情被切成两半,怎么正确处理?
      • 商品切换排序后回不到原顺序,哪里改错了?
      • 商品排序比较函数返回 true 或 false,为什么结果会乱?
      • 复制配置后只改一个页面,为什么其他页面也变了?
      • 分页列表怎样去重,又保留最新资料和原来顺序?
      • 搜索 C++ 或括号时高亮报错,怎么修?
      • 实例方法传给定时器后读不到 this,怎么办?
      • 用户把配置设为 0,为什么还是用了默认值?
      • 接口返回扁平菜单,怎样高效转成树?
      • 页面进出几次后,同一次操作触发多次回调,怎么查?
      • 缓存用了对象作键,改了对象字段为什么还拿到旧结果?
      • 导出表格的内容有逗号、引号和换行,怎样避免错列?
      • 筛选条件怎样写进链接,才能分享和返回后恢复?
      • 目录太深导致递归栈溢出,改成 async 能解决吗?
      • 金额输入框输入小数点或清空时总跳动,怎么处理?
      • 撤销重做为什么不能只记修改后的值?
      • 判断商品规格能否点击时,为什么要先替换当前维度?
    • TypeScript 类型设计
    • HTML 与 CSS
    • 布局与响应式
    • 浏览器与事件
    • 异步与网络请求
    • React 状态与渲染
    • React 项目实践
    • Vue 响应式与组件
    • Vue 项目实践
    • SSR 与 Next.js
    • 前端性能优化
    • 构建与工程化
    • 前端测试
    • Web 安全
    • 登录与权限
    • 数据与状态管理
    • 表单与业务表格
    • 文件上传与多媒体
    • 实时通信
    • 存储与离线
    • 移动端 H5
    • 小程序
    • 跨端与原生集成
    • 组件与设计系统
    • 前端架构
    • 可视化与图形
    • 无障碍与国际化
    • Node.js 与 BFF
    • 发布与可观测性
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

前端场景面试题汇总:业务实战、代码示例与回答思路

# 前端场景面试题汇总

# JavaScript 业务逻辑

金额、日期、字符串、集合与数据转换

# 购物车金额相加出现很多小数,怎样算得准确?

30 秒速记

  • 先区分输入、计算和展示:二进制浮点误差不能只在视图层遮盖。
  • 人民币两位小数可从十进制字符串直接解析为整数分,统一计算边界。
  • 优惠、税率和退款要约定舍入时机;服务端保存权威结算结果。

根因是二进制浮点无法精确表示部分十进制小数。金额计算统一使用最小货币单位整数或十进制定点库,展示时才格式化;不能在每次加法后随意四舍五入,否则多商品累计误差仍会出现。

业务场景 · 故障排查

购物车支持小数单价、满减和退款,前后端偶尔相差一分钱,需要统一计算和展示规则。

回答思路

金额计算要统一单位和舍入规则,不能只在页面最后格式化两位小数。常见做法是用最小货币单位的整数计算,并检查安全范围;展示再单独格式化。

先区分三个层次:接口原始金额是否正确、计算过程是否产生误差、页面是否只需要格式化展示。不要看到长小数就立即修改组件中的 toFixed,否则价格缓存、优惠门槛和退款计算仍可能使用错误值。

如果业务限定人民币两位小数,我会让接口以十进制字符串传金额,在统一的金额模块中解析成分;加减和乘整数数量在这个模块内完成。涉及税率、汇率或比例优惠时,需要另外约定中间精度、舍入方向和舍入时机,不能把“全部转成整数”当成完整的结算规则。

具体示例

两件商品分别为 0.10 元和 0.20 元,预期总额是 30 分。如果页面直接计算 0.1 + 0.2,可能展示出长小数。更隐蔽的情况是多件商品逐项折扣后取整,与先求和再折扣相差一分钱:这是舍入规则不同,不能仅靠换数据类型修复。

下面的例子限定输入为非负人民币金额,拒绝超过两位小数,不默默截断。字符串解析发生在算术之前,避免先经过浮点数再乘 100。

代码示例

function parseCents(value) {
  if (typeof value !== 'string' || !/^\d+(\.\d{1,2})?$/.test(value)) {
    throw new TypeError('金额必须是最多两位小数的非负字符串');
  }
  const [whole, fraction = ''] = value.split('.');
  return BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0'));
}

function formatCents(value) {
  const sign = value < 0n ? '-' : '';
  const cents = value < 0n ? -value : value;
  return `${sign}${cents / 100n}.${String(cents % 100n).padStart(2, '0')}`;
}

const total = parseCents('0.10') + parseCents('0.20');
console.assert(total === 30n);
console.assert(formatCents(total) === '0.30');
console.assert(formatCents(parseCents('19.99') * 3n) === '59.97');
@前端进阶之旅: 代码已经复制到剪贴板
  • 用 Number.EPSILON 修补所有金额不可靠,它不是随金额大小自动变化的业务容差。
  • Math.round(Number(value) * 100) 仍然先经过二进制浮点解析,不能保证所有输入都满足约定的十进制舍入规则。

面试官追问

追问 1前端使用了整数,后端还需要再次校验金额吗?
参考回答

需要。浏览器中的单价、数量和优惠都能被修改;服务端必须以可信商品和活动数据核算,前端负责交互预估及解释差异,不是结算的权威来源。

追问 2页面已经显示 0.30 元,但满减门槛仍偶尔判断错,你会检查哪里?
参考回答

检查参与门槛比较的原始计算值与单位,确认不是只有渲染函数做了格式化。门槛、商品汇总与优惠计算必须使用同一个金额类型和舍入规则,不能一处比较元、一处比较分。

追问 3现在商品批发单价允许四位小数,原来的分整数方案还能直接用吗?
参考回答

不能在读取单价时就压到分,否则数量乘法前已经损失精度。应保存四位小数的价格精度,乘数量并按约定时机舍入到最终结算单位;接口还要区分单价精度与实付精度。

追问 4后端说先求总折扣,前端想逐件折扣便于展示,两边差一分钱怎么处理?
参考回答

先统一权威结算规则。若规则是订单级折扣,前端展示应读取或复现订单级分摊结果,并把尾差归到确定的行,不能分别算完后把差额简单隐藏。

# 接口里的长订单号末尾变了,为什么转字符串也恢复不了?

30 秒速记

  • 先比较原始响应与解析后的值,确定精度在哪个边界丢失。
  • 订单标识不参与算术,应从服务端开始按字符串传输和保存。
  • 把不安全数字转成字符串不会恢复原值,必须重新取得可信数据。

如果 JSON.parse 已把订单号解析成不安全的数字,精度损失已经发生,之后 String 只会保存错误值。应让服务端按字符串返回标识符,并检查网络原始响应与解析后对象来确认丢失发生在哪一步。

业务场景 · 故障排查

接口返回十八位以上的订单号,页面显示与客服系统不一致,复制后也无法查单。

回答思路

长订单号应从接口开始就作为字符串传输。它一旦先被解析成超出安全范围的数字,末尾精度已经丢了,之后再转字符串只会保存错误结果。

排查时先看网络面板中的原始响应,而不是只看已经打印出来的对象。后端可能已经按数字输出了正确的十进制文本,但浏览器解析 JSON 时把它装进了不能精确表示该值的 Number,因此对象里看到的尾号已经变化。

接着追踪标识符进入表单、状态、缓存、路由和日志的过程。如果原始响应本身就是错误尾号,问题发生得更早,需要查后端序列化或存储。修复必须覆盖最早丢失精度的边界,不能只在展示组件加 String(id)。

具体示例

接口返回订单号 9007199254740993,页面却得到 9007199254740992。客服搜索原始订单号能查到,用户复制页面尾号却查不到。两串只差一位,但它们是不同标识,不能按照“数字很接近”认为可接受。

下面同时展示错误协议和修正协议。订单号虽然由数字字符组成,业务语义仍然是不透明标识,因此修正为字符串并不会损失所需能力。

代码示例

const broken = JSON.parse('{"orderId":9007199254740993}');
console.assert(String(broken.orderId) === '9007199254740992');
console.assert(!Number.isSafeInteger(broken.orderId));

const response = '{"orderId":"9007199254740993"}';
const order = JSON.parse(response);
if (typeof order.orderId !== 'string') throw new TypeError('订单号协议错误');

const cached = JSON.parse(JSON.stringify(order));
const search = new URLSearchParams({ orderId: cached.orderId });
console.assert(cached.orderId === '9007199254740993');
console.assert(search.get('orderId') === order.orderId);
console.assert(BigInt(order.orderId) === 9007199254740993n);
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1网络原始响应里的尾号已经错了,前端改字符串还有用吗?
参考回答

只能防止后续继续损坏,无法恢复已经错误的来源数据。需要沿后端序列化、服务调用和存储继续追踪,找到可信原值后重新返回。

追问 2在 JSON.parse 的 reviver 里把订单号转字符串,能避免这个问题吗?
参考回答

如果仅使用已经传入回调的数字值,精度损失已经发生。不能靠这个数字恢复原文;应修改字段协议,或使用明确保留数字原始文本的解析能力与兼容方案。

追问 3用户复制的尾号看起来正确,但搜索仍查不到,你接着查哪里?
参考回答

查看实际发送的搜索参数,确认输入组件、请求拦截器或序列化过程没有再次把字符串转为数字。逐段比较同一订单号的完整字符,找到第一个不一致的边界。

追问 4全部改成 BigInt 保存订单号是不是更好?
参考回答

通常没有必要。订单号不是算术值,还可能有前导零或未来增加字母;字符串更符合标识语义,也便于 JSON 传输。只有确实需要数值比较的局部逻辑才考虑额外转换。

# 用户选了某一天,页面为什么显示成前一天?

30 秒速记

  • 先确认字段表示日历日期,还是全球唯一时间点。
  • 只有年月日的预约保存为规范字符串,不能借助本机时区转换。
  • 格式校验之外还要检查真实日历,拒绝不存在的日期。

纯日期被错误当成了时间点,再经过时区转换导致跨日。预约日期应作为 YYYY-MM-DD 的日历日期处理;若业务需要具体时刻,需同时明确门店时区,再转换成统一时间点存储。

业务场景 · 故障排查

用户在境外预约国内门店,选择周五后确认页变成周四,运营按门店当地日期排班。

回答思路

只表示年月日的预约日期不要随意转成一个时刻。先分清业务要的是日历日期还是精确时间,再约定时区与存储格式,否则UTC解析和本地展示可能跨到前一天。

这类故障通常不是接口丢了一天,而是把“门店营业日”错误建模成了“某个时间点”。Date 保存时间戳,格式化时才投影到时区;标准的纯日期字符串会按协调世界时零点解释,位于负时区的浏览器随后按本地日期显示,就可能落在前一天。先抓接口原文,再比较时间戳、格式化时区和显示结果,不能直接在结果上加一天。

如果用户只选日期,不需要跨时区排序到小时,我会保持 YYYY-MM-DD 的领域值,从输入、状态到请求都使用同一个字符串。边界校验要同时验证形状和月份天数,避免宽松日期解析自动把二月三十日滚动到三月。只有用户进一步选择了门店时段,才在明确门店时区后将它解析为时间点。

具体示例

预约字段为 2026-01-01,业务含义是门店一月一日的名额。把它转为零点时间戳后,在洛杉矶格式化可能显示十二月三十一日。下面的解析器只返回原日历值,不产生时区换算;2024-02-29 被接受,2026-02-29 被拒绝。它限定年份为四位且不接受零年,适合普通业务预约。

代码示例

function parseBookingDate(value) {
  if (typeof value !== 'string') throw new TypeError('日期必须是字符串');
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
  if (!match) throw new Error('使用 YYYY-MM-DD');
  const [year, month, day] = match.slice(1).map(Number);
  const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
  const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  if (year < 1 || month < 1 || month > 12 || day < 1 || day > days[month - 1]) {
    throw new RangeError('日期不存在');
  }
  return value;
}
console.assert(parseBookingDate('2024-02-29') === '2024-02-29');
let rejected = false;
try { parseBookingDate('2026-02-29'); } catch { rejected = true; }
console.assert(rejected);
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1把时分秒固定成中午能彻底解决吗?
参考回答

不能。中午只是避开了部分时区的日期边界,跨度足够大的时区组合仍会跨日,而且人为引入了业务并不存在的时间。应修正字段语义,不能依赖某个魔法小时。

追问 2日期字符串可以直接比较大小吗?
参考回答

经过校验且统一为四位年、两位月日时,字典序与日历先后相同。混入未补零的月份、不同历法或带时分秒值后不再满足这个前提,需要先统一契约。

追问 3生日字段也应该这样处理吗?
参考回答

只记录出生年月日时可以按日历值处理;若业务确实要出生时间与地点,则是额外的时间和时区字段。不能因为两者都叫日期就统一塞进同一个时间戳字段。

追问 4后端已经要求毫秒时间戳怎么办?
参考回答

需要推动区分日期型字段与时间点字段的接口版本。过渡期可约定明确的编码时区并集中适配,所有消费者使用同一规则;不能由每个浏览器自行解释,文档中要标明这只是日期编码。

# 评论截断后表情被切成两半,怎么正确处理?

30 秒速记

  • 截断沿字素边界进行,不能按编码下标切片。
  • 先约定省略号是否占用展示名额,再计算可见预算。
  • 保留完整原文供展开、复制与无障碍阅读。

用 Intl.Segmenter 的 grapheme 粒度分段,再按分段数量截取,并为不支持的目标环境提供经过验证的字素分段实现。Array.from 只能按码点处理,不能保证完整保留组合表情。

业务场景 · 方案设计

评论限制一百个用户可见字符,用户输入表情或组合字符时,字数统计和截断显示不一致。

回答思路

不要按字符串下标直接截用户看到的字符。表情和组合字符可能占多个编码单元,应按可见字符边界分段,再决定截断和字数限制。

先把“预览最多十个字”和“内容最多十个字”分开。评论预览只是展示变换,原文必须完整保存;截断范围应落在字素簇边界,否则可能留下半个代理对、单独的肤色修饰符或组合重音。slice(0, n) 使用编码单元下标,不适合直接实现这种业务规则。

实现时按字素遍历,在达到预算后多读取一个字素判断是否需要省略号。若设计要求省略号也计入总长度,预先为其留一个位置;如果没有发生截断就不加省略号。不要为了判断是否超限先把整篇文章完全展开成数组,短摘要只需要读取足够数量的字素即可。

具体示例

预算为三个可见字符,输入 前端🙂很好 时显示 前端…;输入 前端🙂 刚好占满预算,应原样显示。家庭表情占一个名额,不会被拆开。下面实现将省略号计入预算,并限定预算为非负整数,零预算返回空字符串。

代码示例

const segments = new Intl.Segmenter('zh-CN', { granularity: 'grapheme' });
function preview(text, budget) {
  if (!Number.isInteger(budget) || budget < 0) throw new RangeError('预算无效');
  if (budget === 0) return '';
  const parts = [];
  for (const { segment } of segments.segment(text)) {
    parts.push(segment);
    if (parts.length > budget) return parts.slice(0, budget - 1).join('') + '…';
  }
  return parts.join('');
}
console.assert(preview('前端🙂很好', 3) === '前端…');
console.assert(preview('前端🙂', 3) === '前端🙂');
console.assert(preview('👨‍👩‍👧‍👦好吗', 2) === '👨‍👩‍👧‍👦…');
console.assert(preview('文本', 0) === '');
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1预算为一个字符时怎么办?
参考回答

若原文确实超限且省略号计入预算,就只显示省略号;原文只有一个字素则原样显示。这种窄空间体验应由产品确认,不能出现负下标误截断。

追问 2直接使用样式省略号是否更好?
参考回答

固定宽度的单行或多行预览可以使用样式裁切,它以布局宽度为目标。按“字数”返回摘要、服务端摘要和通知文本则需要分段算法,两者解决的约束不同。

追问 3摘要后面的复制按钮应复制什么?
参考回答

通常复制完整原文,并在交互文案中说明。若只复制摘要,也要明确是摘要内容。不能因为展示组件只拿到了截断字符串,就无意丢掉用户希望复制的信息。

追问 4截断后的文本还需要转义吗?
参考回答

需要在输出层按文本渲染。字素分段只保证字符完整,不会消除标签和脚本。不要把截断结果拼成原始标记插入页面,搜索高亮也应通过安全节点构造。

# 商品切换排序后回不到原顺序,哪里改错了?

30 秒速记

  • 数组排序会修改原数组,推荐顺序要保存独立来源。
  • 价格排序是派生视图,不要回写服务器提供的推荐列表。
  • 排序不复制行对象,修改商品字段仍需处理共享引用。

Array.prototype.sort 会原地修改数组,共享的原始列表可能被覆盖。使用复制后排序或目标环境支持的 toSorted,把原始推荐顺序保留为独立数据,不依赖被多处引用的数组继续当基线。

业务场景 · 故障排查

商品列表先按价格排序,同价商品保留推荐顺序;用户切换排序后原始推荐顺序也跟着变了。

回答思路

sort 会修改原数组。保留原始推荐顺序,排序时复制数组或生成独立结果,不要让展示排序改掉共享数据。

先看推荐列表与当前展示列表是否指向同一个数组。sort 会就地重新排列元素,如果把接口数组直接交给价格排序,原来的推荐顺序就已经被覆盖;再切回“推荐”时读取同一数组,自然无法恢复。这个问题与排序是否稳定无关,稳定排序只保证比较相等的元素保持当前相对顺序,不会记住更早的推荐排名。

我会把接口顺序保存在权威列表中,当前排序模式只参与计算派生视图。可以复制数组后排序,或使用目标环境支持的非修改排序方法。若推荐排名会动态更新,则保存服务端给出的排名或版本,每次按该版本重建,不依赖用户之前切换过哪些排序按钮。

具体示例

接口顺序是商品甲、乙、丙,价格分别为三十、十、二十。价格升序应得到乙、丙、甲,但切回推荐仍应是甲、乙、丙。下面通过创建新数组实现,断言原始数组顺序未变;商品对象本身仍共享,仅做排序时不需要深复制。

代码示例

const recommended = [
  { id: 'a', price: 30 }, { id: 'b', price: 10 }, { id: 'c', price: 20 }
];
function viewProducts(products, mode) {
  if (mode === 'recommended') return [...products];
  const direction = mode === 'price-desc' ? -1 : 1;
  return [...products].sort((a, b) => direction * (a.price - b.price));
}
const priced = viewProducts(recommended, 'price-asc');
console.assert(priced.map(x => x.id).join(',') === 'b,c,a');
console.assert(recommended.map(x => x.id).join(',') === 'a,b,c');
console.assert(viewProducts(recommended, 'recommended')[0].id === 'a');
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1复制数组后为什么编辑价格还会影响推荐列表?
参考回答

复制的是元素引用排列,行对象仍然共享。排序只移动引用没有问题;如果要修改商品字段,应按状态模型创建新对象或通过统一更新动作,不能误以为数组复制等于完整快照。

追问 2反向排序能恢复推荐顺序吗?
参考回答

不能。推荐排名与价格可能无关,价格相同的商品还会丢失恢复依据。必须保存原始推荐顺序或明确排名字段,不能从另一种排序结果反推出业务排序。

追问 3使用稳定排序是不是就不用复制?
参考回答

不是。稳定性约束的是比较相等元素的顺序,修改性约束的是是否改变输入数组,二者独立。稳定排序仍可能破坏原始推荐列表。

追问 4列表接到实时商品更新后如何保持当前模式?
参考回答

先按商品标识更新权威数据,再依据当前模式重新派生展示。若服务端推荐版本变化,更新对应排名;不要直接在已按价格排列的数组里追加后当成新推荐顺序。

# 商品排序比较函数返回 true 或 false,为什么结果会乱?

30 秒速记

  • 比较器需要负数、零、正数表达先后与相等。
  • 布尔结果无法同时表达两个方向,会破坏反对称性。
  • 价格先规范化为有限数值,同价再使用稳定业务次序。

比较器应对小于、相等、大于分别返回负数、零、正数,并满足一致性。布尔值只产生零或一,不能表达反向顺序;应先规范价格类型,再比较数值,相同价格返回零以保留原顺序。

业务场景 · 代码实现

商品列表先按价格排序,同价商品保留推荐顺序;用户切换排序后原始推荐顺序也跟着变了。

回答思路

排序比较函数需要表达小于、相等和大于,布尔值只能表达两种结果。先明确升降序、相等时的次序和缺失值规则,再返回负数、零或正数。

比较器不是筛选条件。a.price > b.price 返回布尔值,转换后只有零和一:当甲应该排在乙前面时返回零,算法会把它当成相等;交换参数后又可能返回正数,导致比较关系不一致。不同排序实现可能以不同顺序调用比较器,所以偶尔看起来正确不能证明这个比较器有效。

正确实现先明确升降序,对合法数值返回差值或显式三路比较;对于缺失价格、非数值和同价商品另外约定规则。比较器应该纯粹,只读取当前两个元素,不修改数据或依赖随机状态,否则同一对元素多次比较的结果也可能不同。

具体示例

价格为二十、十、二十的三个商品,升序先放十元商品,同价按稳定标识排序。下面显式返回三种符号,并断言交换参数后符号反转、相同元素比较为零。示例只接受已经规范化的有限价格,缺失值应在更外层处理。

代码示例

function comparePrice(a, b) {
  if (!Number.isFinite(a.price) || !Number.isFinite(b.price)) {
    throw new TypeError('价格尚未规范化');
  }
  if (a.price !== b.price) return a.price < b.price ? -1 : 1;
  return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
}
const a = { id: 'a', price: 20 };
const b = { id: 'b', price: 10 };
const c = { id: 'c', price: 20 };
console.assert(comparePrice(a, b) === 1);
console.assert(comparePrice(b, a) === -1);
console.assert(comparePrice(a, a) === 0);
console.assert([c, a, b].sort(comparePrice).map(x => x.id).join(',') === 'b,a,c');
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1比较器必须返回负一和正一吗?
参考回答

不必须,只看返回值的符号,数值差通常可以使用。但需要保证输入不会产生无效结果,并且业务允许按该数值比较;价格缺失时直接相减可能得到非数值。

追问 2同价时返回零就有问题吗?
参考回答

不一定。若允许保持输入顺序,返回零是合理的;如果跨分页和多次请求需要完全确定的顺序,则应加稳定的唯一标识作为次级比较。

追问 3比较器内部更新商品缓存可以吗?
参考回答

不宜。排序可能重复比较同一元素,调用次数和顺序不应承担业务副作用。先准备计算字段和缓存,再用纯比较器读取,便于验证且避免不一致。

追问 4只在当前浏览器表现异常,能算兼容性问题吗?
参考回答

先检查比较器是否满足合法顺序关系。非法比较器的结果本来就不可靠,换浏览器只是暴露了问题,不应针对某个浏览器打补丁或依赖它当前恰好得到的排列。

# 复制配置后只改一个页面,为什么其他页面也变了?

30 秒速记

  • 对象展开只复制第一层,嵌套配置仍可能指向默认对象。
  • 按配置领域创建租户独立值,共享部分必须保证不可变。
  • 测试修改嵌套字段之后默认配置和其他租户都没有变化。

对象展开只复制第一层,嵌套对象仍共享引用。定位写入的具体路径,采用按路径不可变更新或按明确数据类型深复制;开发期冻结默认配置帮助发现误写,不能把浅拷贝误当隔离全部状态。

业务场景 · 故障排查

后台允许租户配置主题和功能开关,服务端默认配置与租户配置合并后,其他租户偶尔也受影响。

回答思路

展开运算符只复制外层,嵌套对象仍可能共用引用。先找到共享的那一层,再按业务结构复制或做不可变更新,不要把浅复制当成完整隔离。

先用引用相等检查默认配置与租户配置的嵌套对象。{ ...defaults } 会创建外层对象,但里面的主题、导航数组等仍是原引用,因此修改某个租户的主题色也会修改默认值。下一个租户从已经被污染的默认值初始化,就出现跨租户串色,重载页面后现象还可能暂时消失。

对于纯数据配置,可以在创建租户实例时克隆需要隔离的结构;更好的边界是把默认配置视为不可变输入,合并器返回全新领域对象。不能仅在外层冻结后认为问题解决,也不能每次渲染都深复制全部数据。配置生成和更新应集中执行,组件只读取结果。

具体示例

默认配置包含主题颜色和导航数组。下面使用明确字段构建两个租户的配置,随后修改甲租户的颜色和导航,断言默认值及乙租户未变。示例限定配置结构,因此比声称能复制任意对象的通用深拷贝更容易审核。

代码示例

const defaults = { theme: { color: 'blue' }, links: [{ title: '首页', href: '/' }] };
function createTenant(base) {
  return { theme: { ...base.theme }, links: base.links.map(link => ({ ...link })) };
}
const tenantA = createTenant(defaults);
const tenantB = createTenant(defaults);
tenantA.theme.color = 'red';
tenantA.links[0].title = '甲首页';
console.assert(defaults.theme.color === 'blue');
console.assert(tenantB.theme.color === 'blue');
console.assert(defaults.links[0].title === '首页');
console.assert(tenantA.theme !== tenantB.theme);
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1每次更新都深拷贝整个配置是否最保险?
参考回答

它能减少部分引用串改,但会增加分配成本并破坏引用稳定性。对已知配置结构只复制发生变化的路径,保持其他不可变分支共享,更容易控制性能和更新范围。

追问 2默认对象加外层冻结为什么仍可能串改?
参考回答

外层冻结只限制外层属性,主题对象和导航项仍可修改。可以对已知纯数据树递归冻结用于开发期发现违规,但冻结不替代清晰的更新接口。

追问 3配置里有函数和类实例怎么办?
参考回答

先确定它们是否应出现在租户数据配置中。行为对象不应当作普通数据盲目克隆;可以把函数注册表与可序列化配置分离,通过标识关联,减少复制语义的不确定性。

追问 4为什么单租户测试容易漏掉这个问题?
参考回答

第一个租户修改后自己的界面仍符合预期,只有默认值复用或第二个租户读取共享对象时才暴露。测试需要同时保留默认值和另一实例,并检查嵌套字段及数组元素。

# 分页列表怎样去重,又保留最新资料和原来顺序?

30 秒速记

  • 映射更新已有键不会改变首次插入位置。
  • 资料新旧优先比较服务端版本,不能默认最后到达就是最新。
  • 输出稳定位置与最新实体分开考虑,避免列表更新时跳动。

按输入顺序遍历,用用户标识写入 Map。第一次插入决定键的顺序,后续同键更新值不会移动位置;若最新指更新时间而非后到的数据,应比较版本或时间后再覆盖,防止旧页覆盖新资料。

业务场景 · 代码实现

联系人列表来自多个分页接口,同一用户可能重复返回,用户希望保留最新资料并维持首次出现的顺序。

回答思路

用稳定用户ID去重,不用对象引用判断同一个人。Map可以保留首次插入位置,同时用后来的资料更新该ID对应值。

这个需求有两个维度:位置按首次出现,资料按最新版本。Map 的插入顺序可以保存首次位置,对已有键执行 set 更新值不会移动其位置;如果先删除再插入,就会把联系人移到末尾,违背需求。数组每次过滤再追加也有同样问题。

但“最新资料”不能单靠网络返回顺序判断。请求可能乱序,后一页也可能缓存了更旧快照,因此要使用服务端单调版本或明确更新时间规则。相同版本冲突需要固定策略并记录异常,不能随机覆盖。联系人列表的排序规则若另有定义,则首次位置也应服从产品契约。

具体示例

输入先出现甲旧资料,再出现乙,最后出现甲新资料及甲迟到旧资料。结果顺序仍是甲、乙,而甲显示版本三。下面对同标识只接受更高版本;版本相等保留已接收值,避免非确定覆盖。

代码示例

function mergeContacts(records) {
  const byId = new Map();
  for (const record of records) {
    const previous = byId.get(record.id);
    if (!previous || record.version > previous.version) {
      byId.set(record.id, { ...record });
    }
  }
  return [...byId.values()];
}
const result = mergeContacts([
  { id:'a', version:1, name:'甲旧' }, { id:'b', version:1, name:'乙' },
  { id:'a', version:3, name:'甲新' }, { id:'a', version:2, name:'迟到旧值' }
]);
console.assert(result.map(x=>x.id).join() === 'a,b');
console.assert(result[0].name === '甲新');
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1更新时间字符串可以直接比较吗?
参考回答

只有格式、精度和时区完全统一且保证可排序时才行。更稳妥的是服务端提供单调版本;来自不同设备的本地时间不适合决定新旧,因为设备时钟可能不一致。

追问 2资料删除后又出现,位置要保留吗?
参考回答

需要定义删除语义。如果是临时过滤可保留位置索引;如果实体真正删除后重新加入,可能应视为新出现。不要由是否调用映射删除方法偶然决定产品行为。

追问 3新资料只有部分字段,能直接覆盖整条吗?
参考回答

不能在未说明契约时这样做。完整快照可以替换;局部补丁要有字段清除、缺失继承和版本规则,避免把旧字段误丢或把已经清除的字段合回来。

追问 4分页本身按更新时间排序,还需要首次位置吗?
参考回答

可能不需要。若产品承诺按更新时间动态排序,新版本应进入对应位置。首次位置稳定与动态排序是不同体验,要先确认需求,不能同时默认满足。

加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
加载中...
fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
小程序题库
  • 每日一题每天一道,攒手感
  • 原理篇React / Vue 源码拆解
  • 知识卡片NEW碎片时间过考点
  • 历年面经按年份追踪真实考点
  • 算法题库在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • 前端业务场景题汇总
    • JavaScript 业务逻辑
      • 购物车金额相加出现很多小数,怎样算得准确?
      • 接口里的长订单号末尾变了,为什么转字符串也恢复不了?
      • 用户选了某一天,页面为什么显示成前一天?
      • 评论截断后表情被切成两半,怎么正确处理?
      • 商品切换排序后回不到原顺序,哪里改错了?
      • 商品排序比较函数返回 true 或 false,为什么结果会乱?
      • 复制配置后只改一个页面,为什么其他页面也变了?
      • 分页列表怎样去重,又保留最新资料和原来顺序?
      • 搜索 C++ 或括号时高亮报错,怎么修?
      • 实例方法传给定时器后读不到 this,怎么办?
      • 用户把配置设为 0,为什么还是用了默认值?
      • 接口返回扁平菜单,怎样高效转成树?
      • 页面进出几次后,同一次操作触发多次回调,怎么查?
      • 缓存用了对象作键,改了对象字段为什么还拿到旧结果?
      • 导出表格的内容有逗号、引号和换行,怎样避免错列?
      • 筛选条件怎样写进链接,才能分享和返回后恢复?
      • 目录太深导致递归栈溢出,改成 async 能解决吗?
      • 金额输入框输入小数点或清空时总跳动,怎么处理?
      • 撤销重做为什么不能只记修改后的值?
      • 判断商品规格能否点击时,为什么要先替换当前维度?
    • TypeScript 类型设计
    • HTML 与 CSS
    • 布局与响应式
    • 浏览器与事件
    • 异步与网络请求
    • React 状态与渲染
    • React 项目实践
    • Vue 响应式与组件
    • Vue 项目实践
    • SSR 与 Next.js
    • 前端性能优化
    • 构建与工程化
    • 前端测试
    • Web 安全
    • 登录与权限
    • 数据与状态管理
    • 表单与业务表格
    • 文件上传与多媒体
    • 实时通信
    • 存储与离线
    • 移动端 H5
    • 小程序
    • 跨端与原生集成
    • 组件与设计系统
    • 前端架构
    • 可视化与图形
    • 无障碍与国际化
    • Node.js 与 BFF
    • 发布与可观测性