购物车要算总价,游戏要更新分数,登录页要判断账号和密码是否都已填写。表面上,这些需求很不一样,落到 JavaScript 里却都离不开同一套工具:表达式负责产生值,运算符负责规定怎样计算这些值。
真正容易出错的地方通常不是运算符长什么样,而是它们碰到不同类型时会怎么做:+ 有时相加、有时拼接;== 会悄悄转换类型;&& 和 || 甚至不保证返回布尔值。只要把这些规则看清楚,许多“JavaScript 为什么这样算”的疑问都会消失。
本章会用可以直接运行的小例子建立一套读表达式的方法。你不仅会算出答案,还会判断变量是否被修改、右侧代码是否执行,以及怎样把含糊的写法改得更可靠。
文中的控制台示例可直接粘贴到浏览器开发者工具或 Node.js 中运行。除特别说明外,两种环境的输出一致。
完成本章后,你应该能够:
&&、||、!、?? 的短路规则;表达式是一段求值后会得到结果的代码。字面量 42 会得到数字 42,变量读取 price 会得到变量当前保存的值,price * count 会得到乘法结果,函数调用 getTotal() 也会得到函数返回值。

读表达式时,可以把它想成一台求值机:输入代码,按照规则计算,最后交出一个值。
下面从一笔订单开始:
const price = 12;
const count = 3;
const total = price * count;
console.log(total);
console.log(total >= 30);
console.log(total >= 30 ? "可包邮" : "继续凑单");输出:
36
true
可包邮这段代码包含三类结果:
price * count 得到数字 36;total >= 30 得到布尔值 true;条件 ? 值 A : 值 B 根据条件选择字符串 "可包邮"。有些表达式只读取和计算,例如 price * count。另一些表达式在产生结果的同时,还会改变程序状态或影响外部环境,这种额外影响叫作副作用。
let score = 10;
const updatedScore = (score += 5);
const logResult = console.log(updatedScore);
console.log(score);
console.log(logResult);输出:
15
15
undefinedscore += 5 不仅得到新值 15,还把 score 改成了 15。console.log(updatedScore) 会向控制台输出内容,这也是副作用;它本身的返回值是 undefined,因此最后一行输出 undefined。
阅读一段表达式时依次问:它得到什么值?它修改了什么?它调用的代码是否一定会执行?后面学习短路运算时,这三个问题仍然适用。
JavaScript 使用熟悉的符号完成基本计算:
% 常被口头叫作“取模”,在 JavaScript 中更准确地说,它计算的是余数。例如 125 分钟可以拆成 2 个完整小时,再剩 5 分钟:
const totalMinutes = 125;
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
console.log(hours);
console.log(minutes);
console.log(`${hours} 小时 ${minutes} 分钟`);输出:
2
5
2 小时 5 分钟取余还适合判断奇偶:整数 n % 2 === 0 时,n 是偶数。
JavaScript 的 % 会保留被除数的符号,所以 -5 % 2 是 -1,不是 1。需要始终为非负数的循环下标时,可以使用 ((n % size) + size) % size。
+ 同时承担数字加法和字符串连接两项工作。两边都是数字时,它进行加法;只要运算进入字符串连接场景,结果就会拼成字符串。

数据类型决定了加号走向“数字相加”还是“字符串连接”。
console.log(5 + 2);
console.log("5" + 2);
console.log("总数:" + 5 + 2);
console.log(5 + 2 + " 件");输出:
7
52
总数:52
7 件表达式会按顺序逐步求值。"总数:" + 5 先得到字符串 "总数:5",再连接 2;而 5 + 2 会先算出数字 7,然后才与 " 件" 连接。
减法、乘法和除法没有“连接字符串”的含义,因此会尝试把操作数转成数字:
console.log("5" - 2);
console.log("5" * 2);
console.log("hi" * 2);
console.log(Number("5") + 2);输出:
3
10
NaN
7NaN 表示某次数字运算没有得到有效数字。它仍属于 JavaScript 的 number 类型,但不等于任何值,甚至不等于自己。判断一个结果是否为 NaN 时,应使用 Number.isNaN(value)。
表单输入、地址参数和许多接口字段都可能是字符串。想做数学运算时,先用 Number(value) 显式转换并验证,再参与计算,比依赖隐式转换更清楚。
普通数字除以零会得到无穷值,0 / 0 则得到 NaN:
console.log(5 / 0);
console.log(-5 / 0);
console.log(0 / 0);输出:
Infinity
-Infinity
NaN这些结果不会自动中断程序。涉及比例、均价或速度时,要在计算前确认除数不是零,并检查最终结果是否为有限数字,例如 Number.isFinite(result)。
赋值运算符 = 会先计算右侧,再把结果保存到左侧变量。不要把它和比较混淆:= 是“保存”,=== 才是“严格比较”。
当新值依赖旧值时,可以使用复合赋值:
let points = 10;
points += 5; // points = points + 5
points *= 2; // points = points * 2
points -= 4; // points = points - 4
points %= 7; // points = points % 7
console.log(points);输出:
5计算过程依次是 10 → 15 → 30 → 26 → 5。常用复合赋值还包括 /= 和 **=。
const 声明的变量不能再次赋值,因此下面的更新必须使用 let:
const start = 10;
// start += 1; // TypeError: Assignment to constant variable.
let current = start;
current += 1;
console.log(current);输出:
11++ 把变量加 1,-- 把变量减 1。单独占一行时,前置和后置都会完成同样的更新:
let level = 2;
level++;
console.log(level);输出:
3差别出现在自增表达式的结果被立即使用时:
let n = 2;
console.log(n++); // 先交出旧值 2,再把 n 改成 3
console.log(n); // 3
console.log(++n); // 先把 n 改成 4,再交出新值 4输出:
2
3
4可以把规则压缩成一句话:
n++:表达式得到旧值,变量随后加 1;++n:变量先加 1,表达式得到新值。不要为了少写一行,把多个 ++ 塞进复杂计算。把“更新变量”和“使用变量”拆开,副作用的时机就一眼可见。
一个表达式出现多个运算符时,JavaScript 不会简单地从左到右全部计算。优先级决定哪一类运算先分组,结合性决定同一优先级从哪一侧组合。

不必死背完整表格,但要记住常用层级,并在意图可能含糊时加括号。
本章常见运算符可以先掌握这条简化顺序:
先看最常见的乘加组合:
console.log(2 + 3 * 4);
console.log((2 + 3) * 4);输出:
14
20第一行先算乘法;第二行用括号明确要求先算加法。
加减、乘除通常左结合:
console.log(20 / 5 * 2); // (20 / 5) * 2
console.log(10 - 3 - 2); // (10 - 3) - 2输出:
8
5幂运算 ** 是右结合:
console.log(2 ** 3 ** 2); // 2 ** (3 ** 2)
console.log((2 ** 3) ** 2);输出:
512
64括号不仅能改变结果,也能记录意图。即使你知道优先级,isMember && (total >= 99) 也可能比不加括号更容易快速读懂。
下面的交互实验可以逐步观察表达式如何分组:
比较表达式只会得到 true 或 false,因此它们经常充当条件。大小比较使用 >、>=、<、<=,相等比较优先使用 === 和 !==。
const stock = 8;
const requested = 5;
console.log(stock > requested);
console.log(stock >= requested);
console.log(requested === 5);
console.log(requested !== 0);输出:
true
true
true
true注意 > 与 >= 的边界不同。两边相等时,> 是 false,>= 是 true。
== 和 != 会在比较时尝试类型转换;=== 和 !== 同时检查类型和值。默认使用严格比较,读代码的人就不必猜测转换过程。
const numericId = 5;
const textId = "5";
console.log(numericId == textId);
console.log(numericId === textId);
console.log(numericId != textId);
console.log(numericId !== textId);输出:
true
false
false
true两个特殊边界也值得记住:
console.log(null == undefined);
console.log(null === undefined);
console.log(NaN === NaN);
console.log(Number.isNaN(NaN));输出:
true
false
false
true先统一类型,再严格比较。例如把输入 "5" 转为 Number(input),验证它是有效数字后,再与数字 5 使用 === 比较。
两个字符串比较大小时,会按字符的 Unicode 顺序逐位比较,不会自动理解成十进制数字:
console.log("9" < "10");
console.log(Number("9") < Number("10"));
console.log("apple" < "banana");输出:
false
true
true"9" < "10" 首先比较字符 "9" 和 "1",所以结果与数字比较不同。输入本来代表数字时,先显式转换再比较。
单个比较只能回答一个问题,逻辑运算符可以把多个条件组合起来:
a && b:两边都满足时,整体才满足;a || b:至少一边满足时,整体就满足;!a:把真假性反转。const age = 20;
const hasTicket = true;
const isBanned = false;
console.log(age >= 18 && hasTicket);
console.log(hasTicket || age < 12);
console.log(!isBanned);输出:
true
true
true! 的优先级高于 && 和 ||。取反一个完整条件时,用括号明确范围:
console.log(!(age < 18));逻辑运算会先判断操作数的真假性。以下值属于假值:
false;0、-0 和 0n;"";null 和 undefined;NaN。除此之外的值通常都是真值,包括字符串 "0"、空数组 [] 和空对象 {}。
console.log(Boolean(0));
console.log(Boolean("0"));
console.log(Boolean([]));
console.log(!"");输出:
false
true
true
true&& 和 || 会返回操作数逻辑运算符并不总是返回 true 或 false。它们先利用真假性决定求值流程,再返回其中一个原始操作数:
a && b:a 为假时返回 a;否则继续求值并返回 b;a || b:a 为真时返回 a;否则继续求值并返回 b。这就是短路求值:左侧已经足以决定选择哪个值时,右侧不会执行。

短路求值先检查左侧是否足以决定结果;严格相等则同时检查类型和值。
function mark(label, value) {
console.log(`计算${label}`);
return value;
}
console.log(mark("左", "") || mark("右", "匿名"));
console.log(mark("左", false) && mark输出:
计算左
计算右
匿名
计算左
false第一组的左侧是空字符串,|| 必须继续计算右侧。第二组的左侧已经是假值,&& 直接返回 false,所以没有出现 计算右。
?? 只处理真正缺失的值|| 把所有假值都视为需要备用值,可能误伤合法的 0、"" 和 false。空值合并运算符 ?? 只在左侧是 null 或 undefined 时使用右侧。
console.log(0 || 18);
console.log(0 ?? 18);
console.log("" || "未命名");
console.log("" ?? "未命名");
console.log(null ?? "未命名");输出:
18
0
未命名
未命名第四次输出的是空字符串,因此控制台会看到一个空行。只要 0、空字符串或 false 是合法业务值,就优先考虑 ??。
JavaScript 不允许在没有括号的情况下直接混写 ?? 与 &&、||。例如 a ?? b || c 会产生语法错误,应明确写成 (a ?? b) || c 或 a ?? (b || c)。
条件表达式写作 条件 ? 条件为真时的值 : 条件为假时的值。它本身会产生值,因此适合完成简短、清晰的二选一:
const total = 120;
const shippingLabel = total >= 99 ? "免运费" : "运费 8 元";
console.log(shippingLabel);输出:
免运费如果每个分支需要执行多条语句,或者条件不断嵌套,就应改用更容易阅读的 if...else。条件表达式的优势是产生一个值,而不是压缩所有控制逻辑。
下面的实验台可以切换不同输入,观察 &&、|| 和 ?? 是否继续计算右侧:
现在把本章的运算符组合起来,完成一个小型结算程序。程序接收字符串形式的单价和数量,计算小计,根据会员与优惠码决定折扣,再判断是否免运费。
const rawUnitPrice = "39.9";
const rawQuantity = "3";
const couponCode = "SAVE10";
const isMember = true;Number() 把单价和数量转换成数字;subtotal;"SAVE10" 或者用户是会员时,折扣率为 0.1,否则为 0;这里要求数量是正整数,可以使用 Number.isInteger(quantity) && quantity > 0。金额展示可使用 amount.toFixed(2),它返回格式化后的字符串。
¥107.73;couponCode 改为 "NONE" 且 isMember 改为 false 后,折扣应为 ¥0.00;"2"、关闭折扣后,小计应为 ¥79.80,运费应为 ¥8.00;"0"、"2.5" 或 "hello" 时,应输出输入无效提示;===,计算前不依赖字符串的隐式转换。进一步挑战:把免运费门槛、折扣率和默认运费提取成命名常量,再观察表达式是否更容易读懂。
+ 既能做数字加法,也能连接字符串;数学输入应先显式转换。% 得到余数,适合拆分周期和判断奇偶,但负数结果可能为负。=== 和 !==。&&、|| 会短路并返回操作数,! 根据真假性取反。?? 只处理 null 和 undefined,需要保留 0、空字符串或 false 时比 || 更合适。输出:
小计:¥119.70
折扣:-¥11.97
运费:¥0.00
应付:¥107.73