目錄
{tocify} $title={目錄}
為什麼先搞懂「運算式」與「運算子」?
寫 JS 的每一行,多半都是在建立值、變換值、或判斷值。這三件事,幾乎都靠「運算式」與「運算子」完成。
運算式(Expression):會產生「值」的任何語法片段。
例:3 + 4、user.name、getPrice()、a ?? 0、isReady ? "OK" : "WAIT"。
運算子(Operator):用來運算、比較、邏輯組合等的符號或關鍵字。
例:+、-、*、/、===、&&、||、??、!、typeof、delete、in、instanceof、**…等。
理解兩者差別很關鍵:
運算式會回傳一個值(就算是副作用型的運算式,最終也有值,例如賦值表達式)。
運算子是達成運算式的工具,決定運算規則與優先順序。
一句話記:「運算式是名詞(產生值),運算子是動詞(操作值)」。
運算式 vs. 陳述式(Statement)
陳述式(Statement):執行一件事,不一定回傳值。
例:if (x > 0) {...}、for (...) {...}、return y。
運算式:一定能產生值,因此可以嵌在其他表達式或引數中。
例:const y = (x > 0 ? 1 : -1) * 10;
JS 有所謂 「表達式陳述式」(Expression Statement),像是 a = 3; 雖是陳述式,但核心是賦值「表達式」。這也是為什麼很多框架偏好「以表達式為中心」的寫法——組件的 props、函式的回傳,幾乎都靠表達式。
運算子總整理
| 類別 | 運算子 | 名稱 / 用途 | 元數 | 回傳/特性 | 範例 |
|---|---|---|---|---|---|
| 成員/呼叫 | . | 成員存取 | 二元 | 取物件屬性 | user.name |
| 成員/呼叫 | [] | 計算式成員存取 | 二元 | 用鍵/索引取值 | arr[0] obj[key] |
| 成員/呼叫 | () | 函式呼叫 | N 元 | 呼叫並回傳結果 | fn(a,b) |
| 成員/呼叫 | new | 建構 | N 元 | 產生實例(綁定 this) | new Date() |
| 成員/呼叫 | new.target | 目標建構子 | 一元 | 在建構子內得知是否由 new 呼叫 | function C(){ if(!new.target) return new C(); } |
| 成員/呼叫 | super | 父類/原型呼叫 | N 元 | 取父類成員或呼叫父建構子 | super.method() super() |
| 成員/呼叫 | import() | 動態匯入 | 一元 | 回傳 Promise | import('./x.js') |
| 成員/呼叫 | import.meta | 模組中繼資訊 | 一元 | 物件(如 URL) | import.meta.url |
| 成員/呼叫 | ?. | 可選鏈 | 二元 | 左側為 null/undefined 時短路回 undefined | user?.profile?.name fn?.() obj?.[k] |
| 分組 | () | 分組 | 一元 | 改變優先序 | (a + b) * c |
| 算術 | + | 加法 / 字串連接 | 二元 | 數學加法或字串連接 | 1 + 2 "a" + 1 |
| 算術 | - | 減法 | 二元 | 數值相減 | 5 - 3 |
| 算術 | * | 乘法 | 二元 | 數值相乘 | 2 * 4 |
| 算術 | / | 除法 | 二元 | 浮點除法 | 5 / 2 |
| 算術 | % | 餘數 | 二元 | 同號跟隨被除數 | 5 % 2 |
| 算術 | ** | 指數 | 二元(右結合) | 次方 | 2 ** 3 |
| 算術 | +x | 正號 (轉數值) |
一元 | 轉成 number | +"42" |
| 算術 | -x | 負號 | 一元 | 相反數(含轉型) | -"5" |
| 遞增遞減 | ++x | 前置遞增 | 一元 | 先加再回傳新值 | let i=0; ++i |
| 遞增遞減 | x++ | 後置遞增 | 一元 | 先回舊值再加 | let i=0; i++ |
| 遞增遞減 | --x / x-- | 遞減 | 一元 | 與上相反 | --i、i-- |
| 指派 | = | 指派 | 二元(右結合) | 回傳被指派的值 | a = (b = 3) |
| 複合指派(算術) | += -= *= /= %= **= |
算術複合指派 | 二元(右結合) | 計算後回存 | x += 2 |
| 複合指派(位移) | <<= >>= >>>= |
位移複合指派 | 二元 | 同上 | x <<= 1 |
| 複合指派(位元) | &= ^= |= |
位元複合指派 | 二元 | 32 位整數語意 | mask &= 0b1011 |
| 複合指派(邏輯) | &&= ||= ??= |
邏輯/Nullish 複合指派 | 二元 | 依左值短路決定是否更新 | a ||= 10 b &&= f() c ??= 0 |
| 比較 | == != |
寬鬆相等/ 不等 |
二元 | 允許型別轉換 | "5" == 5 |
| 比較 | === !== |
嚴格相等/ 不等 |
二元 | 不轉型 | "5" === 5 |
| 比較 | < <= > >= |
大小比較 | 二元 | 依規則比較字串或數值 | "2" < 10 |
| 關係 | in | 屬性是否存在 | 二元 | 查物件/原型鏈鍵 | "length" in [] |
| 關係 | instanceof | 原型鏈檢查 | 二元 | 物件是否出自 建構子原型鏈 |
d instanceof Date |
| 邏輯 | ! | 邏輯非 | 一元 | 轉布林後取反 | !0 |
| 邏輯 | && | 邏輯且(短路) | 二元 | 回其中一個操作數 | a && b (a 真回 b,否則回 a) |
| 邏輯 | || | 邏輯或(短路) | 二元 | 回其中一個操作數 | a || b( a 真回 a,否則回 b) |
| Null 合併 | ?? | Nullish 合併 | 二元 | 只在 null/undefined 時採右值 |
n ?? 10 |
| 位元 | & | AND | 二元 | 32 位整數運算 | 5 & 3 |
| 位元 | | | OR | 二元 | 同上 | 5 | 2 |
| 位元 | ^ | XOR | 二元 | 同上 | 5 ^ 1 |
| 位元 | ~ | NOT | 一元 | 位元反轉(等於 -(x+1)) | ~5 |
| 位元 | << | 左移 | 二元 | 整數左移 | 1 << 3 |
| 位元 | >> | 右移(保符號) | 二元 | 算術右移 | -8 >> 1 |
| 位元 | >>> | 無號右移 | 二元 | 高位補 0 | -1 >>> 0 |
| 型別/其他 | typeof | 取得型別字串 | 一元 | 回傳如 "number"、 "object" |
typeof fn |
| 型別/其他 | void | 回傳 undefined | 一元 | 丟棄結果 | void 0 |
| 型別/其他 | delete | 刪物件自有屬性 | 一元 | 回傳布林 | delete obj.key |
| 條件 | ?: | 三元條件 | 三元 | 依條件回分支值 | ok ? A : B |
| 逗號 | , | 逗號運算子 | 二元(左結合) | 依序計算、 回最後一個值 |
(x=1, y=2, y) |
| 非同步 | await | 等待 Promise | 一元 | 取解析值(僅 async 中) | const v = await p |
| 產生器 | yield | 產出值 | 一元 | 暫停並回傳給迭代器 | yield 3 |
| 產生器 | yield* | 委派產出 | 一元 | 迭代另一個可迭代物 | yield* arr |
小備註(避免踩雷)
1. + 只要一邊是字串,會走字串連接;要數學加法請先 Number(...)。
2. || 會把 0/""/false 視為「不成立」而取右值;想保留這些值請用 ??。
3. ?. 不能放在指派目標左邊(obj?.x = 1 會錯)。
4. 位元運算都以 32 位有號整數處理;大整數請改 BigInt 或字串方案。
5. await 只能在 async 函式內;yield 只能在 function* 內。
運算子的主要分類與用途
1. 算術運算子
二元:+ 加、- 減、* 乘、/ 除、% 取餘、** 次方
5 % 2; // 1
2 ** 3; // 8
一元:+x 轉數值、-x 負號
+"42"; // 42(number) -"5"; // -5(number)
2. 字串連接(+ 的雙重身份)
"Hello " + "JS"; // "Hello JS" "5" + 1; // "51"(字串連接,而非數學加法)
只要任一側是字串,+ 多半走字串連接。想做數學加法,請先明確轉型:Number("5") + 1 // 6。
3. 指派(賦值)運算子
基本:=
複合:+=、-=、*=、/=、%=、**=、<<=、>>=…
let n = 3; n += 2; // 5 n **= 2; // 25
4. 比較運算子
寬鬆相等:==、!=(不建議)
嚴格相等:===、!==(預設使用)
大小比較:>、>=、<、<=
0 == false; // true(型別轉換介入) 0 === false; // false(型別不同) "2" > 10; // false(字串轉數值後比較)
5. 邏輯運算子與短路行為
&&(且)、||(或)、!(非)
短路(short-circuit)會直接回傳其中一個操作數,而不一定是布林:
"A" && "B"; // "B"(因為左邊 truthy,回右邊) "" || "B"; // "B"(左邊 falsy,回右邊) !0; // true
6) Null 合併運算子 ??
只在 左側為 null 或 undefined 時才採右側預設值。
0 ?? 10; // 0(不觸發預設) null ?? 10; // 10 undefined ?? "X"; // "X"
與 || 的差異:0、""、false 在 || 會被當作 falsy 而觸發預設;在 ?? 不會。
可選鏈(Optional Chaining)?.
避免存取不存在屬性時拋錯:
user?.profile?.name ?? "Guest";
注意:?. 是運算子,常與 ?? 搭配,提升健壯性。
8. 型別/物件相關
typeof x:回傳型別字串(如 "number", "object", "function")。
instanceof:判斷原型鏈。date instanceof Date
in:屬性名是否存在於物件或其原型鏈。"length" in [] // true
delete obj.key:刪除物件自有屬性(刪不掉 let/const 變數)。
9. 位元運算子(常用於效能或旗標)
&、|、^、~、<<、>>、>>>。
5 & 3; // 1(0101 & 0011 = 0001)
10. 其餘常見
三元:cond ? A : B(把 if/else 收斂成值)
逗號:(expr1, expr2) 回傳最後一個值(少用,易讀性差)
括號 ():分組、決定優先序
展開/其餘(...):不是嚴格意義的運算子,但在表達式中常用來展開陣列/物件或收集其餘參數。
優先序與結合性(Precedence & Associativity)
不同運算子有不同優先序(誰先算)與結合性(同優先序時,從左或從右算)。
幾個實務重點:
() 最高可讀性,不確定就加括號。
** 與賦值(含 =、+= 等)是右結合:a = b = 3; 會先算 b = 3。
?? 與 &&/|| 不可混用而不加括號:a ?? b || c 會報錯或造成閱讀困難。
正確寫法:a ?? (b || c) 或 (a ?? b) || c,依你想要的邏輯加括號。
實作範例
範例 1:安全讀值 + 預設值
function greet(user) {
const name = user?.profile?.name ?? "訪客";
return `Hi, ${name}!`;
}
範例 2:輸入正規化(避免字串相加陷阱)
function add(a, b) {
const x = Number(a);
const y = Number(b);
if (Number.isNaN(x) || Number.isNaN(y)) {
throw new Error("輸入需為可轉成數字的值");
}
return x + y;
}
範例 3:條件運算(收斂 if/else)
const levelLabel = (score) => score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "D";
巢狀三元式可讀性差時,改回 if/else 或抽出函式映射。
範例 4:位元旗標(啟用多個選項)
const FLAG_READ = 1 << 0; // 0001 const FLAG_WRITE = 1 << 1; // 0010 const FLAG_EXEC = 1 << 2; // 0100 let perm = FLAG_READ | FLAG_WRITE; // 0011 const canExec = (perm & FLAG_EXEC) !== 0; // false
範例 5:鏈式賦值與副作用
let a, b; a = b = 10; // 先算 (b = 10) 回傳 10,再指派給 a
可讀性考量,不建議過度使用鏈式賦值。
一步步實作流程
步驟 1|釐清需求:你要的是什麼「值」?
先想「這段程式最後要回傳什麼值?」
把 if/else 能改寫成一個表達式就改(保留可讀性)。
步驟 2|選擇正確運算子
做數學:+ - * / % **
做判斷:=== !== > < >= <=
安全讀值:?. 搭 ??
條件分支:cond ? A : B
預設值:優先 ??,除非真的要把 ""/0/false 視為「無效」。
步驟 3|控制優先序
不要賭運算子優先序,加括號讓意圖明確,尤其 ??、&&、|| 混用時。
步驟 4|型別確認與轉換
讀取外部輸入(URL、表單、localStorage)前先轉型:Number()、String()、Boolean()。
檢查數字有效性:Number.isNaN(x) 與 Number.isFinite(x)。
步驟 5|驗證與除錯
console.log({ value, type: typeof value });
若牽涉可選鏈,刻意餵 null/undefined 測試。
對比 == 與 === 差異,固定使用 ===。
常見錯誤與雷點
1. == 造成的陰招
"" == 0; // true false == 0; // true null == undefined; // true
建議:預設用 === / !==。
2. + 被迫變字串連接
"5" + 1; // "51"
解法:先 Number("5") 再加;或確保兩側都是數字。
3. NaN 的比較
NaN === NaN; // false Number.isNaN(NaN); // true(正確檢查)
4. 浮點數精度
0.1 + 0.2 === 0.3; // false
作法:以整數計算(乘上 100 後再除)或用誤差範圍比較。
5. || 當預設值踩雷
const n = 0 || 10; // 10(你失去合法的 0)
改用:??,只在 null/undefined 才套預設。
6. ?? 與 &&/|| 混用未加括號
a ?? b || c; // 問題多,請加括號
固定:(a ?? b) || c 或 a ?? (b || c)。
7. 可選鏈誤用在賦值左側
user?.name = "A"; // ❌ SyntaxError
原則:?. 只用在存取時,不能當賦值目標。
8. delete 想刪變數
let x = 1; delete x; // false(刪不掉)
用途:刪「物件自有屬性」:delete obj.key。
9. 前置/後置 ++ 差異
let i = 0; i++ // 回傳 0,再把 i 變 1 ++i // 先把 i 變 1,然後回傳 1
在表達式中會有差異,避免寫成一行巫術。
10. parseInt 忘了基底
parseInt("08"); // 8(現代環境多半 OK,但仍建議指定 10)
parseInt("101", 2); // 5
更穩:數字字串用 Number("08")。
11. 位元運算只保留 32 位有號整數
大數或超過 2^31−1 的運算會溢位;需要大整數請用 BigInt 或字串庫。
12. instanceof 與跨 realm
不同 iframe/執行環境的 Array 彼此 instanceof 可能為 false。
普適檢查:Array.isArray(v)、Object.prototype.toString.call(v)。
13. 三元巢狀過深
可讀性直接掛掉。改成 if/else 或字典映射。
14. 忘了括號導致優先序錯判
a + b * c 與 (a + b) * c 天差地遠。寧願多兩個括號,少一天查 bug。
實務範式:把「安全」當預設
A. 讀取深層屬性:?. + ??
const price = product?.variants?.[0]?.price ?? 0;
B. 可輸入多型別但要數學計算:顯式轉型
const total = [10, "20", "3"].reduce((s, v) => s + Number(v), 0); // 33
C. 條件輸出字串:三元 + 模板字串
const tag = isHot ? "🔥 熱門" : "一般";
const label = `商品狀態:${tag}`;
D. 容錯預設值(保留 falsy 合法值)
const page = Number(query.page ?? 1); // 1, 2, 3... const size = Number(query.size ?? 20); // 預設 20
快速對照表
| 目的 | 推薦寫法 | 備註 |
|---|---|---|
| 嚴格比較 | a === b / a !== b |
避免 == |
| 預設值(允許 0/""/false) | a ?? defaultVal |
只處理 null/undefined |
| 安全取值 | obj?.x?.y |
不會拋錯 |
| 數字轉型 | Number(v) |
檢查 Number.isNaN(v) |
| 字串相加 | 先轉型再 + |
避免 "5"+1 |
| 浮點運算 | 以整數運算或誤差比較 | 避免 0.1+0.2 陷阱 |
| 陣列判斷 | Array.isArray(v) |
勝過 instanceof |
| 刪屬性 | delete obj.key |
刪不掉 let/const 變數 |
結語:讓每一個「值」都在你掌控內
把 JS 想成「以表達式為核心」的語言:先想要的值,再選運算子,最後用括號固定邏輯。
穩定的預設是:===、??、?.、顯式轉型、必要括號。做完再用 console.log 與邊界值驗證一次。多一層確定,少一天救火。
