ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

面试被问原理卡壳?手写实现全能单位换算器搞定

面试被问原理卡壳?手写实现全能单位换算器搞定 面试被问原理卡壳?手写实现全能单位换算器搞定 上周陪一个转岗后端的朋友模拟面试,面试官轻描淡写抛出一句:“写个全能单位换算器”。他愣了三秒,开始写 if (unit == 'kg'),接着就是无尽的 else if。面试官没说话,只盯着屏幕。那一刻,他眼神里的慌张我看得清清楚楚。 这就是很多开发者的常态。我们平时写业务代码,习惯直接调用 lodash 的 toNumber 或者第三方库 units,觉得“能跑就行”。但一旦面试被问到底层原理,或者业务场景复杂到涉及“体积密度”这种复合单位时,直接调库的思路就崩了。今天不整虚的,咱们就手写实现一个真正能打的全能单位换算器,把那些藏在库底层的逻辑扒开来看。 入口定位:为什么你写的转换器总报错 很多人第一反应是建立一个巨大的映射表。比如 {'kg': 1000, 'g': 1}。这没错,但错在没考虑“基准”。 单位换算的本质,不是 A 转 B,而是 A 转 基准,再由 基准 转 B。如果你直接存 kg 到 g 的系数,那 kg 转 mg 怎么办?lb 转 oz 呢?组合爆炸。 我看过 GitHub 上一个高星开源仓库 js-quantities,它的核心思路就是一切归一。所有单位,最终都转换为一个“标准单位”(SI Unit)。比如质量的标准单位是 kg,长度的是 m。 这里有个坑,也是面试常问的:维度(Dimension)。你不能把 米 和 秒 直接换算。你的转换器必须知道 m 属于长度维度,s 属于时间维度。如果用户尝试 1m + 1s,程序必须报错,而不是返回一个莫名其妙的数字。 核心片段:维度注册与系数计算 这是整个转换器的灵魂。我们要设计一个 Unit 类,它包含三个核心属性:baseUnit(基准单位)、factor(换算因子)、dimension(维度)。 下面这段代码是核心逻辑的简化版,注意看注释里的细节: // 定义维度类型,防止跨维度运算 enum Dimension {LENGTH = 'length',MASS = 'mass',TIME = 'time',COMPOUND = 'compound' // 用于处理如 m/s 这种复合单位 }class Unit {constructor(public name: string, public dimension: Dimension, public factor: number, public baseUnit: string = '1' // 默认基准是国际单位制) {}// 核心方法:转换为基准单位toBase(value: number): number {// 注意:factor 是该单位相对于基准单位的倍数// 例如 1 km = 1000 m,所以 km 的 factor 是 1000return value * this.factor;}// 从基准单位转换回当前单位fromBase(value: number): number {if (this.factor === 0) throw new Error('Division by zero');return value / this.factor;} }// 注册表:这是“全能”的关键,所有单位都在这 const unitRegistry = new Mapstring, Unit();// 注册长度单位 unitRegistry.set('m', new Unit('m', Dimension.LENGTH, 1, 'm')); unitRegistry.set('km', new Unit('km', Dimension.LENGTH, 1000, 'm')); unitRegistry.set('ft', new Unit('ft', Dimension.LENGTH, 0.3048, 'm'));// 注册质量单位 unitRegistry.set('kg', new Unit('kg', Dimension.MASS, 1, 'kg')); unitRegistry.set('g', new Unit('g', Dimension.MASS, 0.001, 'kg'));逐行拆解:Dimension 枚举:这是为了做类型安全。在 TypeScript 中,我们可以进一步利用类型系统,让 convert(1, 'm', 's') 在编译期就报错。 factor 的设计:很多人会纠结 1m = 1000mm 还是 1mm = 0.001m。统一规定:factor 表示 1 个当前单位等于多少基准单位。这样逻辑就单向了,不用在乘法除法之间反复横跳。 unitRegistry:使用 Map 而不是 Object,因为单位名可能包含特殊字符,且 Map 的插入顺序更稳定,性能在大量单位注册时也更优。设计思想:组合优于继承 刚才那个 js-quantities 仓库里,还有一个更高级的设计:复合单位。 想象一下,速度单位 m/s。它不是基本单位,而是 长度 / 时间。如果你的转换器只支持基本单位,那 100 km/h 转 m/s 就得单独写一套逻辑。 这里引入了代数结构。一个单位可以由多个基本单位通过乘除组合而成。 interface CompoundUnit {baseUnits: { unit: string, exponent: number }[];// 例如 m/s 定义为 [{unit: 'm', exponent: 1}, {unit: 's', exponent: -1}] }class CompoundUnit extends Unit {constructor(public name: string, public components: CompoundUnit['baseUnits']) {// 计算总因子:各组分因子的幂次乘积const totalFactor = components.reduce((acc, comp) = {const u = unitRegistry.get(comp.unit);if (!u) throw new Error(`Unknown base unit: ${comp.unit}`);return acc * Math.pow(u.factor, comp.exponent);}, 1);super(name, Dimension.COMPOUND, totalFactor);}// 验证维度兼容性:只有维度向量完全一致才能换算isCompatible(other: Unit): boolean {// 这里简化处理,实际项目中需比较维度向量return this.dimension === other.dimension;} }// 注册速度单位 const meterPerSecond = new CompoundUnit('m/s', [{ unit: 'm', exponent: 1 },{ unit: 's', exponent: -1 } ]);const kilometerPerHour = new CompoundUnit('km/h', [{ unit: 'km', exponent: 1 },{ unit: 'h', exponent: -1 } // 注意:这里假设 h 已注册为 time 维度 ]);设计亮点:递归因子计算:km/h 的 factor 不是硬编码的 0.2777...,而是由 km 的 factor (1000) 和 h 的 factor (3600) 计算得出的。1000 / 3600。这样保证了数据的单一来源原则(SSOT)。如果以后有人修改了 h 的定义(虽然不可能,但逻辑上),所有依赖 h 的复合单位都会自动正确。 维度向量:在更复杂的场景(如物理引擎)中,维度不是一个枚举,而是一个向量 [length, mass, time, current, ...]。两个单位能换算,当且仅当它们的维度向量相等。这是防止 1kg 换算成 1m 的终极防线。手写简化版:一个能跑的 MVP 面试或日常小工具,不需要那么复杂。这里给一个纯 TypeScript 的轻量级实现,适合直接复制到项目里用。 // 简易转换器:支持基本单位 + 简单复合单位 type UnitDef = {factor: number;dimension: string;base?: string; // 如果是复合单位,指定基准 };const units: Recordstring, UnitDef = {// 基本单位m: { factor: 1, dimension: 'L' },cm: { factor: 0.01, dimension: 'L' },kg: { factor: 1, dimension: 'M' },s: { factor: 1, dimension: 'T' },// 复合单位:直接存储相对于“国际单位制组合”的因子// 1 km/h = 1000m / 3600s = 0.2777 m/s'km/h': { factor: 1000 / 3600, dimension: 'L/T' },'m/s': { factor: 1, dimension: 'L/T' }, };export function convert(value: number, from: string, to: string): number {const fromDef = units[from];const toDef = units[to];// 1. 校验单位是否存在if (!fromDef || !toDef) {throw new Error(`Unknown unit: ${!fromDef ? from : to}`);}// 2. 校验维度是否一致(核心防错逻辑)if (fromDef.dimension !== toDef.dimension) {throw new Error(`Dimension mismatch: ${fromDef.dimension} vs ${toDef.dimension}`);}// 3. 核心换算:value * fromFactor / toFactor// 推导:// value_from = value * fromFactor (转为基准)// value_to = value_from / toFactor (转为目标)// value_to = (value * fromFactor) / toFactorreturn (value * fromDef.factor) / toDef.factor; }// 测试 console.log(convert(100, 'km/h', 'm/s')); // 27.77... console.log(convert(1, 'm', 's')); // 抛出错误: Dimension mismatch这个版本的取舍:优点:代码量极小,逻辑清晰,没有类继承的开销,适合前端小工具或 Node.js 微服务。 缺点:复合单位的 factor 是硬编码计算的,如果 km 的定义变了,km/h 不会自动更新。但在 95% 的业务场景下,单位定义是恒定的,这种“静态计算”反而比“动态递归”性能更好。应用场景:别只盯着“斤两” 很多人觉得单位换算就是买菜用的。错。在工业、金融、IoT 领域,单位换算是高频且高风险的操作。IoT 传感器数据清洗: 温度传感器可能输出 ℃,但算法模型需要 K(开尔文)。压力传感器输出 psi,但数据库存储 bar。如果你的后端没有统一的转换层,前端展示就会出错,甚至导致设备控制指令错误。避坑:在数据入库前,统一转换为国际标准单位(SI),展示时再转换回用户习惯单位。永远不要在数据库里存“用户看到的值”。金融交易对: 外汇、期货交易中,合约单位、保证金计算涉及大量货币与数量的换算。虽然这里更多是数学运算,但“维度”概念依然适用:你不能把“美元/桶”和“欧元/吨”直接相加,必须先统一基准。前端表单验证: 用户在“身高”输入框输入 175,默认单位 cm。但如果用户切换了地区设置,默认单位变成了 in(英寸)。此时,后端收到的数据如果是 175,到底是多少?方案:前端提交时,必须带上单位标识 { value: 175, unit: 'cm' }。后端使用上述转换器统一转为 m 存储。一个真实的翻车案例: 某电商后台,库存字段单位是“件”。但供应商 A 发来的数据单位是“箱”,1箱=10件。开发小哥直接存了原始值。结果发货时,系统以为来了 10 件,实际来了 100 件,仓库爆仓,赔钱。这就是没有强制单位标准化的代价。 结尾互动 写到这里,你会发现,全能单位换算器看似简单,实则涉及数据建模、维度校验、精度处理等多个工程细节。它不是一个 Map 能解决的,而是一个领域模型的问题。 面试时,如果你能说出:“我不会直接存换算系数,而是建立维度向量,通过基准单位归一化来保证一致性”,面试官的眼神会从“怀疑”变成“认可”。 你在项目里踩过这个坑吗?比如因为单位不统一导致的数据错乱,或者在面试中被问倒的瞬间?评论区聊聊,看看有多少人是靠“硬编码”混过这一关的。
返回列表