JSON 作為前後端數據交換標準,安全高效的處理至關重要。本文整理一套輕量級 JSON 工具函數,涵蓋 XSS 防護解析、格式驗證、序列化/反序列化、精確類型判斷,代碼源自 Prototype、json2.js 等經典庫精華,適合直接集成到項目或作為學習參考。
後端返回 JSON 時,為防止直接 eval 導致 XSS,可約定將 JSON 包裹在註釋中:
/*-secure-
{"name": "Violet", "age": 25}
-*/
const JSONFilter = /^\/\*-secure-\s*([\s\S]*?)\s*\*\/\s*$/;
function unfilterJSON(json, filter = JSONFilter) {
return json.replace(filter, (_, captured) => captured || '');
}
function isJSONText(json) {
// 源自 json2.js,驗證純 JSON 格式
return /^[\],:{}\s]*$/
.test(json.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''));
}
function evalJSON(json, sanitize = true) {
if (typeof json !== 'string' || !json) return null;
json = unfilterJSON(json);
if (!sanitize || isJSONText(json)) {
return window.JSON && window.JSON.parse
? window.JSON.parse(json)
: new Function('return ' + json)(); // 兼容舊瀏覽器
}
throw new Error('Invalid JSON: ' + json);
}
const response = '/*-secure-\n{"name":"Violet","age":25}\n*/';
const data = evalJSON(response);
console.log(data.name); // "Violet"
優先使用原生 JSON.stringify,降級實現支持 Date、RegExp、undefined、函數等特殊類型轉換。
function toJSON(obj) {
if (window.JSON && window.JSON.stringify) return JSON.stringify(obj);
const escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
const meta = {'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};
function quote(str) {
return '"' + str.replace(escapable, c => meta[c] || '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)) + '"';
}
function f(n) { return n < 10 ? '0' + n : n; }
if (obj === null) return 'null';
const type = typeof obj;
if (type === 'number') return isFinite(obj) ? String(obj) : 'null';
if (type === 'string') return quote(obj);
if (type === 'boolean') return String(obj);
if (obj instanceof Date) {
return '"' + obj.getUTCFullYear() + '-' + f(obj.getUTCMonth()+1) + '-' + f(obj.getUTCDate()) + 'T' +
f(obj.getUTCHours()) + ':' + f(obj.getUTCMinutes()) + ':' + f(obj.getUTCSeconds()) + 'Z"';
}
if (Array.isArray(obj)) {
const arr = obj.map(v => toJSON(v) === undefined ? 'null' : toJSON(v));
return '[' + arr.join(',') + ']';
}
if (type === 'object') {
const pairs = [];
for (const k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) {
const v = toJSON(obj[k]);
if (v !== undefined) pairs.push(quote(k) + ':' + v);
}
return '{' + pairs.join(',') + '}';
}
return undefined; // 函數、undefined、Symbol 等忽略
}
typeof 和 instanceof 存在跨框架、null/array 混淆等缺陷,推薦基於 Object.prototype.toString 的方案。
// 判斷 obj 是否為 type 類型(type 為 "Array", "Object", "Function", "Date", "RegExp", "String", "Number", "Boolean", "Null", "Undefined")
function is(obj, type) {
if (type === 'Null') return obj === null;
if (type === 'Undefined') return obj === void 0;
if (type === 'Number') return typeof obj === 'number' && isFinite(obj);
return Object.prototype.toString.call(obj).slice(8, -1) === type;
}
// 直接返回類型字符串
function getType(obj) {
if (obj === null) return 'Null';
if (obj === void 0) return 'Undefined';
return Object.prototype.toString.call(obj).slice(8, -1);
}
// 原型鏈判斷(ES5+)
function isA(thing, canon) {
if (thing == null || canon == null) return thing === canon;
return Object.getPrototypeOf(Object(thing)) === Object.getPrototypeOf(Object(canon));
}
// 便捷判斷
const isBool = thing => isA(thing, true);
const isNumber = thing => isA(thing, 0) && isFinite(thing);
const isString = thing => isA(thing, '');
console.log(getType([1,2,3])); // "Array"
console.log(getType(null)); // "Null"
console.log(getType(undefined)); // "Undefined"
console.log(getType(document.getElementsByTagName('span'))); // "HTMLCollection"
console.log(getType(document.body.childNodes)); // "NodeList"
function forEach(array, fn, bind) {
if (is(array, 'Array') && is(Array.forEach, 'Function')) {
array.forEach(fn, bind);
} else {
for (let i = 0, n = array.length; i < n; i++) {
i in array && fn.call(bind, array[i], i, array);
}
}
}
JSON.parse/stringify,性能最優、安全性最高。isJSONText。Content-Security-Policy: script-src 'self' 禁用內聯腳本,從根源阻斷 XSS。isArray()、typeof、instanceof、用戶定義類型守衛,編譯期消除類型不確定性。fast-json-stringify(高性能序列化)、ajv(JSON Schema 驗證)、superjson(支持 Date/Set/Map/undefined 等擴展類型)。