运算符(Operator)是用于执行操作的符号,表达式(Expression)是由运算符和操作数组成的代码片段,会产生一个值。
JavaScript的运算符可以分为以下几类:
let a = 10, b = 3; // 加法 a + b; // 13 "Hello" + " " + "World"; // "Hello World" // 减法 a - b; // 7 // 乘法 a * b; // 30 // 除法 a / b; // 3.333... // 取余(模运算) a % b; // 1(10除以3余1) 10 % 2; // 0(偶数) // 指数运算(ES2016) a ** b; // 1000(10的3次方) Math.pow(a, b); // 1000(等价写法) // 字符串与数字运算 "5" + 3; // "53"(字符串拼接) "5" - 3; // 2(字符串转数字) "5" * 3; // 15 "5" / 3; // 1.666...
let x = 5; // 一元正号(通常无实际作用,但可用于类型转换) +"123"; // 123(转数字) +true; // 1 // 一元负号(取反) -x; // -5 // 递增(++) let y = 5; y++; // 后置递增,返回5,然后y变为6 ++y; // 前置递增,y先变为7,然后返回7 // 递减(--) let z = 5; z--; // 后置递减,返回5,然后z变为4 --z; // 前置递减,z先变为3,然后返回3 // 前置和后置的区别 let a = 5; let b = a++; // b = 5, a = 6 let c = ++a; // a = 7, c = 7 let m = 5; let n = --m; // m = 4, n = 4 let p = m--; // p = 4, m = 3
特殊数值运算:
// Infinity运算 Infinity + Infinity; // Infinity Infinity - Infinity; // NaN Infinity * 0; // NaN Infinity / Infinity; // NaN 10 / 0; // Infinity -10 / 0; // -Infinity // NaN传播 NaN + 5; // NaN NaN * 0; // NaN NaN / NaN; // NaN // 任何数与NaN比较都是false NaN === NaN; // false NaN == NaN; // false isNaN(NaN); // true Number.isNaN(NaN); // true
浮点数运算问题:
// 精度丢失 0.1 + 0.2; // 0.30000000000000004 0.1 + 0.2 === 0.3; // false // 解决方案 // 1. 使用toFixed parseFloat((0.1 + 0.2).toFixed(10)); // 0.3 // 2. 使用整数运算 (1 + 2) / 10; // 0.3 // 3. 使用Number.EPSILON function floatEqual(a, b) { return Math.abs(a - b) < Number.EPSILON; } floatEqual(0.1 + 0.2, 0.3); // true
// 简单赋值 let x = 10; // 连续赋值(从右到左) let a, b, c; a = b = c = 5; // a=5, b=5, c=5
let x = 10; // 加法赋值 x += 5; // x = x + 5; 结果为15 // 减法赋值 x -= 3; // x = x - 3; 结果为12 // 乘法赋值 x *= 2; // x = x * 2; 结果为24 // 除法赋值 x /= 4; // x = x / 4; 结果为6 // 取余赋值 x %= 4; // x = x % 4; 结果为2 // 指数赋值(ES2016) x **= 3; // x = x ** 3; 结果为8 // 位运算赋值(稍后讲解) let y = 5; y &= 3; // y = y & 3; y |= 4; // y = y | 4; y ^= 2; // y = y ^ 2; y <<= 1; // y = y << 1; y >>= 1; // y = y >> 1; y >>>= 1; // y = y >>> 1;
// 数组解构 let [a, b] = [1, 2]; console.log(a, b); // 1 2 // 跳过元素 let [x, , z] = [1, 2, 3]; console.log(x, z); // 1 3 // 剩余元素 let [first, ...rest] = [1, 2, 3, 4]; console.log(first); // 1 console.log(rest); // [2, 3, 4] // 默认值 let [m = 1, n = 2] = [10]; console.log(m, n); // 10 2 // 对象解构 let {name, age} = {name: "张三", age: 25}; console.log(name, age); // "张三" 25 // 重命名 let {name: userName, age: userAge} = {name: "李四", age: 30}; console.log(userName); // "李四" // 默认值 let {status = "active"} = {}; console.log(status); // "active" // 嵌套解构 let user = { info: { address: { city: "北京" } } }; let {info: {address: {city}}} = user; console.log(city); // "北京"
// 宽松相等(==)- 会进行类型转换 5 == "5"; // true 0 == false; // true "" == false; // true null == undefined; // true // 严格相等(===)- 不进行类型转换(推荐) 5 === "5"; // false 0 === false; // false null === undefined; // false // 不等比较 5 != "5"; // false 5 !== "5"; // true
严格相等 vs 宽松相等:
始终使用 === 和 !==,避免意外的类型转换。
// 宽松相等的奇怪行为 [] == false; // true [] == ![]; // true '' == false; // true 0 == '0'; // true 0 == []; // true '0' == []; // false // NaN的特殊性 NaN == NaN; // false NaN === NaN; // false Object.is(NaN, NaN); // true(ES6新方法)
let a = 5, b = 10; // 大于 a > b; // false // 小于 a < b; // true // 大于等于 a >= 5; // true // 小于等于 b <= 10; // true // 字符串比较(按字典序) "apple" < "banana"; // true "Apple" < "apple"; // true(大写字母ASCII码小) "10" < "2"; // true(字符串逐字符比较)
字符串比较原理:
// Unicode码点比较 "a".charCodeAt(0); // 97 "b".charCodeAt(0); // 98 "A".charCodeAt(0); // 65 // 中文字符比较 "中".charCodeAt(0); // 20013 "国".charCodeAt(0); // 22269 "中国" < "美国"; // true(因为 20013 < 32654)
// 基本用法 true && true; // true true && false; // false false && true; // false false && false; // false // 短路求值 // 如果第一个操作数为假,直接返回第一个,不计算第二个 let x = 0; false && (x = 10); // x 仍然是 0 // 常用模式:条件执行 isValid && processData(); // 返回第一个假值或最后一个真值 0 && "hello"; // 0 1 && "hello"; // "hello" "hello" && 42; // 42
// 基本用法 true || true; // true true || false; // true false || true; // true false || false; // false // 短路求值 // 如果第一个操作数为真,直接返回第一个,不计算第二个 true || console.log("不会执行"); // 常用模式:默认值 let name = userName || "Anonymous"; // 返回第一个真值或最后一个假值 0 || "hello"; // "hello" "" || 42; // 42 null || undefined; // undefined
空值合并运算符 ??(ES2020):
// || 的问题:假值会被替换 0 || 100; // 100(但0可能是有效值) "" || "default"; // "default"(但空字符串可能是有效的) // ?? 只在 null 和 undefined 时返回右侧值 0 ?? 100; // 0 "" ?? "default"; // "" null ?? "default"; // "default" undefined ?? "default"; // "default"
// 基本用法 !true; // false !false; // true // 双重否定转布尔 !!"hello"; // true !!0; // false !!{}; // true !![]; // true // 等价的布尔转换 Boolean("hello"); // true Boolean(0); // false
let x = 0; let y = 10; // 逻辑与赋值(&&=) x &&= y; // 等价于 x = x && y // x 是 0(假值),所以 x 保持 0 // 逻辑或赋值(||=) let name = ""; name ||= "default"; // name 是 ""(假值),所以赋值为 "default" // 空值合并赋值(??=) let value = null; value ??= "fallback"; // value 是 null,所以赋值为 "fallback" let zero = 0; zero ??= 100; // zero 保持 0(不是null或undefined)
位运算符直接操作数字的二进制表示。
// 按位与(&)- 两个都为1才是1 5 & 3; // 1 // 5: 101 // 3: 011 // &: 001 = 1 // 按位或(|)- 有一个为1就是1 5 | 3; // 7 // 5: 101 // 3: 011 // |: 111 = 7 // 按位异或(^)- 不同为1,相同为0 5 ^ 3; // 6 // 5: 101 // 3: 011 // ^: 110 = 6 // 按位非(~)- 取反(包括符号位) ~5; // -6 // 5: 0000...00000101 // ~5: 1111...11111010 = -6(补码表示) // 左移(<<)- 乘以2的n次方 5 << 1; // 10 5 << 2; // 20 // 右移(>>)- 带符号右移(除以2的n次方,保留符号) -16 >> 2; // -4 16 >> 2; // 4 // 无符号右移(>>>)- 用0填充高位 -16 >>> 2; // 1073741820(很大的正数)
权限管理:
// 使用位运算管理权限 const PERMISSION_READ = 1 << 0; // 1 (0001) const PERMISSION_WRITE = 1 << 1; // 2 (0010) const PERMISSION_EXECUTE = 1 << 2; // 4 (0100) const PERMISSION_DELETE = 1 << 3; // 8 (1000) // 组合权限 let userPermissions = PERMISSION_READ | PERMISSION_WRITE; // 3 // 检查权限 if (userPermissions & PERMISSION_READ) { console.log("有读权限"); } if (userPermissions & PERMISSION_EXECUTE) { console.log("有执行权限"); // 不会执行 } // 添加权限 userPermissions |= PERMISSION_EXECUTE; // 现在为7 // 移除权限 userPermissions &= ~PERMISSION_WRITE; // 现在为5 // 切换权限 userPermissions ^= PERMISSION_DELETE; // 添加或删除权限
颜色处理:
// RGB颜色提取 let color = 0xFF5733; // 16734003 let red = (color >> 16) & 0xFF; // 255 let green = (color >> 8) & 0xFF; // 87 let blue = color & 0xFF; // 51 // 组合RGB let newColor = (red << 16) | (green << 8) | blue;
判断奇偶:
// 比 % 2 更快 function isOdd(n) { return (n & 1) === 1; } function isEven(n) { return (n & 1) === 0; } isOdd(5); // true isEven(4); // true
交换两个数:
// 不使用临时变量 let a = 5, b = 3; a = a ^ b; b = a ^ b; // b 现在是原来的 a a = a ^ b; // a 现在是原来的 b console.log(a, b); // 3 5
// 语法:condition ? valueIfTrue : valueIfFalse let age = 20; let status = age >= 18 ? "成年" : "未成年"; // 嵌套三元(不推荐,可读性差) let category = age < 13 ? "儿童" : age < 20 ? "青少年" : age < 60 ? "成年人" : "老年人"; // 与if-else等价 let message; if (age >= 18) { message = "成年"; } else { message = "未成年"; }
typeof undefined; // "undefined" typeof null; // "object"(历史bug) typeof true; // "boolean" typeof 42; // "number" typeof 42n; // "bigint" typeof "hello"; // "string" typeof Symbol(); // "symbol" typeof {}; // "object" typeof []; // "object" typeof function() {}; // "function" // 判断数组 Array.isArray([]); // true
检查对象是否是某个类的实例。
[] instanceof Array; // true {} instanceof Object; // true new Date() instanceof Date; // true // 构造函数 function Person() {} let p = new Person(); p instanceof Person; // true // 原型链 class Animal {} class Dog extends Animal {} let dog = new Dog(); dog instanceof Dog; // true dog instanceof Animal; // true dog instanceof Object; // true
检查属性是否在对象中。
let obj = { name: "张三", age: 25 }; "name" in obj; // true "toString" in obj; // true(继承自原型) let arr = ["a", "b", "c"]; 0 in arr; // true 3 in arr; // false "length" in arr; // true // 删除属性后 delete obj.age; "age" in obj; // false
let user = { profile: { name: "张三" } }; // 传统写法(容易出错) // let name = user.profile.name; // 如果profile为null/undefined会报错 // 安全访问 let name = user && user.profile && user.profile.name; // 使用可选链 let name2 = user?.profile?.name; // "张三" // 访问不存在的嵌套属性 let city = user?.address?.city; // undefined,不会报错 // 与函数调用结合 let result = someObject?.someMethod?.(); // 与方括号结合 let prop = user?.["profile"]?.["name"];
// 展开数组 let arr1 = [1, 2, 3]; let arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5] // 复制数组(浅拷贝) let copy = [...arr1]; // 合并数组 let merged = [...arr1, ...arr2]; // 展开对象(ES2018) let obj1 = { a: 1, b: 2 }; let obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 } // 对象合并(后面的属性覆盖前面的) let defaults = { theme: "light", fontSize: 14 }; let userPrefs = { fontSize: 16 }; let settings = { ...defaults, ...userPrefs }; // { theme: "light", fontSize: 16 } // 函数参数展开 function sum(...numbers) { return numbers.reduce((a, b) => a + b, 0); } sum(1, 2, 3, 4); // 10 // 数组解构中的剩余元素 let [first, second, ...rest] = [1, 2, 3, 4, 5];
运算符优先级决定了表达式中运算的执行顺序。
// 优先级从高到低: // 1. 分组 ( ) (2 + 3) * 4; // 20 // 2. 成员访问 . 、计算属性 [] 、new (带参数列表) 、函数调用 () obj.property; obj["property"]; new Date(); func(); // 3. new (无参数列表) new Date; // 4. 后置递增/递减 ++ -- x++; x--; // 5. 逻辑非 ! 、按位非 ~ 、一元加减 + - 、前置递增/递减 ++ -- 、typeof 、void 、delete 、await !true; ~5; +x; ++x; typeof x; await promise; // 6. 指数 **(右结合) 2 ** 3 ** 2; // 512(2 ** (3 ** 2) = 2 ** 9) // 7. 乘除取余 * / % 10 / 2 * 5; // 25(从左到右) // 8. 加减 + - 5 + 3 - 2; // 6(从左到右) // 9. 移位 << >> >>> 8 >> 1; // 4 // 10. 比较 < <= > >= instanceof in 5 > 3; "a" in obj; // 11. 相等 == != === !== 5 === "5"; // 12. 按位与 & 5 & 3; // 13. 按位异或 ^ 5 ^ 3; // 14. 按位或 | 5 | 3; // 15. 逻辑与 && true && false; // 16. 逻辑或 || true || false; // 17. 空值合并 ?? null ?? "default"; // 18. 条件 ?: condition ? a : b; // 19. 赋值 = += -= 等 x = 5; x += 3; // 20. yield 、yield* 、=> function* gen() { yield 1; } () => {}; // 21. 展开 ... [...arr];
优先级示例:
// 复杂的优先级示例 let a = 1, b = 2, c = 3; // 等价于:a + (b * c) a + b * c; // 7 // 等价于:a * (b + c) a * b + c; // 5(不是9) // 等价于:(a && b) || c a && b || c; // 2 // 使用括号明确意图 (a + b) * c; // 9(更清晰)
预测以下表达式的结果:
2 + 3 * 4; (2 + 3) * 4; 10 / 2 * 5; 10 / (2 * 5); 5 > 3 && 2 > 4; 5 > 3 || 2 > 4 && 1 > 0; 2 ** 3 ** 2; (2 ** 3) ** 2;
完成以下位运算练习:
// 1. 编写函数 isPowerOfTwo,判断一个数是否是2的幂次方 // 提示:2的幂次方的二进制表示只有一个1 // 2. 编写函数 getBit,获取数字n的第i位(从0开始) // getBit(5, 0) 应该返回 1(5的二进制是101) // 3. 编写函数 setBit,将数字n的第i位设为1 // 4. 编写函数 clearBit,将数字n的第i位设为0 // 5. 编写函数 toggleBit,切换数字n的第i位(0变1,1变0)
分析以下代码的执行结果:
let x = 0; let y = 5; let result1 = x && (y = 10); console.log(x, y, result1); let result2 = x || (y = 20); console.log(x, y, result2); let result3 = null ?? (y = 30); console.log(y, result3); let result4 = 0 ?? (y = 40); console.log(y, result4);
编写一个函数 `safeGet`,使用可选链和空值合并运算符,安全地获取嵌套对象属性,如果属性不存在则返回默认值:
const user = { profile: { address: { city: "北京" } } }; safeGet(user, "profile.address.city", "未知"); // "北京" safeGet(user, "profile.address.zipCode", "未知"); // "未知" safeGet(user, "settings.theme", "light"); // "light" safeGet(null, "any.path", "default"); // "default"
练习1答案:
2 + 3 * 4; // 14(先乘后加) (2 + 3) * 4; // 20 10 / 2 * 5; // 25(从左到右) 10 / (2 * 5); // 1 5 > 3 && 2 > 4; // false 5 > 3 || 2 > 4 && 1 > 0; // true(&&优先级高于||) 2 ** 3 ** 2; // 512(右结合) (2 ** 3) ** 2; // 64
练习2答案:
// 1. 判断2的幂次方 function isPowerOfTwo(n) { return n > 0 && (n & (n - 1)) === 0; } // 原理:2的幂次方只有一位是1,n-1会将该位及后面所有位反转 // 8 = 1000 // 7 = 0111 // 8 & 7 = 0000 = 0 // 2. 获取第i位 function getBit(n, i) { return (n >> i) & 1; } // 3. 设置第i位为1 function setBit(n, i) { return n | (1 << i); } // 4. 清除第i位 function clearBit(n, i) { return n & ~(1 << i); } // 5. 切换第i位 function toggleBit(n, i) { return n ^ (1 << i); }
练习3答案:
let x = 0; let y = 5; let result1 = x && (y = 10); // x是0(假值),短路返回x console.log(x, y, result1); // 0, 5, 0 let result2 = x || (y = 20); // x是0(假值),执行y=20,返回20 console.log(x, y, result2); // 0, 20, 20 let result3 = null ?? (y = 30); // null,执行y=30,返回30 console.log(y, result3); // 30, 30 let result4 = 0 ?? (y = 40); // 0不是null/undefined,短路返回0,y保持30 console.log(y, result4); // 30, 0
练习4答案:
function safeGet(obj, path, defaultValue) { if (obj == null) return defaultValue; const keys = path.split('.'); let result = obj; for (const key of keys) { result = result?.[key]; if (result === undefined) { return defaultValue; } } return result ?? defaultValue; } // 或使用reduce的简洁版本 function safeGet2(obj, path, defaultValue) { const result = path.split('.').reduce((o, k) => o?.[k], obj); return result ?? defaultValue; } // 测试 const user = { profile: { address: { city: "北京" } } }; console.log(safeGet(user, "profile.address.city", "未知")); // "北京" console.log(safeGet(user, "profile.address.zipCode", "未知")); // "未知" console.log(safeGet(user, "settings.theme", "light")); // "light" console.log(safeGet(null, "any.path", "default")); // "default"
本章详细介绍了JavaScript的各种运算符:
关键要点:
下一章将学习控制流语句,实现程序的条件执行和循环处理。