我们熟悉 JSON.stringify
,通常用于序列化(深拷贝)。它将我们的对象转换为 JSON 字符串。这种方法在我们的工作中确实非常方便,但是这种方法也有一些缺点,我们很少遇到。
如果对象的属性是一个函数,这个属性在序列化过程中将会丢失。
let obj = {
name: 'iyongbao',
foo: function () {
console.log(`${ this.name }`)
}
}
console.log(JSON.stringify(obj)); // {"name":"iyongbao"}
如果对象的属性值是 undefined,转换后将会丢失。
let obj = {
name: undefined
}
console.log(JSON.stringify(obj)); // {}
如果对象的属性是一个正则表达式,在转换后将变成一个空对象。
let obj = {
name: 'iyongbao',
zoo: /^i/ig,
foo: function () {
console.log(`${ this.name }`)
}
}
console.log(JSON.stringify(obj)); // {"name":"iyongbao","zoo":{}}
如果是数组对象,上述情况同样会发生。
let arr = [
{
name: undefined
}
]
console.log(JSON.stringify(arr)); // [{}]
- 当
undefined
、任何function
和symbol
这三个特殊值被用作对象属性、数组元素或独立值时,JSON.stringify()
将返回不同的结果。
const data = {
a: "aaa",
b: undefined,
c: Symbol("dd"),
fn: function() {
return true;
}
};
JSON.stringify(data); // "{"a":"aaa"}"
- 当
undefined
、任何function
和symbol
被用作数组元素值时,JSON.stringify()
将把它们序列化为null
。
JSON.stringify(["aaa", undefined, function aa() {
return true
}, Symbol('dd')]) // "["aaa",null,null,null]"
- 当
undefined
、任何function
和symbol
被独立值序列化时,将返回undefined
。
JSON.stringify(function a (){console.log('a')})
// undefined
JSON.stringify(undefined)
// undefined
JSON.stringify(Symbol('dd'))
// undefined
非数组对象的属性在序列化字符串中的出现顺序无法保证。正如第一点所述,JSON.stringify()
在序列化过程中忽略了一些特殊值,因此无法保证序列化后的字符串仍然以特定顺序出现(除了数组)。
const data = {
a: "aaa",
b: undefined,
c: Symbol("dd"),
fn: function() {
return true;
},
d: "ddd"
};
JSON.stringify(data); // "{"a":"aaa","d":"ddd"}"
JSON.stringify(["aaa", undefined, function aa() {
return true
}, Symbol('dd'),"eee"]) // "["aaa",null,null,null,"eee"]"
如果要转换的值具有 toJSON()
函数,则序列化结果将是该函数返回的任何值,其他属性的值将被忽略。
JSON.stringify({
say: "hello JSON.stringify",
toJSON: function() {
return "today i learn";
}
})
// "today i learn"
JSON.stringify()
将正常序列化 Date
值。实际上,Date 对象本身实现了 toJSON()
方法(相当于 Date.toISOString()
),因此 Date 对象被视为字符串。
JSON.stringify({ now: new Date() });
// "{"now":"2024-06-16T12:43:13.577Z"}"
NaN
和 Infinity
格式的数值,以及 null
,都将被视为 null
。
JSON.stringify(NaN)
// "null"
JSON.stringify(null)
// "null"
JSON.stringify(Infinity)
// "null"
布尔值、数字和字符串的包装对象在序列化过程中将自动转换为相应的原始值。
JSON.stringify([new Number(1), new String("false"), new Boolean(false)]);
// "[1,"false",false]"
其他类型的对象,包括 Map/Set/WeakMap/WeakSet
,只会序列化可枚举属性。默认情况下,不可枚举属性将被忽略。
JSON.stringify(
Object.create(
null,
{
x: { value: 'json', enumerable: false },
y: { value: 'stringify', enumerable: true }
}
)
);
// "{"y":"stringify"}"
我们都知道实现深度克隆的最简单和最原始的方法是序列化:JSON.parse(JSON.stringify())
。由于序列化的各种特性,这种实现深度克隆的方法会导致许多问题,例如我们现在正在解决的循环引用问题。
// 在具有循环引用的对象(相互引用形成无限循环的对象)上执行此方法将会抛出错误。
const obj = {
name: "loopObj"
};
const loopObj = {
obj
};
// 对象形成循环引用,创建一个闭环
obj.loopObj = loopObj;
// 封装一个深度克隆函数
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
// 执行深度克隆,抛出错误
deepClone(obj)
/**
VM44:9 Uncaught TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'loopObj' -> object with constructor 'Object'
--- property 'obj' closes the circle
at JSON.stringify (<anonymous>)
at deepClone (<anonymous>:9:26)
at <anonymous>:11:13
*/
以 symbol
作为属性键的属性将被完全忽略,即使它们明确包含在替换器参数中。
JSON.stringify({ [Symbol.for("json")]: "stringify" }, function(k, v) {
if (typeof k === "symbol") {
return v;
}
})
// undefined
JSON.stringify
还有可选的第二和第三参数。
第二个参数 replacer
可以采用两种形式:函数或数组。作为函数时,它接收两个参数,键和值。该函数类似于数组方法(如 map、filter 等)中的回调函数,对每个属性值执行一次。如果 replacer 是一个数组,则数组中的值表示将被序列化为 JSON 字符串的属性的名称。
作为函数使用:
const data = {
a: "aaa",
b: undefined,
c: Symbol("dd"),
fn: function() {
return true;
}
};
// 不使用替换器参数
JSON.stringify(data);
// "{"a":"aaa"}"
// 使用替换器参数作为函数时
JSON.stringify(data, (key, value) => {
switch (true) {
case typeof value === "undefined":
return "undefined";
case typeof value === "symbol":
return value.toString();
case typeof value === "function":
return value.toString();
default:
break;
}
return value;
})
// "{"a":"aaa","b":"undefined","c":"Symbol(dd)","fn":"function() {\n return true;\n }"}"
当使用替换器函数时,传递给该函数的第一个参数不是对象的第一个键值对。相反,一个空字符串被用作键,值是整个对象的键值对:
const data = {
a: 2,
b: 3,
c: 4,
d: 5
};
JSON.stringify(data, (key, value) => {
console.log(value);
return value;
})
// 传递给替换器函数的第一个参数是 {"":{a: 2, b: 3, c: 4, d: 5}}
// {a: 2, b: 3, c: 4, d: 5}
// 2
// 3
// 4
// 5
实现一个 map 函数
我们还可以使用它手动实现一个类似对象的 map 函数。
// 实现一个 map 函数
const data = {
a: 2,
b: 3,
c: 4,
d: 5
};
const objMap = (obj, fn) => {
if (typeof fn !== "function") {
throw new TypeError(`${fn} is not a function !`);
}
return JSON.parse(JSON.stringify(obj, fn));
};
objMap(data, (key, value) => {
if (value % 2 === 0) {
return value / 2;
}
return value;
});
// {a: 1, b: 3, c: 2, d: 5}
作为数组使用
当替换器作为数组使用时,结果非常直接。数组中的值表示将被序列化为 JSON 字符串的属性的名称。
const jsonObj = {
name: "JSON.stringify",
params: "obj,replacer,space"
};
// 仅保留 params 属性的值
JSON.stringify(jsonObj, ["params"]);
// "{"params":"obj,replacer,space"}"
第三个参数 space 用于控制结果字符串中的间距。让我们看一个例子来理解它的作用:
const tiedan = {
name: "Jhon",
describe: "JSON.stringify()",
emotion: "like"
};
JSON.stringify(tiedan, null, "--");
// 输出如下
// "{
// --"name": "Jhon",
// --"describe": "JSON.stringify()",
// --"emotion": "like"
// }"
JSON.stringify(tiedan, null, 2);
// "{
// "name": "Jhon",
// "describe": "JSON.stringify()",
// "emotion": "like"
// }"