UrsaKit 封面圖:JavaScript 主控台方法指南

多數人的除錯從 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在沙箱裡練習

  1. 貼上程式碼並執行 — JavaScript 網上執行器 會先解析程式碼,語法錯誤會連行號一起回報,讓你還沒執行就先抓到打錯字。
  2. 讀分級輸出 — log、warn、error 會分別標示,主控台的分組方式也和瀏覽器一致。
  3. 看執行計時 — 回報的耗時就是這段程式碼的真實成本,足以比較兩種寫法。
  4. 試試失敗的那條分支 — 用會破壞假設的輸入再跑一次,因為你從未觸發的那個斷言,正是本來能抓到 bug 的那個。

主控台是量測工具,不是記事本。用對方法,才能把一整頁輸出變成一回答問題的那一行。

常見問題

為什麼 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()。它會在呼叫點印出堆疊追蹤,這正是找出五個呼叫者裡誰該負責的方法。

返回文章列表

相關文章

相關工具