上一篇:03 变量与数据类型:给数据贴标签(let / const · 字符串、数字、布尔 · typeof)
上篇练习参考答案
练习 1(手动模拟):手动模拟(先写答案再回车):typeof “true”、typeof typeof 1、console.log(28 + " 岁") 各输出什么? typeof "true" 得 "string"——引号包着就是文字,哪怕内容长得像布尔值;typeof typeof 1 得 "string"——检查员递来的是写着"number"的纸条,纸条本身是文字;28 + " 岁" 得 "28 岁"——+ 见到字符串就切拼接,28 被拉进文字世界。 
练习 2(预测再验证):预测再验证:把实战代码里 const birthYear 改成 let birthYear,文件末尾加一行 birthYear = 1999;——运行结果和原来有什么不同?const 和 let 差的到底是什么? 输出和原来几乎完全一样。const/let 差的不是"值能不能用",是"标签能不能重新赋值"——const 版本会在 birthYear = 1999 那行当场炸出 TypeError。最好的错误是还没跑就被拦下的错误:报错拦你,是在救你。
练习 3(改造要点):改造要点:给资料卡加一条"爱好"数据(提示:数据区、代码区、体检区三个位置各动一行); 三个位置各动一行
const hobby = "写代码和摸鱼"; // 数据区
console.log(`爱好:${hobby}`); // 代码区
console.log(`hobby 的类型:${typeof hobby}`); // 体检区

练习 4(疫苗揭晓):在控制台分别执行 console.log(1 + 1) 和 console.log(“1” + 1)——先别回车,把两个答案写在纸上再运行。如果猜得不全对,第 4 篇会讲清这两个结果背后同一套规则,以及它在真实项目里如何把"合计 640"悄悄变成"198225217" 1 + 1 得 2,"1" + 1 得 "11"。规则:+ 号两边只要有一边是字符串,就切成拼接模式,另一边被悄悄转成文字。数字一旦被拉进文字世界,后面的 + 全是拼接。你写的是合计,它执行的是串门——这就是本篇要拆的雷。 
一、场景引入:一起真实的"账单事故"

上篇你学会了贴标签,本篇拆 JS 民间第一事故。
团建结束你来记账,从群里复制来三笔费用:"198元"、"225元"、"217元"。看着像数字?它们是带单位的字符串——从表格、网页、输入框复制来的"数字",十有八九是字符串(输入框产出的永远是字符串,16 篇会再撞见一次)。
于是事故发生:
const total = "198元" + "225元" + "217元";
console.log(total); // "198元225元217元"——三笔钱串成了一串门牌号

没有任何报错,控制台安静地交出一串鬼话——这就是 02 篇说过的"静默失败"在数据界的形态,比报错可怕十倍。本篇四件事:说清 + 的规则、学会把文字洗成数字、认识 NaN、用模板字符串优雅说话。
二、本篇目标
三、知识点讲解
3.1 + 号的两副面孔:算术模式与拼接模式
‘+’ 是 JS 里唯一"身兼两职"的运算符,切换规则就一句: 两边都是数字 → 相加;任意一边是字符串 → 拼接(另一边被隐式转成文字)
控制台四连测(先猜后跑):
console.log(1 + 1); // 2 纯数字:算术
console.log("1" + 1); // "11" 有文字:拼接,1 被拉进文字世界
console.log(1 + 1 + "1"); // "21" 从左往右算:先 1+1=2,再 2+"1"
console.log("1" + 1 + 1); // "111" 先 "1"+1="11",从此全拼接

第三、四条是精髓:运算从左往右,一旦中途某步进了文字世界,后面全部沦陷。所以 "1" + 1 + 1 和 1 + 1 + "1" 结果不同——顺序决定命运。
顺带一个大新闻:减、乘、除没有两副面孔:
console.log("12" – 8); // 4 减号走数学:字符串被隐式转成数字
console.log("3" * "4"); // 12 乘除同上
console.log("12" / "4"); // 3 乘除同上

于是老江湖有句黑话叫 "12" – 0 转数字。认识即可,别学——隐式转换是"赌浏览器猜对你的心思",显式转换才是"明说"。赌赢一百次,输的那一次会让你在客户面前社死。
3.2 显式转换:把文字洗成数字的三把刷子
Number:严格派
Number("18") // 18
Number("3.5") // 3.5
Number(" 18 ") // 18 前后空格能容忍
Number("18元") // NaN 混进任何杂质:拒绝
Number("") // 0 空串算 0(历史决定,记住就好)

parseInt / parseFloat:实干派——从左往右薅数字,遇到第一个非数字字符就停,杂质直接丢弃:
parseInt("198元") // 198 带单位的账单,直接薅出数字
parseFloat("60.5元") // 60.5 parseFloat 保留小数
parseInt("12.9") // 12 parseInt 只取整数,小数直接砍
parseInt("元198") // NaN 开头就不是数字:没得薅

从网页、群聊、表格里扒带单位的数字,parseInt 一族是神器。选型口诀:
纯数字串用 Number(严格),带单位带杂质用 parseInt / parseFloat(实干)。
反方向(数字转文字)两个顺手工具:String(18) 得 "18";金额显示专用 (640).toFixed(2) 得 "640.00"——注意 toFixed 返回的是字符串:它是化妆师不是数学家,用来显示,不能拿来再参与计算。 
3.3 NaN:数字世界的传染病人
Number 洗不动的数据不会报错,会变成 NaN(Not a Number)。三个必知:
typeof NaN // "number" NaN 是 number 类型却"不是数字"(历史包袱,别较真)
NaN + 5 // NaN 特性一:传染,沾上 NaN 的运算全是 NaN
NaN === NaN // false 特性二:全 JS 唯一不等于自己的值

特性一解释了它的可怕:账单里出现一个 NaN,说明上游某笔数据脏了——它不拦你,只是让结果一路烂到底。特性二是个大坑:想检测 NaN 写 x === NaN 永远是 false(它连自己都不认)。正确姿势:
Number.isNaN(NaN) // true 才是 NaN

排查 NaN 的标准流程也简单:沿着计算链逐个 console.log(typeof x, x),第一个变 NaN 的就是病灶——11 篇会把这套"沿链排查"升级成专业调试术。 
3.4 模板字符串:拼接的优雅时代
数字洗好了,最后一环是"说人话"。老式拼接你已经领教过:
console.log("人均 " + per + " 元,共 " + people + " 人,合计 " + total + " 元");
引号和加号排长队,漏一个引号能让你排查半小时。现代 JS 的正解——模板字符串:一对反引号(键盘 Esc 下方、Tab 上方那个键 `)包住整句话,变量用 ${} 塞进去:
console.log(`人均 ${per} 元,共 ${people} 人,合计 ${total} 元`);
三大优点,各值回票价:
03 篇你已经照抄过它,本篇正式转正:从本篇起,本专栏所有输出默认模板字符串。
四、完整实战:团建账单分摊器
把四件事拼成一条流水线:脏数据 → 洗 → 算 → 说人话——这条流水线以后会在表单、爬虫、看板里反复出现。新建 账单分摊器.html(点击html名下载):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>团建账单分摊器</title>
</head>
<body>
<h2>团建账单分摊器</h2>
<p>账单报告在控制台——按 F12 查看。</p>
<script>
// ===== 数据区:群里复制来的原始账单(全是带单位的字符串)=====
const bill1 = "198元"; // 午餐
const bill2 = "225元"; // 晚餐
const bill3 = "217元"; // 唱歌
const drinks = "60.5元"; // 饮料
const people = 8; // 人数(自己数的,是真数字)
// ===== 第一步:洗(带单位 → parseFloat 薅出数值)=====
const food1 = parseFloat(bill1);
const food2 = parseFloat(bill2);
const food3 = parseFloat(bill3);
const drinkCost = parseFloat(drinks);
// 不放心就体检:洗完是什么类型?
console.log(`洗后体检:${typeof food1} ${food1},${typeof drinkCost} ${drinkCost}`);
// ===== 第二步:算 =====
const foodTotal = food1 + food2 + food3;
const total = foodTotal + drinkCost;
const perPerson = total / people;
// ===== 第三步:说人话(模板字符串 + toFixed)=====
console.log("=".repeat(40));
console.log(" 团 建 账 单");
console.log("=".repeat(40));
console.log(`人数:${people} 人`);
console.log(`餐费三笔:${food1} + ${food2} + ${food3} = ${foodTotal} 元`);
console.log(`含饮料总计:${total.toFixed(2)} 元`);
console.log(`人均全含:${perPerson.toFixed(2)} 元`);
console.log(`每人整数收款:${Math.round(perPerson)} 元(四舍五入)`);
console.log("=".repeat(40));
// ===== 事故现场还原:不洗直接加会怎样 =====
const dirtyTotal = bill1 + bill2 + bill3;
console.log(`事故还原——不洗直接加:${dirtyTotal}`);
console.log(`类型对比——洗前 ${typeof bill1},洗后 ${typeof food1}`);
</script>
</body>
</html>
注意 Math.round(perPerson)——Math 是 JS 自带的数学工具箱,round 负责四舍五入,先混脸熟,用到再讲。
五、运行效果

洗后体检:number 198,number 60.5
========================================
团 建 账 单
========================================
人数:8 人
餐费三笔:198 + 225 + 217 = 640 元
含饮料总计:700.50 元
人均全含:87.56 元
每人整数收款:88 元(四舍五入)
========================================
事故还原——不洗直接加:198元225元217元
类型对比——洗前 string,洗后 number
六、常见报错与排查
现象 1:合计变成"198元225元217元"一长串(没有任何报错)
原因:+ 的遇文则文——加的全是字符串。 解决:计算前先洗,parseFloat / Number 处理一遍再参与运算。这是 JS 数据处理的第一反射:拿到外部数据,先问一句"你真的是数字吗"。
现象 2:金额输出 NaN
原因:某笔数据 Number 洗不动(比如 "元198" 开头就是杂质),NaN 一路传染到结果。 解决:沿计算链逐个 console.log(typeof x, x),第一个 NaN 就是病灶;修数据或改用 parseInt。检测一律 Number.isNaN(x),别写 x === NaN(永远 false)。
现象 3:页面上显示出一串 ${perPerson} 原文
原因:模板字符串忘了用反引号,用了普通引号——普通引号里的 ${} 没有魔法,原样输出。 解决:整句话最外层换成反引号。以后看到页面出现 ${ 字样,直接去查引号类型。
排查心法:结果不对先查类型;金额 NaN 沿链体检;出现 ${ 检查反引号。数据问题九成是类型问题——typeof 是你唯一的听诊器。
七、练习作业
下一篇预告:《05 条件判断:=== 才是正道》——程序怎么回答"是/否"问题?为什么永远不要用两个等号做比较?if/else、三元表达式、switch 一次讲齐。



