跳至內容
Toova
所有工具

如何在 JavaScript 中格式化 JSON — 5 種方法

Toova

JSON 無所不在:API 回應、設定檔、資料庫匯出、日誌管線。但原始 JSON 輸出通常是壓縮的一團 — 沒有換行、沒有縮排,鍵以序列化器吐出的任意順序排列。這讓除錯變得痛苦,協作也比預期困難。

JavaScript 提供多種格式化 JSON 的方式,從內建的 JSON.stringify 到適用於大到無法載入記憶體的檔案的串流函式庫。本指南介紹五個實用方法,涵蓋最常見的情況以及讓開發者措手不及的邊界情況。

方法 1:JSON.stringify 搭配縮排

最簡單的方法早已內建於語言中。JSON.stringify 接受三個參數:要序列化的值、取代器(方法 2 將進一步說明),以及控制縮排的 space 參數。

const data = {
  name: "Alice",
  role: "engineer",
  skills: ["JavaScript", "TypeScript", "Node.js"],
  active: true,
};

// 2 個空格縮排(常見預設)
console.log(JSON.stringify(data, null, 2));

// 4 個空格縮排
console.log(JSON.stringify(data, null, 4));

// Tab 縮排
console.log(JSON.stringify(data, null, "\t"));

使用 null, 2,輸出看起來如下:

{
  "name": "Alice",
  "role": "engineer",
  "skills": [
    "JavaScript",
    "TypeScript",
    "Node.js"
  ],
  "active": true
}

兩個空格是 JavaScript 專案中最常見的慣例 — 它與 ESLint、Prettier 及多數樣式指引的預設相符。四個空格在 Python 相關工具中常見。當 space 被省略或設為 0 時,輸出為精簡的單行字串 — 適合在傳輸時使用。

你也可以使用 JSON 格式化工具在瀏覽器中立即格式化 JSON — 貼上原始 JSON、取得美化輸出,並複製結果,無需撰寫任何程式碼。

方法 2:自訂取代器函數

JSON.stringify 的第二個參數 — 取代器 — 是個函數或陣列,會在序列化前過濾與轉換值。這是讓你能真正控制輸出內容的地方。

陣列形式是鍵白名單的最簡單方法:

const user = {
  id: 42,
  name: "Bob",
  password: "s3cr3t",
  email: "bob@example.com",
  createdAt: new Date(),
};

// 陣列取代器:僅包含列出的鍵
const safeJson = JSON.stringify(user, ["id", "name", "email"], 2);
console.log(safeJson);
// {
//   "id": 42,
//   "name": "Bob",
//   "email": "bob@example.com"
// }

函數形式提供逐值控制。回傳值以納入、回傳 undefined 以丟棄,或回傳已轉換的值:

function replacer(key, value) {
  // 丟棄以底線開頭的鍵(私有慣例)
  if (key.startsWith("_")) return undefined;

  // 遮罩敏感欄位
  if (key === "password" || key === "token") return "[REDACTED]";

  // 將 Date 物件明確轉為 ISO 字串
  if (value instanceof Date) return value.toISOString();

  return value;
}

const payload = {
  id: 1,
  name: "Carol",
  password: "hunter2",
  _internalFlag: true,
  lastLogin: new Date("2026-05-01"),
};

console.log(JSON.stringify(payload, replacer, 2));
// {
//   "id": 1,
//   "name": "Carol",
//   "password": "[REDACTED]",
//   "lastLogin": "2026-05-01T00:00:00.000Z"
// }

取代器會對每個巢狀物件與陣列遞迴執行。這個模式對日誌管線很有用,在寫入磁碟或傳送到可觀測性服務前可去除憑證。

方法 3:以排序鍵美化列印

JavaScript 物件並不保證鍵順序(雖然 V8 與多數引擎會為字串鍵保留插入順序)。當你需要確定性輸出 — 用於比較、快取或標準表示 — 以字母順序排序鍵是正確的做法。

function sortedStringify(value, indent = 2) {
  return JSON.stringify(value, sortReplacer, indent);
}

function sortReplacer(key, value) {
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
    return Object.keys(value)
      .sort()
      .reduce((sorted, k) => {
        sorted[k] = value[k];
        return sorted;
      }, {});
  }
  return value;
}

const config = {
  version: "1.0",
  author: "Dave",
  dependencies: { typescript: "^5.4", eslint: "^9.0", astro: "^5.0" },
  name: "my-project",
};

console.log(sortedStringify(config));
// {
//   "author": "Dave",
//   "dependencies": { "astro": "^5.0", "eslint": "^9.0", "typescript": "^5.4" },
//   "name": "my-project",
//   "version": "1.0"
// }

排序鍵意味著 git diff 只會顯示實際變更的行,而不是不同序列化器的任意重新排序。這對於提交到版本控制的 package.json 與類似設定檔特別有用。

若需將 JSON 轉換為其他格式,JSON 轉 YAML 工具JSON 轉 CSV 工具也會處理輸出中的鍵順序。

方法 4:從字串格式化(解析 + 字串化)

現實世界的 JSON 通常以字串形式抵達 — 來自 fetch 回應、檔案讀取、剪貼簿貼上,或資料庫 TEXT 欄位。你需要先解析,再重新格式化。關鍵在於妥善的錯誤處理:無效 JSON 會擲出例外,而你希望優雅地捕捉它。

function formatJsonString(rawString, indent = 2) {
  try {
    const parsed = JSON.parse(rawString);
    return { ok: true, result: JSON.stringify(parsed, null, indent) };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

const raw = '{"name":"Eve","scores":[100,95,88],"active":true}';
const { ok, result, error } = formatJsonString(raw);

if (ok) {
  console.log(result);
  // {
  //   "name": "Eve",
  //   "scores": [100, 95, 88],
  //   "active": true
  // }
} else {
  console.error("解析失敗:", error);
}

// 無效輸入
const bad = '{"name": "Eve", "broken":}';
const r2 = formatJsonString(bad);
// { ok: false, error: "Unexpected token '}'" }

將解析錯誤包裝在結構化的回傳物件中,讓此函數可安全地用於 UI 元件與建構腳本,而無需在每個呼叫處包圍 try/catch。JSON.stringify 的 MDN 文件涵蓋完整參數規格,而 RFC 8259 在協定層級定義什麼是有效的 JSON。

方法 5:為大型檔案進行串流

方法 1 至 4 都會在格式化前將整個 JSON 結構載入記憶體。對於數百 MB 或數 GB 範圍的檔案,這會阻塞 Node.js 事件迴圈,可能讓行程完全崩潰。

串流方式以區塊讀取檔案,並以漸進方式寫入格式化輸出。對於 NDJSON(每行一個 JSON 物件,在日誌檔與資料庫匯出中常見),基於 readline 的方法無需額外相依套件即可運作:

import { createReadStream, createWriteStream } from "node:fs";
import { createInterface } from "node:readline";

async function formatNdjsonFile(inputPath, outputPath) {
  const rl = createInterface({
    input: createReadStream(inputPath),
    crlfDelay: Infinity,
  });

  const output = createWriteStream(outputPath);

  for await (const line of rl) {
    if (!line.trim()) continue;
    try {
      const obj = JSON.parse(line);
      output.write(JSON.stringify(obj, null, 2) + "\n---\n");
    } catch (e) {
      output.write("[無效 JSON 行: " + e.message + "]\n---\n");
    }
  }

  output.end();
}

NDJSON 是最簡單的串流格式:每行都是一個有效且完整的 JSON 物件。許多匯出工具支援它,正是因為它易於串流。如果你可以控制大型資料匯出的格式,請優先採用 NDJSON 而非單一龐大的 JSON 陣列。

需要注意的邊界情況

這些是標準 JSON 格式化會默默失敗或意外擲出例外的情境。

循環引用

如果物件直接或間接引用自己,JSON.stringify 會擲出 TypeError。透過使用 WeakSet 追蹤造訪過物件的取代器來修正:

function safeStringify(obj, indent = 2) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) return "[Circular]";
      seen.add(value);
    }
    return value;
  }, indent);
}

const a = { name: "circular" };
a.self = a;
console.log(safeStringify(a));
// { "name": "circular", "self": "[Circular]" }

WeakSet 持有引用而不阻止垃圾回收,可避免長期運行的行程中出現記憶體洩漏。

BigInt 值

JSON.stringifyBigInt 值會擲出 TypeError,因為 JSON 規格沒有 64 位元整數型別。在取代器中轉為字串:

const data = { id: 9007199254740993n, value: 42 };

JSON.stringify(data, (key, value) =>
  typeof value === "bigint" ? value.toString() : value
, 2);
// { "id": "9007199254740993", "value": 42 }

Map 與 Set 值

Map 會序列化為空物件,Set 會序列化為空陣列 — 並非其內容。請在取代器中明確轉換它們:

const data = {
  tags: new Set(["json", "javascript"]),
  meta: new Map([["source", "api"]]),
};

JSON.stringify(data, (key, value) => {
  if (value instanceof Set) return [...value];
  if (value instanceof Map) return Object.fromEntries(value);
  return value;
}, 2);
// { "tags": ["json", "javascript"], "meta": { "source": "api" } }

undefined 值

undefined 值的物件屬性會被默默丟棄。陣列中具 undefined 的位置會變成 null。若需保留所有鍵,使用取代器將 undefined 轉為 null:

const obj = { a: 1, b: undefined, c: null };
JSON.stringify(obj, null, 2);
// { "a": 1, "c": null }  — "b" 被默默丟棄

// 修正:
JSON.stringify(obj, (key, value) =>
  value === undefined ? null : value
, 2);
// { "a": 1, "b": null, "c": null }

在瀏覽器中立即格式化 JSON

如果你現在就需要格式化 JSON 區塊而不想撰寫程式碼,Toova JSON 格式化工具一鍵就能完成 — 貼上原始 JSON,取得 2 或 4 個空格縮排的美化輸出,並複製結果。無需註冊、無需上傳檔案,所有處理都在你的瀏覽器中本地執行。

若需在格式間轉換,JSON 轉 YAMLJSON 轉 CSV 也採用相同的隱私為先做法 — 你的資料絕不離開裝置。

結論

對多數用例,JSON.stringify(obj, null, 2) 就已足夠。當你需要過濾、遮罩或排序鍵時,加上取代器函數。處理外部輸入時,將 JSON.parse 包在 try/catch 中。只有在檔案大小讓同步解析變得不切實際時,才採用串流。並將邊界情況 — 循環引用、BigIntMap/Setundefined — 放在腦海中,在處理不尋常的資料形狀時隨時取用。