7 Mẹo JSON Giúp Bạn Tiết Kiệm Hàng Giờ
Mọi nhà phát triển JavaScript đều dùng JSON.parse và JSON.stringify hàng chục lần mỗi tuần. Nhưng đa số dừng ở mức cơ bản — phân tích phản hồi API và tuần tự hóa object thành chuỗi. API này cung cấp nhiều hơn thế, và một số tính năng ít được dùng có thể giải quyết các vấn đề mà nếu không sẽ cần thư viện bên thứ ba hoặc hàng giờ gỡ lỗi.
Hướng dẫn này bao quát bảy mẹo vượt ra ngoài các mặc định. Mỗi mẹo áp dụng được ngay vào codebase thực tế. Không trừu tượng, không ví dụ bịa — đây là các mẫu xuất hiện trong các hệ thống production.
Bạn có thể xác minh và khám phá bất kỳ ví dụ code nào trong bài viết này bằng Toova JSON Formatter, công cụ xác thực và in đẹp JSON hoàn toàn trong trình duyệt của bạn.
1. Sao Chép Sâu Bằng JSON Round-Trip (Và Giới Hạn Của Nó)
Mẹo xưa nhất trong sổ tay JavaScript: dùng JSON.parse(JSON.stringify(obj)) để tạo bản sao sâu của một object thông thường.
const original = { a: 1, b: { c: 2 } };
// Cách ngây thơ — các object lồng sâu vẫn được dùng chung
const shallowCopy = { ...original }; // b vẫn trỏ tới cùng object
// Sao chép sâu bằng JSON — tạo một bản sao hoàn toàn độc lập
const deepClone = JSON.parse(JSON.stringify(original));
deepClone.b.c = 99;
console.log(original.b.c); // 2 — bản gốc không bị thay đổi Cách này hoạt động vì tuần tự hóa thành chuỗi và phân tích trở lại tạo một cây object hoàn toàn mới không có tham chiếu chia sẻ. Nhanh, không cần dependency, và đã có từ ES5.
Phương án hiện đại cho việc dùng trong bộ nhớ là structuredClone():
// Phương án hiện đại: structuredClone() — xử lý nhiều kiểu hơn
// Hỗ trợ trong Node.js 17+ và mọi trình duyệt evergreen
const clone = structuredClone(original); Biết giới hạn của cách tiếp cận JSON trước khi dựa vào nó:
// JSON.parse/stringify KHÔNG xử lý được:
const broken = {
date: new Date(), // trở thành chuỗi — mất prototype Date
fn: () => 'hello', // bị bỏ âm thầm
undef: undefined, // bị bỏ âm thầm
inf: Infinity, // trở thành null
map: new Map(), // trở thành {}
cycle: null, // tham chiếu vòng tròn sẽ throw
};
Nếu object của bạn chỉ chứa dữ liệu đơn giản — chuỗi, số, boolean, mảng và các object lồng đơn giản — vòng round-trip JSON an toàn và nhanh. Đối với bất cứ điều gì khác, ưu tiên structuredClone() hoặc một thư viện chuyên biệt.
2. Replacer Tùy Chỉnh Để Lọc Và Ẩn
Hầu hết nhà phát triển biết rằng JSON.stringify nhận đối số thứ hai, nhưng hiếm khi sử dụng nó. Đối số thứ hai đó là replacer: hoặc là mảng các khóa để bao gồm, hoặc là hàm kiểm soát chính xác cách mỗi giá trị được tuần tự hóa.
Replacer dạng mảng — danh sách trắng các khóa cụ thể:
const user = {
id: 'u_001',
name: 'Alice',
password: 'hunter2', // không được xuất hiện trong log
creditCard: '4111111111111111', // không được xuất hiện trong log
role: 'admin',
};
// Replacer dạng mảng: chỉ bao gồm các khóa này
JSON.stringify(user, ['id', 'name', 'role']);
// '{"id":"u_001","name":"Alice","role":"admin"}' Replacer dạng hàm — biến đổi hoặc ẩn các giá trị:
// Replacer dạng hàm: kiểm soát hoàn toàn key/value
const masked = JSON.stringify(user, (key, value) => {
if (key === 'password' || key === 'creditCard') return '[REDACTED]';
if (key === '' ) return value; // object gốc — luôn return
return value;
});
// '{"id":"u_001","name":"Alice","password":"[REDACTED]","creditCard":"[REDACTED]","role":"admin"}' Phiên bản tinh vi hơn ẩn giá trị dựa trên hình dạng của chúng thay vì tên khóa:
// Replacer cho việc ẩn dựa trên kiểu
const sanitize = (key, value) => {
if (typeof value === 'string' && value.match(/^4[0-9]{15}$/)) {
return '****-****-****-' + value.slice(-4);
}
return value;
}; Kỹ thuật này không thể thiếu cho middleware ghi log: bạn muốn log có cấu trúc với ngữ cảnh object đầy đủ, nhưng một số trường không bao giờ được đến aggregator log. Replacer cho phép bạn xử lý điều này tại biên tuần tự hóa thay vì rải logic ẩn khắp codebase.
3. Sắp Xếp Khóa Một Cách Quyết Định
Thứ tự khóa object trong JavaScript là thứ tự chèn (đối với khóa chuỗi). Hai object có cùng khóa nhưng được tạo theo thứ tự khác nhau tạo ra chuỗi JSON khác nhau, phá vỡ các phép kiểm tra bằng ngây thơ, khóa cache, và hash nội dung.
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 — thứ tự khóa được chuẩn hóa Đối với object lồng sâu, áp dụng sắp xếp đệ quy:
// Sắp xếp khóa đệ quy cho object lồng nhau
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 đã sắp xếp cần thiết khi bạn:
- Tạo khóa cache từ body request
- Tính tổng kiểm tra hoặc chữ ký trên payload JSON
- So sánh phản hồi API trong test bất kể thứ tự trường
- Lưu trữ object cấu hình mà thứ tự không nên ảnh hưởng đến tính bằng
Sau khi sắp xếp và in đẹp, dùng công cụ Text Diff để so sánh hai chuỗi JSON đã chuẩn hóa và xem chính xác giá trị nào đã thay đổi giữa các phiên bản.
4. Xử Lý BigInt Mà Không Mất Độ Chính Xác
Kiểu Number của JavaScript có thể biểu diễn an toàn các số nguyên đến 253 − 1. Đối với ID được tạo bởi các hệ thống phân tán, số tiền tài chính theo đơn vị nhỏ, hoặc dấu thời gian theo nanosec, điều này không đủ. BigInt bao quát các số nguyên với độ chính xác tùy ý, nhưng JSON.stringify không biết phải làm gì với chúng.
const data = {
amount: 9007199254740993n, // lớn hơn Number.MAX_SAFE_INTEGER
};
// Cái này throw: TypeError: Do not know how to serialize a BigInt
JSON.stringify(data); // LỖI Các giải pháp tiêu chuẩn:
// Giải pháp 1: Chuyển thành chuỗi với replacer
JSON.stringify(data, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"amount":"9007199254740993"}'
// Giải pháp 2: toJSON() trên prototype BigInt (monkey-patch — dùng cẩn thận)
BigInt.prototype.toJSON = function () { return this.toString(); };
JSON.stringify(data); // '{"amount":"9007199254740993"}' Ở phía phân tích, một hàm reviver có thể khôi phục các giá trị BigInt từ biểu diễn chuỗi của chúng:
// Reviver để khôi phục BigInt khi parse
const revived = JSON.parse('{"amount":"9007199254740993"}', (key, value) => {
if (key === 'amount') return BigInt(value);
return value;
});
console.log(typeof revived.amount); // 'bigint'
Giữ chuyển đổi BigInt-thành-chuỗi trong một lớp tuần tự hóa chia sẻ để nó được áp dụng nhất quán. Để các BigInt rò rỉ tới các cuộc gọi JSON.stringify đặc biệt ở các rìa của codebase dẫn đến các lỗi không thể đoán trước rất khó truy vết.
5. Phát Hiện Tham Chiếu Vòng Tròn
Tham chiếu vòng tròn xảy ra khi một object chứa tham chiếu đến chính nó hoặc đến một tổ tiên trong đồ thị object. Chúng phổ biến hơn bạn nghĩ: các event emitter, node DOM, node React fiber, và các thực thể ORM đều thường có tham chiếu ngược.
// Ví dụ tham chiếu vòng tròn
const obj = { name: 'node' };
obj.self = obj; // obj tham chiếu chính nó
JSON.stringify(obj); // throw: TypeError: Converting circular structure to JSON Xử lý nó với một replacer tùy chỉnh theo dõi các object đã được ghé thăm:
// Bộ xử lý tham chiếu vòng tròn thủ công
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 là cấu trúc dữ liệu phù hợp ở đây: nó giữ các tham chiếu object mà không ngăn cản garbage collection, và tra cứu là O(1). Mẫu này cũng hoạt động trong middleware ghi log nơi bạn muốn các lỗi suy thoái nhẹ nhàng thay vì throw trong quá trình tuần tự hóa.
Một biến thể phổ biến: thay vì đánh dấu các tham chiếu vòng tròn là [Circular], thay chúng bằng một chuỗi đường dẫn như [Circular: $.config.parent] để làm cho vị trí tham chiếu rõ ràng khi gỡ lỗi.
6. In Đẹp Mảng Trên Một Dòng
Đối số thứ ba của JSON.stringify là phần thụt dòng. Truyền 2 hoặc 4 mở rộng mọi thứ trên nhiều dòng, điều này tuyệt vời cho object nhưng có thể dài dòng cho mảng các primitive như tag, ID hoặc tọa độ.
const mixed = {
title: 'Report',
tags: ['json', 'api', 'debug'],
config: { indent: 2, sortKeys: true },
count: 42,
};
// Mặc định: mọi thứ đa dòng
JSON.stringify(mixed, null, 2);
// {
// "title": "Report",
// "tags": [
// "json",
// "api",
// "debug"
// ],
// "config": {
// "indent": 2,
// "sortKeys": true
// },
// "count": 42
// } Bạn có thể xử lý hậu kỳ đầu ra để gộp các mảng đơn giản về một dòng:
// Replacer tùy chỉnh cho mảng một dòng
function prettyMixed(obj) {
const raw = JSON.stringify(obj, null, 2);
// Gộp các mảng chỉ chứa primitive về một dòng
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;
}
);
} Kết quả: các object giữ đa dòng để dễ đọc, các mảng primitive gộp về một dòng để gọn. Đây là định dạng được dùng trong nhiều tệp cấu hình và đầu ra log có cấu trúc nơi con người và máy móc đều cần đọc cùng dữ liệu. JSON Formatter và JSON to YAML converter xử lý việc gộp này tự động.
7. JSON Lớn Dạng Streaming
Tải một tệp JSON lớn hoàn toàn vào bộ nhớ trước khi phân tích là sai lầm hiệu năng phổ biến nhất trong pipeline xử lý JSON. Một phản hồi JSON 100 MB cấp phát ít nhất 100 MB heap cho chuỗi thô, sau đó cấp phát thứ hai cho object đã phân tích. Đối với các tệp trên vài megabyte, bộ phân tích streaming xử lý dữ liệu tăng dần.
Trên Node.js với một thư viện streaming:
// node --experimental-vm-modules (hoặc dùng thư viện streaming JSON)
// Streaming nguyên bản với đề xuất JSON Source Map (Stage 3, 2026):
// Hiện tại, dùng thư viện như @streamparser/json hoặc 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('Tìm thấy id:', value);
}
};
createReadStream('large.json').pipe(jsonParser); Trong trình duyệt dùng Fetch Streams API:
// Streaming phía trình duyệt với Fetch + bộ giải mã JSON stream:
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 });
// Xử lý các object JSON hoàn chỉnh từ buffer...
}
Streaming hữu ích nhất trong các pipeline xử lý batch, các điểm cuối xuất khẩu lớn, và các script phân tích log. Đối với phản hồi API điển hình dưới 1 MB, JSON.parse tiêu chuẩn đủ nhanh và đơn giản hơn nhiều. Ngưỡng nơi streaming bắt đầu mang lại lợi ích trong thực tế khoảng 5–10 MB, tùy vào độ phức tạp của cấu trúc đã phân tích và kích thước heap của môi trường mục tiêu.
Để khám phá cấu trúc của phản hồi JSON lớn trước khi bạn xây dựng bộ phân tích streaming, dán một mẫu vào JSON Formatter để xem các đường dẫn nào chứa dữ liệu bạn cần, sau đó chuyển sang CSV hoặc YAML để phân tích thêm.
Tổng Hợp
Bảy kỹ thuật này bao quát các góc tác động lớn nhất của JSON API:
- Sao chép sâu qua round-trip — nhanh cho dữ liệu đơn giản, biết giới hạn
- Replacer — lọc khóa và ẩn giá trị nhạy cảm tại biên tuần tự hóa
- Sắp xếp khóa — đầu ra quyết định cho khóa cache, chữ ký, và xác nhận test
- Xử lý BigInt — tuần tự hóa thành chuỗi, khôi phục khi parse
- Phát hiện tham chiếu vòng tròn — replacer dựa trên WeakSet để tuần tự hóa an toàn
- In đẹp với mảng gọn — đầu ra dễ đọc cho người không có khoảng trắng dư thừa
- Streaming JSON lớn — phân tích tăng dần cho các tệp và phản hồi API lớn
Để có tài liệu đầy đủ về các đối số replacer và space của JSON.stringify, xem tham khảo MDN JSON.stringify. Đối với phương án sao chép sâu hiện đại, xem tài liệu structuredClone(), bao quát tất cả các kiểu được hỗ trợ và các trường hợp biên.