JS运算符别让字符串混进来
一次经纬度计算故障让我重新想起 JavaScript 的隐式类型转换:减法正常,加法却变成了字符串拼接,结果页面数据完全跑偏。这篇从故障现象讲起,拆开加减乘除的类型规则,说明 Number、parseInt、parseFloat 怎么选,并补上前后端数据和浮点精度的处理建议。
前段时间做一个地图功能,需要根据中心经纬度计算默认范围,再生成一个正方形经纬度数组。
逻辑看起来很普通,无非就是经纬度加减一个偏移量。
结果调试时遇到一个挺离谱的现象:
- 减法计算看着正常
- 加法计算结果完全不对
- 后端返回的数据肉眼看着也没有问题
- 页面最后生成的坐标数组像是被打乱了
一开始我还以为是后端坐标精度丢了。
后来把对象类型打印出来,才发现经纬度虽然看起来像数字,实际是字符串。
这就撞上了 JavaScript 里一个很容易被忽略的规则:加号不只有数学加法,还可能是字符串拼接。
故障是怎么发生的
先看一个最简单的例子:
var x = 7 + 8
var y = '7' + 8
var z = 'Hello' + 7
console.log(x)
console.log(y)
console.log(z)
结果分别是:
15
78
Hello7
7 + 8 是数字相加。
'7' + 8 则变成了字符串拼接。
'Hello' + 7 更不用说,数字直接被转成字符串拼到后面。
这三个结果看起来有点反直觉,但 JavaScript 的规则就是如此。
经纬度案例
假设后端返回了中心点:
var longitude = '120.1234'
var offset = 0.01
var left = longitude - offset
var right = longitude + offset
结果可能是:
left = 120.1134
right = 120.12340.01
减号把字符串转成数字后进行计算。
加号发现一边是字符串,就优先走字符串拼接。
所以问题不在经纬度数据本身,而在计算之前没有明确完成类型转换。
调试这种问题时,建议不要只打印值,同时打印类型:
console.log(longitude)
console.log(typeof longitude)
console.log(offset)
console.log(typeof offset)
只看 Console 里的值,很容易被 120.1234 这种长得像数字的字符串骗过去。
加号为什么这么特殊
JavaScript 里的 + 同时承担两种含义:
- 两边是数值时,执行数学加法
- 任意一边经过转换后是字符串时,执行字符串拼接
可以把它画成一个简单判断:
遇到 a + b
│
▼
是否有一侧是字符串?
├── 是:转成字符串后拼接
└── 否:尝试转成数值后相加
这里的“任意一侧”很关键。
1 + '2' // '12'
'1' + 2 // '12'
1 + 2 + '3' // '33'
'1' + 2 + 3 // '123'
最后两个例子还说明了一个问题:+ 是从左到右计算的。
1 + 2 + '3' 会先得到数字 3,再和字符串 '3' 拼接成 '33'。
'1' + 2 + 3 则从第一步开始就进入字符串拼接,最后得到 '123'。
这类代码看着短,排查起来却很费脑子。
我的习惯是,涉及金额、坐标、数量、分页参数等业务数据时,先做类型转换,再开始计算。
不要把类型推断交给运气。
其他运算符也会转换
减法、乘法和除法没有字符串拼接这层含义,所以它们通常会尝试把操作数转成数字。
'7' - 2 // 5
'7' * 2 // 14
'7' / 2 // 3.5
'7' - '2' // 5
这就是为什么故障里减法看起来正常,而加法坏掉了。
但是不要因此得出“减法会自动帮我处理类型,所以都不用转换”的结论。
隐式转换还有很多边界情况:
'' - 1 // -1
null - 1 // -1
undefined - 1 // NaN
'abc' - 1 // NaN
NaN 会继续污染后面的计算:
var value = 'abc' - 1
var result = value + 10
// result 也是 NaN
如果没有及时校验,最后页面可能只显示一个看不懂的空白、NaN 或错误坐标。
先看运算符,再看类型
可以简单记成:
| 运算符 | 字符串参与时的常见表现 |
|---|---|
+ |
可能转成字符串拼接 |
- |
通常尝试转成数字 |
* |
通常尝试转成数字 |
/ |
通常尝试转成数字 |
== |
可能发生隐式类型转换 |
=== |
不转换类型,值和类型都要相同 |
这不是让我们记更多奇怪规则,而是提醒:运算前把数据类型控制在自己手里。
字符串怎么转成数字
Number
最直接的方式是 Number(value):
var longitude = '120.1234'
var latitude = '30.5678'
longitude = Number(longitude)
latitude = Number(latitude)
转换后再进行计算:
var left = longitude - 0.01
var right = longitude + 0.01
Number 的特点是会尝试把整个值转换成一个数字。
Number('12.5') // 12.5
Number(' 12.5 ') // 12.5
Number('12px') // NaN
Number('') // 0
最后一个结果需要特别注意。
如果空字符串在业务上应该被认为是无效输入,就不能只调用 Number 后不做校验。
可以配合 trim 和 Number.isFinite:
function toNumber(value) {
if (value === null || value === undefined) {
return null
}
var text = String(value).trim()
if (text === '') {
return null
}
var number = Number(text)
return Number.isFinite(number) ? number : null
}
转换之后再判断是否为 null,比让 NaN 继续流进业务逻辑安全得多。
一元加号
一元 + 也可以把值转换成数字:
var count = +'10'
var price = +'12.50'
写起来很短,但可读性不如 Number。
业务代码里我更推荐 Number(value),看到代码的人一眼就知道这里是在做类型转换。
parseInt
parseInt 适合解析整数,并且会从字符串开头读取可以解析的部分:
parseInt('12', 10)
parseInt('12px', 10)
parseInt('12.8', 10)
结果分别是 12、12、12。
第二个参数建议明确写成 10,避免历史环境下的进制判断问题。
如果你的业务要求整个输入都必须是合法数字,parseInt('12px', 10) 这种行为反而可能掩盖脏数据。
parseFloat
parseFloat 适合读取浮点数前缀:
parseFloat('12.8px')
parseFloat('12.8')
同样,它可能接受字符串前缀而忽略后面的非法内容。
所以可以这样选:
- 完整转换并严格校验:
Number - 解析整数前缀:
parseInt - 解析浮点数前缀:
parseFloat
前端数据为什么经常是字符串
这类问题不只来自后端接口。
前端有很多数据入口天然就是字符串:
- 表单
input.value - URL 查询参数
localStoragesessionStoragedataset自定义属性- HTML 标签属性
- 部分旧接口或表单提交数据
var input = document.querySelector('#count')
var count = input.value
console.log(typeof count)
// 通常是 string,即使输入框里填的是 10
URL 参数也一样:
var page = new URLSearchParams(location.search).get('page')
// page 看起来是 2,实际类型通常是 string
所以不要只在后端返回 JSON 的地方做类型检查。
凡是跨边界进入业务逻辑的数据,都应该在边界处完成转换和校验。
外部输入
│
▼
读取原始字符串
│
▼
转换为业务类型
│
▼
校验合法性
│
▼
进入计算逻辑
边界处理一次,后面的代码就不用到处猜类型。
一个更稳的坐标处理方式
以经纬度为例,可以把转换和计算拆开:
function buildSquare(center, offset) {
var longitude = toNumber(center.longitude)
var latitude = toNumber(center.latitude)
var distance = toNumber(offset)
if (longitude === null || latitude === null || distance === null) {
throw new Error('坐标数据不是有效数字')
}
return [
[longitude - distance, latitude - distance],
[longitude + distance, latitude - distance],
[longitude + distance, latitude + distance],
[longitude - distance, latitude + distance]
]
}
这里有几个好处:
- 进入计算前统一转换
- 坐标无效时立即失败,不把
NaN传下去 - 加减运算面对的都是明确数字
- 计算函数不需要关心数据来自接口、表单还是 URL
故障排查时,先把这个函数的输入和 typeof 打出来,通常很快就能找到问题。
别忘了浮点数精度
把字符串转换成数字,只能解决类型问题,不能消灭 JavaScript 的浮点数精度问题。
0.1 + 0.2
// 结果可能是 0.30000000000000004
JavaScript 的 Number 使用 IEEE 754 双精度浮点数。
经纬度、比例和普通展示数据通常可以通过格式化处理:
var value = Number((longitude + distance).toFixed(6))
但金额、订单价格、积分等业务不能简单依赖 toFixed 就认为精度问题解决了。
常见做法是:
- 用最小单位整数存储,例如金额转成分
- 后端使用十进制定点类型
- 前端使用专门的 decimal 库
- 展示时统一格式化小数位
参考旧数据的准确性边界,前端负责展示和交互计算,最终入库或结算仍然要由后端做可靠校验。
排查这类故障的顺序
以后再遇到“减法正常、加法异常”的问题,可以按这个顺序排:
- 打印参与运算的每个值
- 使用
typeof确认实际类型 - 查看 Network 响应,确认后端字段到底是数字还是字符串
- 对外部输入统一执行
Number或明确的解析函数 - 用
Number.isFinite、范围判断校验转换结果 - 检查
+是否发生了字符串拼接 - 如果结果仍有尾数,再单独检查浮点精度
不要一看到结果不对就去怀疑地图组件、坐标算法或浏览器。
很多时候,故障就藏在一个看起来很普通的加号里。
小结
JavaScript 的 + 有两张脸:
- 两边是数字,执行加法
- 任意一边是字符串,可能执行拼接
减法、乘法和除法会尝试做数字转换,但隐式转换也会带来 NaN、空字符串转 0 等边界问题。
实际开发建议:
- 外部输入进入业务边界时就做类型转换
- 需要严格转换时优先使用
Number parseInt记得指定进制,且注意它会接受数字前缀- 转换后用
Number.isFinite和业务范围继续校验 - 金额等场景不要把浮点数精度交给运气
- 调试时同时看值和
typeof,别被“看起来像数字”骗了
一句话总结:不要让字符串混进数学运算,尤其不要让一个字符串悄悄改变加号的含义。
先转换,再计算;先校验,再提交。这个习惯看起来啰嗦一点,但能省掉不少离离原上谱的故障排查。
