跳至內容
Toova
所有工具

能為你省下大量時間的 7 個 JSON 技巧

Toova

每位 JavaScript 開發者每週都會使用 JSON.parseJSON.stringify 數十次。但多數人只停留在基礎用法 — 解析 API 回應、將物件序列化為字串。這個 API 提供的功能遠不止於此,其中較少使用的某些功能能解決原本需要第三方函式庫或數小時除錯時間的問題。

本指南介紹七個超越預設行為的技巧。每一個都能立即套用於真實的程式碼基底。沒有抽象的內容,也沒有杜撰的範例 — 這些都是在生產系統中會出現的模式。

你可以使用 Toova JSON 格式化工具驗證並探索本文中的任何程式碼範例,它能完全在瀏覽器內驗證並美化 JSON。

1. 透過 JSON 來回實現深拷貝(及其限制)

JavaScript 領域中最古老的技巧:使用 JSON.parse(JSON.stringify(obj)) 建立純物件的深拷貝。

const original = { a: 1, b: { c: 2 } };

// 簡單作法 — 深層巢狀物件仍會共用
const shallowCopy = { ...original }; // b 仍指向同一個物件

// JSON 深拷貝 — 建立完全獨立的副本
const deepClone = JSON.parse(JSON.stringify(original));

deepClone.b.c = 99;
console.log(original.b.c); // 2 — 原始物件未受影響

這之所以有效,是因為將物件序列化為字串並重新解析後,會建立完全沒有共享引用的全新物件樹。它快速、不需要相依套件,自 ES5 起就已可用。

用於記憶體內處理的現代替代方案是 structuredClone()

// 現代替代方案:structuredClone() — 處理更多型別
// Node.js 17+ 與所有常綠瀏覽器皆支援
const clone = structuredClone(original);

在仰賴 JSON 方法之前,先了解其限制:

// JSON.parse/stringify「無法」處理:
const broken = {
  date: new Date(),      // 變成字串 — 失去 Date 原型
  fn: () => 'hello',    // 默默被丟棄
  undef: undefined,     // 默默被丟棄
  inf: Infinity,        // 變成 null
  map: new Map(),       // 變成 {}
  cycle: null,          // 循環引用會擲出例外
};

如果你的物件只包含純資料 — 字串、數字、布林、陣列與巢狀純物件 — JSON 來回拷貝既安全又快速。對於其他情況,優先使用 structuredClone() 或專用函式庫。

2. 用於過濾與遮罩的自訂取代器

多數開發者知道 JSON.stringify 接受第二個參數,但很少使用。該第二個參數是「取代器」(replacer):可以是要納入的鍵陣列,也可以是控制每個值如何序列化的函數。

陣列取代器 — 將特定鍵列為白名單:

const user = {
  id: 'u_001',
  name: 'Alice',
  password: 'hunter2',        // 不應出現在日誌中
  creditCard: '4111111111111111', // 不應出現在日誌中
  role: 'admin',
};

// 取代器為陣列:只包含這些鍵
JSON.stringify(user, ['id', 'name', 'role']);
// '{"id":"u_001","name":"Alice","role":"admin"}'

函數取代器 — 轉換或塗銷值:

// 取代器為函數:完全控制鍵/值
const masked = JSON.stringify(user, (key, value) => {
  if (key === 'password' || key === 'creditCard') return '[REDACTED]';
  if (key === '' ) return value; // 根物件 — 永遠回傳
  return value;
});
// '{"id":"u_001","name":"Alice","password":"[REDACTED]","creditCard":"[REDACTED]","role":"admin"}'

更精緻的版本依據值的形狀而非其鍵名遮罩:

// 依型別遮罩的取代器
const sanitize = (key, value) => {
  if (typeof value === 'string' && value.match(/^4[0-9]{15}$/)) {
    return '****-****-****-' + value.slice(-4);
  }
  return value;
};

這項技巧對日誌中介軟體不可或缺:你希望有完整物件上下文的結構化日誌,但某些欄位絕不能抵達日誌彙整器。取代器讓你能在序列化邊界處理這件事,而不是將塗銷邏輯分散到程式碼基底各處。

3. 以確定性方式排序鍵

JavaScript 中的物件鍵順序為插入順序(字串鍵)。具有相同鍵但以不同順序建立的兩個物件會產生不同的 JSON 字串,這會破壞單純的相等性檢查、快取鍵與內容雜湊。

function sortedStringify(obj) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

const a = { z: 1, a: 2, m: 3 };
const b = { a: 2, m: 3, z: 1 };

sortedStringify(a) === sortedStringify(b); // true — 鍵順序已標準化

對深層巢狀物件,請遞迴套用排序:

// 對巢狀物件遞迴排序鍵
function sortKeys(value) {
  if (Array.isArray(value)) return value.map(sortKeys);
  if (value !== null && typeof value === 'object') {
    return Object.fromEntries(
      Object.keys(value).sort().map((k) => [k, sortKeys(value[k])])
    );
  }
  return value;
}

const sorted = JSON.stringify(sortKeys(deepNested));

在以下情境中,排序後的 JSON 至關重要:

  • 從請求主體產生快取鍵
  • 對 JSON 載荷計算 checksum 或簽章
  • 在測試中比較 API 回應,不論欄位順序
  • 儲存設定物件時,順序不應影響相等性

排序與美化後,使用 文字差異比對工具比較兩個標準化後的 JSON 字串,看出版本之間哪些值有所變化。

4. 處理 BigInt 而不損失精度

JavaScript 的 Number 型別能安全表示最大到 253 − 1 的整數。對於分散式系統產生的 ID、以最小單位表示的金額或以奈秒為單位的時間戳而言,這是不夠的。BigInt 涵蓋任意精度整數,但 JSON.stringify 不知道該如何處理它們。

const data = {
  amount: 9007199254740993n, // 大於 Number.MAX_SAFE_INTEGER
};

// 這會擲出例外:TypeError: Do not know how to serialize a BigInt
JSON.stringify(data); // 錯誤

標準的變通方式:

// 解法 1:使用取代器轉成字串
JSON.stringify(data, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"amount":"9007199254740993"}'

// 解法 2:在 BigInt 原型上加入 toJSON()(猴子補丁 — 請謹慎使用)
BigInt.prototype.toJSON = function () { return this.toString(); };
JSON.stringify(data); // '{"amount":"9007199254740993"}'

在解析端,reviver 函數能從 BigInt 的字串表示還原為 BigInt 值:

// 使用 reviver 在解析時還原 BigInt
const revived = JSON.parse('{"amount":"9007199254740993"}', (key, value) => {
  if (key === 'amount') return BigInt(value);
  return value;
});
console.log(typeof revived.amount); // 'bigint'

請將 BigInt 對字串的轉換放在共享的序列化層,以便一致套用。如果讓 BigInt 洩漏到程式碼基底邊緣的臨時 JSON.stringify 呼叫中,會導致難以追蹤的不可預測錯誤。

5. 循環引用偵測

當物件包含對自身或對物件圖中祖先的引用時,就會出現循環引用。它們比你想像中更常見:事件發射器、DOM 節點、React fiber 節點與 ORM 實體常常有回向引用。

// 循環引用範例
const obj = { name: 'node' };
obj.self = obj; // obj 引用自身

JSON.stringify(obj); // 擲出:TypeError: Converting circular structure to JSON

使用追蹤已造訪物件的自訂取代器來處理它:

// 手動的循環引用處理器
function safeStringify(obj) {
  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;
  });
}

safeStringify(obj); // '{"name":"node","self":"[Circular]"}'

WeakSet 是此處正確的資料結構:它持有物件引用而不阻止垃圾回收,查詢為 O(1)。此模式也適用於日誌中介軟體,讓錯誤能優雅地降級,而不是在序列化期間擲出例外。

常見變體:不要將循環引用標記為 [Circular],而是將其取代為路徑字串,例如 [Circular: $.config.parent],讓引用位置在除錯時更為明確。

6. 將陣列美化為單行

JSON.stringify 的第三個參數是縮排。傳入 24 會將所有內容展開為多行,這對物件來說很好,但對於標籤、ID 或座標等基本型別陣列而言可能過於囉嗦。

const mixed = {
  title: 'Report',
  tags: ['json', 'api', 'debug'],
  config: { indent: 2, sortKeys: true },
  count: 42,
};

// 預設:全部多行
JSON.stringify(mixed, null, 2);
// {
//   "title": "Report",
//   "tags": [
//     "json",
//     "api",
//     "debug"
//   ],
//   "config": {
//     "indent": 2,
//     "sortKeys": true
//   },
//   "count": 42
// }

可後處理輸出,將簡單陣列收合為單行:

// 自訂取代器將陣列收合為單行
function prettyMixed(obj) {
  const raw = JSON.stringify(obj, null, 2);
  // 將僅含基本型別的陣列收合為單行
  return raw.replace(
    /\[\n\s+([\s\S]*?)\n\s+\]/g,
    (match, inner) => {
      const items = inner.split(',\n').map((s) => s.trim());
      if (items.every((s) => !/^[{\[]/.test(s))) {
        return '[' + items.join(', ') + ']';
      }
      return match;
    }
  );
}

結果:物件保持多行以利可讀性,基本型別陣列則收合為單行以利精簡。這是許多設定檔與結構化日誌輸出所採用的格式,人類與機器都需要讀取相同的資料。JSON 格式化工具JSON 轉 YAML 轉換工具會自動處理這種收合。

7. 串流大型 JSON

在解析之前將大型 JSON 檔案完整載入記憶體,是 JSON 處理流程中最常見的單一效能錯誤。100 MB 的 JSON 回應會至少配置 100 MB 的堆積空間給原始字串,再配置一份給解析後的物件。對於數 MB 以上的檔案,串流解析器會以漸進方式處理資料。

在 Node.js 上搭配串流函式庫:

// node --experimental-vm-modules(或使用串流 JSON 函式庫)
// 使用 JSON Source Map 提案(2026 年 Stage 3)的原生串流:

// 目前可使用 @streamparser/json 或 jsonstream2 等函式庫
import { createReadStream } from 'fs';
import parser from '@streamparser/json';

const jsonParser = new parser.JSONParser();
jsonParser.onValue = ({ value, key, parent }) => {
  if (key === 'id') {
    console.log('Found id:', value);
  }
};

createReadStream('large.json').pipe(jsonParser);

在瀏覽器中使用 Fetch Streams API:

// 瀏覽器端透過 Fetch + JSON 串流解碼器:
const response = await fetch('/api/large-data');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  // 從緩衝區處理完整的 JSON 物件...
}

串流在批次處理流程、大型匯出端點與日誌分析腳本中最為有用。對於通常小於 1 MB 的 API 回應,標準的 JSON.parse 已足夠快且簡單得多。串流在實務上開始有報酬的門檻大約是 5–10 MB,實際取決於解析結構的複雜度與目標環境的堆積大小。

在建構串流解析器之前,若要探索大型 JSON 回應的結構,請將樣本貼到 JSON 格式化工具中,查看哪些路徑包含所需資料,再轉換為 CSVYAML 進行進一步分析。

整合運用

這七種技巧涵蓋了 JSON API 最具影響力的幾個角落:

  • 透過來回拷貝實現深拷貝 — 對純資料快速,需注意限制
  • 取代器 — 在序列化邊界過濾鍵與遮罩敏感值
  • 鍵排序 — 為快取鍵、簽章與測試斷言產生確定性輸出
  • BigInt 處理 — 序列化為字串,解析時還原
  • 循環引用偵測 — 以 WeakSet 為基礎的取代器實現安全序列化
  • 美化列印搭配精簡陣列 — 人類可讀的輸出,沒有多餘空白
  • 串流大型 JSON — 對大型檔案與 API 回應進行漸進式解析

若需 JSON.stringify 的取代器與空白參數的完整文件,請參閱 MDN JSON.stringify 參考。若需現代深拷貝替代方案,請參閱 structuredClone() 文件,其中涵蓋所有支援的型別與邊界情況。