
內容摘要
console.log 印出的是物件的即時參考,所以事後展開看到的狀態會變。table、group、time、assert 才能把雜訊變成有結構的證據。
多數人的除錯從 console.log 開始,也停在那裡。這很可惜,因為主控台有十幾個方法,而它們之間的差別,就是「讀一整面文字牆」與「讀一個答案」的差別。這篇講值得記住的方法、會讓日誌看起來錯誤的「參考 vs 副本」行為,以及當非同步呼叫默默失敗時,怎麼讀堆疊追蹤。
文中所有內容都能在 UrsaKit 的 JavaScript 網上執行器 上跑,它會分級輸出主控台內容、支援群組,並顯示執行耗時。
01console.log 只是第一個工具
console.log 把你給它的東西依序印出來,沒有任何結構。對一個數字來說沒問題,對四十筆記錄來說毫無用處。其餘 API 存在的目的就是加上結構:
| 方法 | 用在 |
|---|---|
console.log | 任何值;可用 %s / %d / %o 格式化 |
console.table | 物件陣列,渲染成表格 |
console.dir | 檢視單一物件的屬性,而不是它的 toString |
console.group / console.groupEnd | 把相關輸出巢狀起來 |
console.time / console.timeEnd | 量測兩點之間的時間 |
console.count | 計算某個分支被執行幾次 |
console.assert | 只在假設失敗時輸出 |
console.trace | 在當下位置印出堆疊追蹤 |
02物件是以參考記錄的
這是最浪費時間的行為,因為主控台沒有說謊——它顯示的是即時物件。
const user = { name: 'Ada', plan: 'free' }
console.log(user) // 之後展開,可能顯示 { name: 'Grace', plan: 'pro' }
console.log(user.name) // 'Ada' —— 基本型別是以值捕捉的
user.name = 'Grace'
user.plan = 'pro'
在可互動的主控台裡,物件檢視是在你展開時才建立,所以它反映的是物件的最終狀態,不是你呼叫方法的那一刻。想捕捉當下的值,有三種做法:
console.log(user.name) // 直接記錄你在乎的基本型別
console.log(JSON.stringify(user)) // 字串快照,在任何主控台都一樣
console.log(structuredClone(user)) // 真正的深拷貝,保留型別
記錄你要比較的狀態,而不是整個物件。
console.log('plan=' + user.plan + ' items=' + items.length)可以活過序列化、在伺服器日誌與瀏覽器裡讀起來一樣,也不會被偷偷改掉。整包物件記錄留給探索階段用就好。
03分組與計時
相關輸出巢狀起來就變得可讀,時間用量的方式比較就變得可比較。
console.group('checkout')
console.time('validate')
console.count('line') // line: 1
console.count('line') // line: 2
for (let i = 0; i < 1e6; i++) {
/* work */
}
console.timeEnd('validate') // validate: 12.4ms
console.groupEnd()
幾個實務上重要的細節:
console.time與console.timeEnd必須用同一個標籤,而且每個標籤要先結束才能再開始。console.count每個標籤各自從零起算並印出累計次數,是證明某個 render 函數跑了兩次最快的方法。console.groupCollapsed與console.group相同,但預設收起,長篇輸出會比較好讀。
這樣量到的時間包含兩個呼叫之間的所有事,所以它適合做相對比較——改動前後——而不是當成效能基準。執行器本身也會回報整段程式碼的總執行時間。
04斷言與計數
console.assert 是 console.log 的反面:假設成立時它保持安靜。
function applyDiscount(price, percent) {
console.assert(
percent >= 0 && percent <= 100,
'discount out of range',
percent
)
return price * (1 - percent / 100)
}
applyDiscount(100, 20) // 沒有輸出
applyDiscount(100, 150) // Assertion failed: discount out of range 150
放在循環裡,斷言就是篩網:只有違反規則的記錄才會有輸出,不必在一堆正常資料中滾動尋找。
05讀堆疊追蹤
當你需要知道是哪個呼叫端進到某個函數,就印出追蹤,而不是在每個候選位置都加一行 log:
function parse(input) {
console.trace('parse called')
return JSON.parse(input)
}
parse('{"a":1}')
// parse called
// at parse (<anonymous>:2:11)
// at <anonymous>:4:1
對於拋出的錯誤,堆疊就是從失敗點回到呼叫端的地圖。由上往下讀:第一行是錯誤被拋出的位置,下面是它的呼叫者。屬於你的程式碼的那一層——不是函數庫、也不是執行環境——通常就是要修的地方。
try {
JSON.parse('{ "a": 1, }')
} catch (error) {
console.error(error.name) // SyntaxError
console.error(error.message) // Expected double-quoted property name in JSON at position 9
console.error(error.stack) // 一層一層指出發生位置
}
06永遠不會印出來的非同步錯誤
在非同步函數裡拋出的錯誤會變成被拒絕的 promise。如果沒有人處理,這個失敗可能發生,卻從未經過你盯著的那一行。
async function load() {
throw new Error('network down')
}
load() // 未處理的 rejection:沒有任何地方接住它
兩個習慣能讓這類錯誤現形:
- 在 try/catch 裡 await,讓失敗落在你能控制的那一行。
- 在邊界處理 rejection:對射後不理的呼叫加上
.catch(),或在你擁有的頁面上監聽執行環境的 rejection 事件。
async function load() {
throw new Error('network down')
}
try {
await load()
} catch (error) {
console.error('load failed:', error.message) // load failed: network down
}
不要把囉嗦的 console 輸出帶上正式環境。
正式環境的除錯日誌會把內部狀態攤給任何打開主控台的人,而且在熱路徑上記錄大型物件真的會花時間。上線前把它們加上開關或移除。
07在沙箱裡練習
- 貼上程式碼並執行 — JavaScript 網上執行器 會先解析程式碼,語法錯誤會連行號一起回報,讓你還沒執行就先抓到打錯字。
- 讀分級輸出 — log、warn、error 會分別標示,主控台的分組方式也和瀏覽器一致。
- 看執行計時 — 回報的耗時就是這段程式碼的真實成本,足以比較兩種寫法。
- 試試失敗的那條分支 — 用會破壞假設的輸入再跑一次,因為你從未觸發的那個斷言,正是本來能抓到 bug 的那個。
主控台是量測工具,不是記事本。用對方法,才能把一整頁輸出變成一回答問題的那一行。
在沙箱主控台裡執行程式碼,內置語法檢查、執行耗時與分級輸出。
JavaScript 網上執行器常見問題
為什麼 console.log 印出的物件值是錯的?
主控台存的是物件的即時參考,不是副本。如果物件在 log 之後被修改,展開時看到的就是當下狀態。想捕捉當下的值,就記錄基本型別、JSON 字串或 structuredClone 的副本。
console.table 到底做了什麼?
它把物件陣列渲染成表格,用物件的鍵當欄位。要讀一筆筆記錄時,這是最快的方式,它還接受第二個參數來限制要顯示哪些欄位。
什麼時候該用 console.assert?
當你只想在假設被違反時才有輸出時。console.assert(條件, 訊息) 在條件為真時什麼都不印,正常執行時主控台就很乾淨。
Node.js 與瀏覽器的 console 一樣嗎?
大致相同。table、group、time、count、assert 兩邊都有,但瀏覽器渲染成可互動的物件,Node 印出 inspect 結果,而且 Node 多了 unhandledRejection 這類行程層級事件。
console 呼叫會拖慢正式環境嗎?
記錄大型物件確實有成本,而且把囉嗦的日誌留在正式環境,等於把內部狀態攤給任何打開主控台的人看。上線前移除,或加上開關。
不想丟出錯誤,要怎麼看到堆疊追蹤?
呼叫 console.trace()。它會在呼叫點印出堆疊追蹤,這正是找出五個呼叫者裡誰該負責的方法。
相關文章
- JSON 格式化、驗證與壓縮
把你檢視的物件變成可讀、可比的格式。
- 正則表達式的群組、前後查找與回溯
在主控台裡對著真實字串測試你正在除錯的樣式。
相關工具
- JavaScript 網上執行器
沙箱主控台、執行耗時與分級輸出。
- JSON 格式化與驗證
把你 dump 出來的物件美化。
- HTML 網上工具
渲染腳本產生的標記。
- 正則表達式測試器
檢查程式碼依賴的那條樣式。