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 查询参数
  • localStorage
  • sessionStorage
  • dataset 自定义属性
  • 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 库
  • 展示时统一格式化小数位

参考旧数据的准确性边界,前端负责展示和交互计算,最终入库或结算仍然要由后端做可靠校验。

排查这类故障的顺序

以后再遇到“减法正常、加法异常”的问题,可以按这个顺序排:

  1. 打印参与运算的每个值
  2. 使用 typeof 确认实际类型
  3. 查看 Network 响应,确认后端字段到底是数字还是字符串
  4. 对外部输入统一执行 Number 或明确的解析函数
  5. 用 Number.isFinite、范围判断校验转换结果
  6. 检查 + 是否发生了字符串拼接
  7. 如果结果仍有尾数,再单独检查浮点精度

不要一看到结果不对就去怀疑地图组件、坐标算法或浏览器。

很多时候,故障就藏在一个看起来很普通的加号里。

小结

JavaScript 的 + 有两张脸:

  • 两边是数字,执行加法
  • 任意一边是字符串,可能执行拼接

减法、乘法和除法会尝试做数字转换,但隐式转换也会带来 NaN、空字符串转 0 等边界问题。

实际开发建议:

  • 外部输入进入业务边界时就做类型转换
  • 需要严格转换时优先使用 Number
  • parseInt 记得指定进制,且注意它会接受数字前缀
  • 转换后用 Number.isFinite 和业务范围继续校验
  • 金额等场景不要把浮点数精度交给运气
  • 调试时同时看值和 typeof,别被“看起来像数字”骗了

一句话总结:不要让字符串混进数学运算,尤其不要让一个字符串悄悄改变加号的含义。

先转换,再计算;先校验,再提交。这个习惯看起来啰嗦一点,但能省掉不少离离原上谱的故障排查。

更多推荐

章节目录