3. 运算符与表达式:算术、比较、逻辑、三元与优先级

运算符这东西,说白了就是让数据「动起来」的工具。我刚开始学 JavaScript 时,总觉得运算符很简单,不就是加减乘除嘛。但后来在项目中踩过几次坑,才发现这里面的门道比想象中多得多。

今天咱们就把运算符这块彻底捋清楚。我会结合自己实际开发中的经验,把每个运算符的用法、坑点、以及什么时候该用哪个,都给你讲明白。

3.1 算术运算符:不只是加减乘除

算术运算符大家最熟悉了:+-*/%(取余)、**(幂运算)。但有几个细节,我建议你特别注意。

核心要点:JavaScript 中的 + 运算符有双重身份——数值相加和字符串拼接。这是最容易出 bug 的地方。

// 数值相加
console.log(10 + 20); // 30

// 字符串拼接
console.log('10' + 20); // '1020'
console.log(10 + '20'); // '1020'

// 混合运算
console.log(10 + 20 + '30'); // '3030',先算 10+20=30,再拼 '30'
console.log('10' + 20 + 30); // '102030',从左到右全部拼接

我曾经在做一个购物车功能时,遇到过这么个问题:商品单价是数字,数量也是数字,但总价算出来却是个字符串。查了半天才发现,原来是从接口拿到的单价字段是字符串类型。嗯,从那以后我养成了一个习惯——做数值运算前,先用 Number() 或一元 + 转一下。

小技巧:一元 + 运算符可以快速将字符串转为数字:+'42' 结果是 42。但要注意,如果字符串不能转成有效数字,会得到 NaN

取余运算符 % 在实际开发中非常实用。比如判断奇偶、分页计算、循环队列等场景都会用到。

// 判断奇偶
let num = 7;
if (num % 2 === 0) {
  console.log('偶数');
} else {
  console.log('奇数');
}

// 分页:计算当前页第一条数据的索引
let page = 3;
let pageSize = 10;
let startIndex = (page - 1) * pageSize; // 20

// 循环队列
let index = 0;
let arr = ['a', 'b', 'c'];
index = (index + 1) % arr.length; // 1
index = (index + 1) % arr.length; // 2
index = (index + 1) % arr.length; // 0,回到开头

3.2 比较运算符:小心隐式类型转换

比较运算符包括 =====!=!==><>=<=

我个人习惯:永远使用 ===!==。为什么?因为 == 会做隐式类型转换,结果往往出乎意料。

// 看看这些结果,你猜对几个?
console.log(0 == false);   // true
console.log('' == false);  // true
console.log(null == undefined); // true
console.log('0' == 0);     // true
console.log([] == false);  // true

// 用 === 就清晰多了
console.log(0 === false);   // false
console.log('' === false);  // false
console.log(null === undefined); // false

注意:我曾经在写一个权限判断时,用了 == 比较用户角色。结果因为某个字段是字符串 '0',而另一个是数字 0,导致判断逻辑完全跑偏。那次排查花了我整整两个小时。所以,记住我的教训——能用 === 就别用 ==

字符串比较是按字典序(Unicode 编码)进行的。这一点在排序时特别重要。

console.log('apple' > 'banana'); // false,因为 'a' 的编码小于 'b'
console.log('2' > '12');        // true,因为 '2' 的编码大于 '1'
console.log(2 > '12');          // false,因为数字比较时 '12' 被转成了 12

3.3 逻辑运算符:短路求值是个好东西

逻辑运算符有三个:&&(与)、||(或)、!(非)。它们有个重要特性——短路求值

短路求值规则:

  • a && b:如果 a 为假,直接返回 a,不再计算 b
  • a || b:如果 a 为真,直接返回 a,不再计算 b

这个特性在实际开发中非常实用。我经常用它来做「默认值」和「条件执行」。

// 设置默认值
let username = inputUsername || '匿名用户';

// 条件执行
isLoggedIn && renderDashboard();

// 更复杂的例子
let config = {
  theme: userConfig.theme || 'light',
  lang: userConfig.lang || 'zh-CN',
  showTips: userConfig.showTips !== false // 默认 true
};

你想想看,如果没有短路求值,每次都要写 if...else,代码会变得很啰嗦。但用 ||&& 一行就能搞定。

小技巧:!! 可以将任意值转为布尔类型:!!0false!!'hello'true。这比 Boolean() 函数写起来更简洁。

3.4 三元运算符:简洁的条件表达式

三元运算符 条件 ? 值1 : 值2if...else 的简洁写法。我个人非常喜欢用它,但有个原则——只在表达式简单时使用

// 基础用法
let age = 20;
let status = age >= 18 ? '成年' : '未成年';

// 嵌套三元(不推荐,可读性差)
let score = 85;
let grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : 'D';

// 更好的写法:用 if...else 或 switch
function getGrade(score) {
  if (score >= 90) return 'A';
  if (score >= 80) return 'B';
  if (score >= 70) return 'C';
  return 'D';
}

注意:我曾经接手过一个项目,里面有个三层嵌套的三元运算符。我看了五分钟才搞明白它在做什么。从那以后,我给自己定了个规矩——超过一层的三元,一律改成 if...elseswitch。代码是写给人看的,不是写给机器看的。

3.5 运算符优先级:别让代码「猜」你的意图

运算符优先级决定了表达式中哪个运算符先执行。虽然我们可以查表,但我建议你记住一个原则:不确定优先级时,就用括号

优先级 运算符 说明
1(最高) () 括号分组
2 ++ -- ! 一元运算符
3 * / % 乘除取余
4 + - 加减
5 < > <= >= 比较运算符
6 === !== 相等性判断
7 && 逻辑与
8 || 逻辑或
9(最低) ?: 三元运算符

看这个表格你会发现,&& 的优先级比 || 高。这意味着 a || b && c 实际上被解释为 a || (b && c)

// 容易混淆的例子
let a = true, b = false, c = true;
console.log(a || b && c);  // true,等价于 a || (b && c)
console.log((a || b) && c); // true,但含义不同

// 实际开发中的坑
let user = { name: '张三', age: 25 };
// 我想判断:如果用户存在且年龄大于18,或者用户名为空
// 错误写法
if (user && user.age > 18 || !user.name) {
  // 这里逻辑有问题,因为 && 优先级高于 ||
}
// 正确写法
if ((user && user.age > 18) || !user.name) {
  // 用括号明确优先级
}

我的建议:不要依赖记忆中的优先级顺序。写代码时,只要涉及两种以上运算符,就用括号把意图表达清楚。这样既避免了 bug,也让读你代码的人一目了然。

3.6 综合实战:一个表单验证的例子

说了这么多,咱们来写一个实际点的例子。假设我们要验证一个用户注册表单:

function validateForm(formData) {
  // 用户名:必填,长度 2-20
  let username = formData.username || '';
  if (!username || username.length < 2 || username.length > 20) {
    return '用户名长度需在2-20位之间';
  }

  // 年龄:可选,但如果填写必须是 0-150 的数字
  let age = Number(formData.age);
  if (formData.age !== undefined && formData.age !== '') {
    if (isNaN(age) || age < 0 || age > 150) {
      return '年龄必须是0-150之间的数字';
    }
  }

  // 邮箱:可选,但如果填写必须包含 @
  let email = formData.email || '';
  if (email && !email.includes('@')) {
    return '邮箱格式不正确';
  }

  // 同意协议:必须勾选
  if (!formData.agree) {
    return '请先同意用户协议';
  }

  return null; // 验证通过
}

// 测试
console.log(validateForm({ username: 'abc', age: '25', email: 'test@example.com', agree: true }));
// null,验证通过

console.log(validateForm({ username: 'a', age: '-1', agree: false }));
// '用户名长度需在2-20位之间'

这个例子用到了咱们今天讲的大部分运算符:比较运算符、逻辑运算符、三元运算符(虽然这里没用,但你可以试试把 || 改成三元)。而且你发现没有,!|| 的配合使用,让代码变得非常简洁。

总结一下:运算符是 JavaScript 的基础,但基础不等于简单。我见过不少资深开发者在运算符上翻车。记住三点:用 === 代替 ==、善用短路求值、不确定优先级就用括号。做到这三点,运算符这块基本就不会出问题了。