不只 console.log#

Debug JavaScript 最直覺的方式是 console.log,但 Console API 還有非常多其他 method,每個都有適合的使用場景。多數時候 console.log 確實能解決問題,可是用對 method 可以省下不少時間,本章挑幾個原作者最常用的成員來介紹。

console.assert#

console.assert 的作用接近 console.log,最大的差別是:只有第一個參數是 falsy 時才會輸出。

JavaScript 的 falsy 值清單:

[false, null, undefined, 0, -0, 0n, NaN, ""];

舉例:要檢查 user.name 是否為空,常見寫法是:

const user = {
  name: "",
};
if (!user.name) {
  console.log("哪邊出錯了 QQ", user);
}

console.assert 可以省掉 if

console.assert(user.name, "哪邊出錯了 QQ", user);

語意上更清晰:「條件不符就拋警告」。

唯一要小心的是條件方向:第一個參數是 falsy 時才會作用,條件寫反了會完全沒反應,反而讓你以為一切正常。

console.count#

console.count(label) 印出某個 label 被執行了幾次,預設 label 是 default

最簡單的用法:

function count(arg) {
  console.count(arg);
}
count("foo");
count("bar");
count("bar");

也能用來統計多種狀況的出現次數:

for (let i = 0; i < 5; i++) {
  const int = Math.ceil(Math.random() * 100);
  if (int < 20) console.count("太高了");
  if (int > 20) console.count("太低了");
}

每次呼叫對應 label 的計數會自動 +1,比自己維護一個 counter 變數方便很多。

console.countReset#

console.count 是一對,把指定 label 的計數歸零。常見用法是計算「單次行為」會觸發幾次,例如 React 中按下按鈕後總共觸發了幾次 render。

同步的 setState 會被 React batch 在一起,但非同步狀態下每個 setState 都會觸發 render:

function App() {
  const [count, setCount] = React.useState(0);
  const [count2, setCount2] = React.useState(0);
  const [asyncCount, setAsyncCount] = React.useState(0);
  const [asyncCount2, setAsyncCount2] = React.useState(0);
  const onClick = () => {
    console.countReset("render");
    setCount(count + 1);
    setCount2(count2 + 1);
    Promise.resolve().then(() => {
      setAsyncCount(asyncCount + 1);
      setAsyncCount2(asyncCount2 + 1);
    });
  };
  console.count("render");
  return (
    <div onClick={onClick}>
      <h1>Hello, please click me.</h1>
      <h2>{count}</h2>
    </div>
  );
}

按下按鈕後可以看到 ‘render’ 計數從 1 跳到 3,代表這次互動觸發了 3 次 render。

console.group#

為了在大量 log 中一眼看到自己的訊息,是不是寫過這種程式碼:

console.log("---------");
console.log(object);
console.log("---end---");

console.group 是更乾淨的解法,可以給 log 加上可摺疊的標籤,也能多層巢狀:

console.group("Start debugging");
console.log("de-");
console.group("Nested");
console.warn("deeper message");
console.groupEnd();
console.log("bug");
console.groupEnd();

每呼叫一次 console.group 就要對應一次 console.groupEnd。另外還有 console.groupCollapsed,差別只在預設是收合狀態,需要手動展開。

console.table#

如果只能在這些 Console API 裡推一個,原作者選 console.table

要印出陣列裡的物件時,console.log 必須一層一層展開,非常不方便。試試以下資料:

const rows = [
  {
    name: "Frozen yoghurt",
    calories: 159,
    fat: 6,
    carbs: 24,
    protein: 4,
  },
  {
    name: "Ice cream sandwich",
    calories: 237,
    fat: 9,
    carbs: 37,
    protein: 4.3,
  },
  {
    name: "Eclair",
    calories: 262,
    fat: 16,
    carbs: 24,
    protein: 6,
  },
];

console.log(rows) 會印出收合的物件陣列;按住 Option / Alt 點開會把所有屬性(包括 __proto__)展開,畫面非常雜。

換成 console.table(rows),會用表格呈現所有物件,欄位寬度可以拖拉調整,還能用第二個參數限制顯示欄位:

console.table(rows, ["name", "fat"]);

console.table 還解決另一個 console.log 的陷阱:物件被印出後,如果之後還會被修改,「展開」物件時看到的是當下值,而不是 log 當時的值。例子:

const animal = {
  name: "mimi",
  type: "cat",
  other: {
    emoji: "?",
    sound: "meow",
  },
};
console.log(animal);
animal.name = "ami";

之後手動展開 log 出的物件,會看到 name: 'ami',跟印出當下不一致。console.log 的右上角其實有個小圖示,hover 上去會提示 Value below was evaluated just now.,明確說了「展開時的值」≠「當時的值」。

console.table 會在執行當下就把資料 snapshot 成表格,自然避開這個問題。

物件層級很深的時候,JSON.stringify(obj, null, 2) 也是不錯的選擇,直接把整個物件以縮排 JSON 字串呈現。

console.time#

要量某段程式碼或使用者行為的執行時間,常看到這種寫法:

const t0 = performance.now();
alert("Hello World!");
const t1 = performance.now();
alert("Another Hello World!");
console.log(`Spent: ${t1 - t0} ms`);
const t2 = performance.now();
console.log(`Spent: ${t2 - t0} ms`);

繁瑣又容易寫錯。改用 console.time

console.time("Spent");
alert("Hello World!");
console.timeLog("Spent");
alert("Another Hello World!");
console.timeEnd("Spent");

用法:

  • console.time(label):啟動一個計時器
  • console.timeLog(label):印出目前已經過了多久(可以呼叫無限多次)
  • console.timeEnd(label):停止計時器並印出總時間

如果 timeLogtimeEnd 用了沒啟動過的 label,DevTools 會印 warning。

console.trace#

當問題涉及多個 function 或第三方套件、同一個 function 在多處被呼叫時,console.log 只能看到「印出當下的位置」,不夠用。console.trace 會直接印出調用堆疊(Call Stack)並展開:

function a() {
  console.trace();
}
function b() {
  a();
}
function c() {
  b();
}
b();
c();

實戰例子:使用某影片播放器套件時,影片偶爾會莫名暫停。要追是哪段程式碼呼叫了 pause,可以這樣:

video.addEventListener("pause", console.trace);

馬上就能看出是套件內哪個 function 觸發暫停,比把 console.log 撒到處都是有效率很多。

銜接#

console.log 大法之外,這些 Console API 是日常 debug 的好搭檔。下一章進入 Console Utilities API:那些不能用在 production 程式碼、但在 Console panel 內超強的 utility function($_$0$x$$debugmonitor 等)。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10240826