JS 怎么直接截断保留一位小数而不进行四舍五入
全面讲解在 JavaScript 中直接截断并保留一位小数且绝不进行四舍五入的实现方法。深入对比数学缩放法、字符串分割法与正则表达式截取,分析负数边界与浮点数精度陷阱,提供健壮的生产级解决方案。
Math.trunc(num * 10) / 10;若用于页面排版、文本展示或需要避免浮点数二进制乘除误差,更稳妥的做法是将其转换为字符串通过正则匹配 String(num).match(/^-?\d+(?:\.\d)?/)[0] 直接切断。本文将从核心机制、数学算法、字符串操作及边界防御四个层面,系统拆解截断保留一位小数的完整落地方案。
一、为什么常规的 toFixed(1) 不能胜任截断任务
很多开发者在需要保留小数时,下意识会使用 JavaScript 原生提供的 Number.prototype.toFixed() 方法。例如调用 (3.86).toFixed(1),得到的结果是 "3.9"。这是因为 toFixed() 的设计目标是进行舍入(基于类似银行家舍入的四舍五入变体规则),当目标位后一位大于等于 5 时会自动向前进位。
如果业务规则要求的是“严格截断”,例如电商中“满 10 元返 1 元”类阶梯计算、视频播放进度时间戳显示、或者特定计费规则中只取有效刻度,任何向上的进位都会导致数据超出实际范围。此时,四舍五入不仅不符合需求,还可能带来逻辑错误。
// toFixed(1) 的四舍五入行为示例(示例输出)
console.log((3.86).toFixed(1)); // 输出 "3.9" (发生了进位,不合截断要求)
console.log((1.24).toFixed(1)); // 输出 "1.2" (恰好因小于5而舍去)
console.log((-1.58).toFixed(1)); // 输出 "-1.6" (负数同样发生远离0的进位)
从图 1 可以直观看出:四舍五入以 0.05 为临界点双向进位,而纯截断则是把整个 [1.10, 1.20) 区间内的数值全部恒定归敛为 1.1。因此,必须寻找真正具备“向零方向切除尾数”的算法。
二、方法一:数学缩放截断法(Math.trunc 核心机制)
数学缩放法的原理非常纯粹:既然我们要保留一位小数,就先将目标数字乘以 10,让原本的十分位数字跻身到个位(整数部分);然后剔除所有新的小数位;最后再除以 10 还原量级。
1. 操作步骤详解
- 步骤一(放大):执行
num * 10。例如12.38 * 10得到123.8。 - 步骤二(向零取整):执行
Math.trunc(123.8)得到整数123。 - 步骤三(还原):执行
123 / 10得到最终结果12.3。
// 基础数学截断实现代码
function truncateOneDecimalMath(num) {
return Math.trunc(num * 10) / 10;
}
// 运行测试(示例输出)
console.log(truncateOneDecimalMath(3.86)); // 3.8
console.log(truncateOneDecimalMath(1.24)); // 1.2
console.log(truncateOneDecimalMath(5.0)); // 5
console.log(truncateOneDecimalMath(-1.58)); // -1.5
Math.floor(num * 10) / 10,这在处理正数时完全正确,但在处理负数时会引发隐蔽的 Bug。
Math.floor 的定义是“向下取整(向负无穷方向取整)”。当传入 -1.58 时,乘以 10 得 -15.8。Math.floor(-15.8) 会得到更小的整数 -16,除以 10 后变成 -1.6,反而产生了进位!
而 ES6 引入的
Math.trunc 是真正的“向零取整(去除小数部分)”,Math.trunc(-15.8) 稳定返回 -15,还原后得到预期的 -1.5。
三、方法二:字符串切片与正则表达式法
虽然数学缩放法计算速度极高,但由于 JavaScript 采用 IEEE 754 双精度浮点数标准,任何浮点乘除法都有极小概率受到二进制精度漂移的影响。例如 1.15 * 10 在部分引擎内部实际表示为 11.499999999999998。如果在这种极端边界上执行向下取整,极偶发情况下会导致计算值微弱失真。
如果你的应用场景是表单输入、商品标价或报表文字展示,直接通过文本字符切片是绝对免疫浮点误差的方案。
1. 正则表达式截取法(推荐)
通过简单的正则模式 /^-?\d+(?:\.\d)?/,可以直接匹配可选负号、完整整数部分,以及至多一位的小数部分,彻底忽略后续多余数字:
// 使用正则截断保留一位小数
function truncateOneDecimalReg(val) {
const str = String(val);
const match = str.match(/^-?\d+(?:\.\d)?/);
return match ? match[0] : "0";
}
// 运行测试(示例输出)
console.log(truncateOneDecimalReg(3.86)); // "3.8"
console.log(truncateOneDecimalReg(-1.58)); // "-1.5"
console.log(truncateOneDecimalReg(9)); // "9"
console.log(truncateOneDecimalReg("12.05"));// "12.0"
2. 字符串 split 分割法
如果不使用正则,也可以将字符串以小数点 . 为界进行切割,保留左侧整数部分与右侧小数首字符:
function truncateOneDecimalSplit(val) {
const str = String(val);
const dotIndex = str.indexOf('.');
if (dotIndex === -1) return str; // 纯整数无小数点
return str.slice(0, dotIndex + 2); // 截取到小数点后一位
}
// 运行测试(示例输出)
console.log(truncateOneDecimalSplit(8.99)); // "8.9"
console.log(truncateOneDecimalSplit(-4.72)); // "-4.7"
四、关键细节与常见疑问辨析
1. 截断后的数值末尾是否要补零?(如 12 变成 12.0)
这取决于目标数据的消费方是运算程序还是人类视觉:
- 如果作为 Number 类型:JavaScript 的数值类型在内存中不记录尾随零。
12.0在数值上与12完全等价,重新转回 Number 时末尾的.0必然会被自动省略。 - 如果作为 String 展示类型:例如电商价格标签要求整齐划一的“¥12.0”,则必须在格式化阶段进行判断,在切片后主动使用
padEnd或补全.0。
2. 为什么不用位运算符(如 ~~(num * 10) / 10)?
双波浪线 ~~ 或按位或 | 0 在 JS 引擎内部确实等同于 32 位有符号整数的向零取整,而且在老旧引擎中执行极快。但在现代浏览器中:
Math.trunc早已被各大引擎高度内联优化,性能差异微乎其微。- 位运算存在致命的 32 位溢出陷阱(数值范围被强行限制在
-2,147,483,648到2,147,483,647之间)。一旦数字乘以 10 后超出此范围,符号位就会翻转错乱。因此通用代码绝不推荐使用位运算代替Math.trunc。
五、工程级健壮方案:通用截断函数封装
为了满足真实项目开发中可能遇到的类型不一(数字或字符串)、非数值防崩、末位补零定制、以及科学计数法防范,我们可以编写一个稳健的封装函数:
/**
* 严格截断保留一位小数(绝不四舍五入)
* @param {number|string} value - 输入的原始数值或数字字符串
* @param {Object} options - 配置项
* @param {boolean} options.padZero - 整数是否强制补全 .0(默认 false)
* @param {boolean} options.asNumber - 是否以数字类型返回(默认 false,返回字符串)
* @returns {string|number} 截断后的结果
*/
function truncateToOneDecimal(value, { padZero = false, asNumber = false } = {}) {
const num = Number(value);
// 防御非合法数值
if (Number.isNaN(num)) return asNumber ? NaN : "0";
// 针对绝对值极小的科学计数法特殊防范(如 1e-7)
const rawStr = typeof value === 'number' ? value.toString() : String(value).trim();
if (rawStr.includes('e') || rawStr.includes('E')) {
const safeNum = Math.trunc(num * 10) / 10;
if (asNumber) return safeNum;
return padZero ? safeNum.toFixed(1) : String(safeNum);
}
// 正则精准捕获:可选负号 + 整数部分 + 可选的一位小数
const match = rawStr.match(/^(-?\d+)(?:\.(\d))?/);
if (!match) return asNumber ? 0 : (padZero ? "0.0" : "0");
const intPart = match[1];
const decPart = match[2];
let resultStr = "";
if (decPart !== undefined) {
resultStr = `${intPart}.${decPart}`;
} else {
resultStr = padZero ? `${intPart}.0` : intPart;
}
return asNumber ? parseFloat(resultStr) : resultStr;
}
// 运行全用例验证(示例输出)
console.log(truncateToOneDecimal(3.86)); // "3.8"
console.log(truncateToOneDecimal(3.86, { asNumber: true })); // 3.8
console.log(truncateToOneDecimal(12, { padZero: true })); // "12.0"
console.log(truncateToOneDecimal(12, { padZero: false })); // "12"
console.log(truncateToOneDecimal(-1.59)); // "-1.5"
console.log(truncateToOneDecimal("45.678")); // "45.6"
console.log(truncateToOneDecimal(0.04)); // "0.0"
六、方案对比与选型决策指南
在面对不同业务场景时,可以参考以下评估维度进行合理选型:
| 实现方案 | 正数表现 | 负数表现 | 返回类型 | 核心优势与适用场景 |
|---|---|---|---|---|
Math.trunc(n * 10) / 10 |
准确截断 | 准确向零截断 | Number | 计算首选:代码简洁,无额外解析开销,适合密集计算与图表坐标点生成。 |
Math.floor(n * 10) / 10 |
准确截断 | 偏小进位 (错误) | Number | 限制场景:仅在 100% 确保数值非负时可用,不建议作为通用方案。 |
String / RegExp 切片 |
准确截断 | 准确截断 | String | UI展示首选:完全避免 IEEE 754 乘除微偏,便于支持补零控制。 |
toFixed(1) |
四舍五入 | 四舍五入 | String | 不适用:无法满足非四舍五入截断的要求。 |
Math.trunc(x * 10) / 10;如果是在开发 Vue/React 业务组件、处理接口返回的价格/评分/百分比文本渲染,优先使用正则截取或工程级封装函数,既安全直观,又能从容满足设计稿对末尾补零的格式要求。
JSP页面中怎么嵌入JavaScript代码及数据交互方式
2026-11-06 16:10:52
p5.js 动画原理:利用 frameCount 控制运动轨迹
2026-11-05 23:50:45
JS中super关键字在class继承里的用法与注意事项
2026-11-05 21:55:33
JavaScript三目运算符基础语法与执行流程解析
2026-11-05 21:43:51
JavaScript变量提升与暂时性死区测试题:var/let/const在函数中的作用
2026-11-05 21:20:30








