如果你剛開始寫 JS,或是寫了一陣子總覺得哪裡怪怪的,這篇是把觀念、手感、工具一次補齊的快速路線。從命名與保留字,到變數宣告與作用域,再到函數寫法與分號選擇,我會用可複製的範例帶你走過一遍。
中間穿插「雷點清單」:像是 ASI 造成的 return 陷阱、await 沒加錯誤處理、innerHTML 帶來的 XSS 風險。讀完不會變魔法師,但能少走冤枉路,寫得更乾淨也更安心。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
有哪些字不能用?——保留字與「不該用」的識別字
ECMAScript 保留字(關鍵字)
以下字不能拿來當變數/函數名(不同階段與嚴格模式有細微差異,以下列常見且實務需避用者):
break, case, catch, class, const, continue,
debugger, default, delete, do, else, enum,
export, extends, false, finally, for, function,
if, implements, import, in, instanceof, interface,
let, new, null, package, private, protected,
public, return, super, switch, static, this, throw,
true, try, typeof, var, void, while, with, yield, await
備註 :
1. await 在 async/module 情境下視為保留。
2. of 是「情境保留字」(如 for...of),一般不要用來當識別字。
3. arguments、eval 雖非關鍵字,但強烈建議不要拿來命名,尤其在嚴格模式下有多種限制與副作用。
4. 物件屬性可以使用保留字(以字串鍵名),如:obj["default"] = 1,但識別字(變數名)不行。
命名規則與限制
識別字可使用 Unicode,但不建議用容易混淆的字元(例如全形空白、相似字母),避免可讀性問題。
不可用數字開頭(1name ❌),可用底線/錢字號(_name, $el)但需要節制。
建議:
變數/函數採 camelCase(totalPrice)
類別/建構式用 PascalCase(UserService)
常數(非重新指定、跨檔案共用)可 UPPER_SNAKE_CASE(MAX_RETRY)
布林前綴 is/has/can/should:isReady、hasToken
變數宣告與作用域:var / let / const
1. 三種宣告的本質差異
var:
函數作用域、會變數提升(hoisting)、可重複宣告,容易出 bug,除非維護舊碼,不建議新專案使用。
let:
區塊作用域、不可重複宣告、存在 暫時性死區(TDZ),可重新賦值。
const:
區塊作用域、不可重複宣告、不可重新指定(注意:物件屬性仍可變),預設首選。
// TDZ 範例:在宣告前讀取會拋錯
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;
// var 的提升陷阱
console.log(b); // undefined(非錯誤)
var b = 2;
// const 不可重新指定
const config = { retries: 3 };
config.retries = 5; // ✅ 允許改屬性
// config = {} // ❌ 不可重新指定
2. 作用域觀念(Block / Function / Module)
區塊作用域:
{ ... } 內用 let/const 只在該區塊有效。
函數作用域:
var 與函數內部變數只在函數內可見。
模組作用域:
ES Modules(.mjs 或 type: "module")檔案本身是一個作用域,天然嚴格模式、頂層的 this 為 undefined。
{
const x = 1;
}
console.log(typeof x); // 'undefined'
3. 何時用 let、何時用 const
預設用 const;
只有在「需要重新指定」的情境改用 let(例如計數器、累加器);
基本上不再使用 var。
函數宣告:宣告式、表達式、箭頭函數
1. 三種寫法
// 1) 函數宣告(有提升)——適合工具類純函數
function sum(a, b) {
return a + b;
}
// 2) 函數表達式(無提升)
const multiply = function (a, b) {
return a * b;
};
// 3) 箭頭函數(詞法 this、無 prototype)
const divide = (a, b) => a / b;
2. 什麼時候選哪個?
宣告式:純函數工具,需提前使用、或希望在檔案上半部快速索引。
箭頭函數:回呼(callback)、內聯小函數、需要詞法綁定 this 的場景(例如在 class 方法內用私有欄位搭配箭頭函數作事件處理)。
表達式:需要閉包、或想讓函數只在某區塊存在。
3. this 與箭頭函數
箭頭函數的 this 來自外層詞法作用域;不適合當建構函數。
Class 成員方法若要當事件處理器,常見寫法:
class Counter {
#count = 0;
constructor(btn) {
this.btn = btn;
this.handleClick = () => {
this.#count++;
this.render();
};
this.btn.addEventListener('click', this.handleClick);
}
render() { /* ... */ }
}
需要用分號 ; 嗎?
建議:統一使用分號。瀏覽器有自動插入分號(ASI),但某些邊界會出問題,例如行首是 [、(、/、+、- 或以 return 換行時:
// 風險案例:
function f() {
return
{
ok: true
}
}
// 實際上回傳的是 undefined,而不是物件
從下圖可觀察,會有這種問題發生
test1函數傳回undefined , test2函數傳回物件
此案例比較直白的寫法如下
若團隊偏好「無分號」,請務必用 ESLint + Prettier 固定化,並遵循特定換行規則,避免 ASI 陷阱。整體來說,加分號最省心。
註釋:怎麼寫才加分
單行與多行
// 單行:解釋「為什麼」或標註 TODO/FIXME
/* 多行:
描述設計考量或複雜邏輯的脈絡 */
JSDoc(搭配型別提示更穩)
/**
* 計算折扣後金額
* @param {number} price 原價(>=0)
* @param {number} rate 折扣率(0~1)
* @returns {number} 折扣後金額
*/
function discounted(price, rate) {
if (price < 0 || rate < 0 || rate > 1) throw new RangeError('invalid input');
return Math.round(price * (1 - rate));
}
優點:
讓編輯器提供更準確的自動完成與跳轉
不用引入 TypeScript 也能獲得不少型別安全(可作為過渡)
實作範例:一步一步做出「表單驗證+清單篩選」的小模組
場景:你要做一個簡單的商品清單頁,具備兩件事
a. 依關鍵字篩選商品
b. 表單送出前檢查資料(例:金額為正、關鍵字長度限制)
1. 規劃(先定資料結構)
// 單一商品
/**
* @typedef {Object} Item
* @property {string} id
* @property {string} name
* @property {number} price
*/
// 篩選條件
/**
* @typedef {Object} Filter
* @property {string} keyword
* @property {number} [maxPrice]
*/
2. utilities.js(純函數、可測)
/**
* @param {string} s
* @returns {string}
*/
export function normalizeKeyword(s) {
return (s ?? '').trim().toLowerCase();
}
/**
* @param {Item[]} items
* @param {Filter} f
* @returns {Item[]}
*/
export function filterItems(items, f) {
const kw = normalizeKeyword(f.keyword);
return items.filter(it => {
const nameHit = it.name.toLowerCase().includes(kw);
const priceHit = typeof f.maxPrice === 'number' ? it.price <= f.maxPrice : true;
return nameHit && priceHit;
});
}
/**
* 檢查表單資料是否合法
* @param {{ keyword: string, maxPrice?: number }} form
* @returns {{ ok: true } | { ok: false, message: string }}
*/
export function validateForm(form) {
const kw = normalizeKeyword(form.keyword);
if (kw.length === 0) return { ok: false, message: '請輸入關鍵字' };
if (kw.length > 30) return { ok: false, message: '關鍵字過長(上限 30)' };
if (form.maxPrice != null && (isNaN(form.maxPrice) || form.maxPrice <= 0)) {
return { ok: false, message: '金額需為正數' };
}
return { ok: true };
}
3. index.js(組裝、事件處理、與 DOM 互動)
import { filterItems, validateForm } from './utilities.js';
const items = [
{ id: 'p01', name: 'Macchiato Coffee', price: 120 },
{ id: 'p02', name: 'Matcha Latte', price: 150 },
{ id: 'p03', name: 'Cocoa', price: 100 },
];
const formEl = document.querySelector('#search-form');
const listEl = document.querySelector('#item-list');
const msgEl = document.querySelector('#message');
function render(list) {
listEl.innerHTML = list.map(it => `<li>${it.name} - $${it.price}</li>`).join('');
}
formEl.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(formEl);
const keyword = formData.get('keyword');
const maxPriceRaw = formData.get('maxPrice');
const maxPrice = maxPriceRaw ? Number(maxPriceRaw) : undefined;
const result = validateForm({ keyword, maxPrice });
if (!result.ok) {
msgEl.textContent = result.message;
return;
}
msgEl.textContent = '';
const filtered = filterItems(items, { keyword, maxPrice });
render(filtered);
});
// 初次渲染
render(items);
4. HTML(簡版)
<form id="search-form">
<input name="keyword" placeholder="輸入關鍵字">
<input name="maxPrice" type="number" min="1" placeholder="最高價格">
<button type="submit">搜尋</button>
</form>
<p id="message" style="color:crimson;"></p>
<ul id="item-list"></ul>
5. 常見錯誤(對應此範例)
忘了 e.preventDefault() 導致頁面重新載入。
將 FormData.get() 的結果直接當數字用(字串需轉型)。
innerHTML 拼接未處理使用者輸入(如需安全,請用 textContent 或模板轉義)。
normalizeKeyword 沒處理 null/undefined。
事件綁定在錯誤的元素上(form vs button)。
更多語言眉角與實務建議
1. 嚴格模式
ES Module 自帶嚴格模式;非模組環境建議在檔案頂部加 "use strict"; 以避免隱式全域變數等問題。
2. 比較運算子:一律用 === / !==
避免 == 的隱式轉型陷阱(0 == false 為真、'' == 0 為真);
若需容忍 null 與 undefined 共同判斷,可用 value == null(僅此例外)。
if (input == null) { /* 同時攔截 null 與 undefined */ }
3. 真值/偽值(Truthy/Falsy)
偽值:false, 0, -0, 0n, "", null, undefined, NaN。
檢查字串是否有內容,應用 .trim() 後再判斷。
4. 浮點數與金額
避免直接用浮點數計價,改存整數的最小貨幣單位(例如分)。或用 Intl.NumberFormat 與 BigInt/decimal(新提案)策略搭配。
const priceCents = 1999; // $19.99
const totalCents = priceCents * 3;
const display = (totalCents / 100).toFixed(2);
5. this 綁定與回呼
DOM 事件處理器裡使用 class 成員時,確保 this 指向正確(箭頭函數或 bind)。
不要把需要 this 的方法直接當回呼丟出(容易丟失綁定)。
6. 非同步:async/await 的錯誤處理
每個 await 旁都應有錯誤處理策略:try/catch、或封裝成回傳 { ok, data|error } 的函數。
多個獨立請求併發,用 Promise.all;彼此相依則串接。
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('HTTP ' + res.status);
return { ok: true, data: await res.json() };
} catch (err) {
return { ok: false, error: err };
}
}
7. JSON 與資料邊界
JSON.parse 需 try/catch;
API 回傳可能是空字串、204 No Content,別直接 parse。
8. 日期與時區
使用 Date 時注意時區轉換;顯示建議 Intl.DateTimeFormat 或引入專門庫(若可)。
避免自己手刻時差計算。
9. 物件與陣列的不可變更新
使用展開運算子(spread)或 Array.prototype.map/filter,避免直接改原陣列(除非刻意)。
const next = { ...state, count: state.count + 1 };
const newList = list.map(x => x.id === id ? { ...x, done: true } : x);
10. 模組與匯出
預設用 ES Modules:export / import。
一個檔案最好僅有一個預設匯出(default),其餘用具名匯出,利於 IDE 跳轉與 Tree Shaking。
詳細操作步驟(把上述原則落地)
以下以「做一個可重用的數值工具庫」為例:包含安全加法、四捨五入到指定位數、平均值。
步驟 1:建立專案結構
/src
number.js
average.js
index.js
/test
number.test.js
average.test.js
步驟 2:定義需求與邊界
safeAdd(a, b):若任一不是有限數,拋錯。
roundTo(num, digits):digits 為整數且介於 0~10。
average(nums):空陣列拋錯;若含非數字,拋錯。
步驟 3:實作(src/number.js)
/**
* @param {unknown} x
* @returns {boolean}
*/
function isFiniteNumber(x) {
return typeof x === 'number' && Number.isFinite(x);
}
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
export function safeAdd(a, b) {
if (!isFiniteNumber(a) || !isFiniteNumber(b)) {
throw new TypeError('safeAdd expects finite numbers');
}
return a + b;
}
/**
* @param {number} num
* @param {number} digits
*/
export function roundTo(num, digits = 0) {
if (!Number.isInteger(digits) || digits < 0 || digits > 10) {
throw new RangeError('digits must be 0..10');
}
if (!isFiniteNumber(num)) throw new TypeError('num must be finite');
const p = 10 ** digits;
return Math.round(num * p) / p;
}
步驟 4:實作(src/average.js)
import { safeAdd } from './number.js';
/**
* @param {number[]} nums
* @returns {number}
*/
export function average(nums) {
if (!Array.isArray(nums) || nums.length === 0) {
throw new TypeError('nums must be non-empty array');
}
let sum = 0;
for (const n of nums) {
if (typeof n !== 'number' || !Number.isFinite(n)) {
throw new TypeError('all elements must be finite numbers');
}
sum = safeAdd(sum, n);
}
return sum / nums.length;
}
步驟 5:集中匯出(src/index.js)
export * from './number.js';
export * from './average.js';
步驟 6:簡單使用(示例)
import { roundTo, average } from './src/index.js';
console.log(roundTo(1.005, 2)); // 1.01(正確處理小數)
console.log(average([1, 2, 3, 4])); // 2.5
步驟 7:加上基本測試要點(概念示例)
safeAdd(1, NaN) 應拋 TypeError
roundTo(1.2345, 2) 應等於 1.23
average([]) 應拋錯;average([1, Infinity]) 應拋錯
步驟 8:工具化與一致性
安裝並配置 ESLint + Prettier(規則如:必加分號、單引號、無未用變數、eqeqeq)。
在 Git pre-commit 加上 lint-staged:提交前自動格式化、靜態檢查。
常見錯誤與雷點
1. 把 var 當 let 用
風險:提升與函數作用域導致值被覆蓋、for 迴圈閉包錯亂。
作法:新碼一律 const / let,for 迴圈用 let i = 0。
2. 忘了處理 undefined/null
風險:TypeError: Cannot read properties of undefined。
作法:可用可選鏈 ?.、空值合併 ??,但別濫用;邏輯邊界要先定義清楚。
3. 濫用全域狀態
風險:測試困難、難以追蹤副作用。
作法:封裝在模組或 class,明確的輸入/輸出。
4. this 綁定錯誤
風險:方法裡的 this 是 undefined 或指到 window。
作法:箭頭函數/bind;或在 constructor 進行綁定。
5. await 沒有錯誤處理
風險:一個錯誤讓整串流程中斷。
作法:try/catch 或包裝成 { ok, data|error }。
6. 不必要的深拷貝
風險:效能殺手。
作法:先想資料流,再決定用淺拷貝(spread)或結構分享(immutable patterns)。
7. JSON 處理掉坑
風險:非 JSON 字串導致 JSON.parse 拋錯。
作法:先判斷內容、或 try/catch 包住;後端回應設計要一致。
8. 浮點數誤差
風險:金額計算不準。
作法:存最小單位(整數),顯示才除以 10^n;或集中用工具函數。
9. 正則過度貪婪
風險:意外吃掉過多內容。
作法:學會使用懶惰量詞 *?、具名群組、與邊界錨點。
10. DOM XSS 風險
風險:把使用者輸入直接塞進 innerHTML。
作法:用 textContent 或可信模板引擎;必要時做嚴格轉義。
11. 魔法數字/字串
風險:看不懂,也難改。
作法:提升為常數或列舉(enum-like object)。
12. 過度抽象
風險:程式變繞口令。
作法:先求直白,確定重複再抽象;抽象後要有測試保護。
範例片段大全
1. 防呆型資料讀取
/**
* @template T
* @param {unknown} x
* @param {(x: unknown) => x is T} guard
* @param {T} fallback
* @returns {T}
*/
export function getOr(x, guard, fallback) {
return guard(x) ? x : fallback;
}
export const isString = (x) => typeof x === 'string';
const name = getOr(data.name, isString, 'Guest');
2. 小而美的錯誤類型
export class HttpError extends Error {
constructor(status, message) {
super(message);
this.name = 'HttpError';
this.status = status;
}
}
3. 併發請求與超時
export function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, rej) => setTimeout(() => rej(new Error('Timeout')), ms)),
]);
}
const [user, posts] = await Promise.all([
withTimeout(fetchJSON('/user'), 5000),
withTimeout(fetchJSON('/posts'), 5000),
]);
結語:讓程式替你省腦,而不是偷你的腦
寫 JavaScript 的路上,最花時間的不是語法,而是「選擇」:用什麼宣告、如何命名、哪裡要拆函數、要不要分號、怎麼註釋。你不必每次都從零想,只要把一套穩定原則變成肌肉記憶。久了,你的專案會更安靜——少跳錯、多通過、重構也不心虛。
