深拷贝系列 ———— 自己实现一个JSON.stringify和JSON.parse

深拷贝系列 ———— 什么是深拷贝、浅拷贝、Object.assign
深拷贝系列 ———— 自己实现一个 JSON.stringify 和 JSON.parse
深拷贝系列 ———— 自己通过递归实现一个深拷贝
深拷贝系列 ———— 分析 lodash 中的 deepcopy

简介

在上篇文章我们已经了解什么是深拷贝浅拷贝,也着重介绍了浅拷贝相关的一下实现方法,或者自己实现一个浅拷贝等等。本篇文章主要介绍深拷贝的一种简单实现方式JSON.parse/JSON.stringify。在平常开发时我们可以经常的看到别人使用,或者在不那么了解深拷贝时自己也有使用。

JSON.parse/JSON.stringify其实是用来序列化 JSON 格式的数据的方法。那它为什么能实现一个简单的深拷贝呢?
在执行JSON.stringify会把我们的一个对象序列化为字符串,而字符串是基本类型。
再通过JSON.parse时,把字符串类型反序列化为对象,这个时候因为在反序列化之前它是基本类型所以他会指向一个新的地址,在反序列化之后它是一个对象会再分配内存空间。
所以JSON.parse/JSON.stringify可以实现一个简单的深拷贝

本篇文章首先实现一个JSON.stringify/JSON.parse,下一篇文章实现一个比较完整的深拷贝

实例

直接上代码验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 声明原始对象
var old = {
name: "old",
attr: {
age: 18,
sex: "man"
},
title: ["M1", "P6"]
};

// 声明一个新对象,通过SON.parse/JSON.stringify 实现对原始对象深拷贝,并且赋值给新对象
var newValue = JSON.parse(JSON.stringify(old));
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}, title: [['M1', 'P6']]}

// 修改原始对象的name,新对象不受影响
old.name = "new";
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}, title: [['M1', 'P6']]}
console.log(old); // {name: "new", attr: {age: 18, sex: "man"}, title: [['M1', 'P6']]}

// 修改原始对象的引用类型,新对象也不受影响
old.attr.age = 20;
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}, title: [['M1', 'P6']]}
console.log(old); // {name: "new", attr: {age: 20, sex: "man"}, title: [['M1', 'P6']]}

其实是不是以为用这个就可以了,并没有什么问题啊,下面我们就来一点点揭开它的面纱。

局限性

其实JSON.parse/JSON.stringify还是有很多局限性,大致如下:

  • 会忽略 undefined
  • 会忽略 Symbol
  • 无法序列化function,也会忽略
  • 无法解决循环引用,会报错
  • 深层对象转换爆栈

直接上代码验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明一个包含undefined、null、symbol、function的对象
var oldObj = {
name: "old",
age: undefined,
sex: Symbol("setter"),
title: function() {},
lastName: null
};
var newObj = JSON.parse(JSON.stringify(oldObj));
// 可以看到会忽略undefined、symbol、function的对象
console.log(newObj); // {name: "old", lastName: null}

var firstObj = {
name: "firstObj"
};
firstObj.newKey = firstObj;
// Converting circular structure to JSON
var newFirstObj = JSON.parse(JSON.stringify(firstObj));

如果循环引用报错如下图所示:
JSON.parse/JSON.stringify

一个生成任意深度、广度对象方法。

1
2
3
4
5
6
7
8
9
10
11
12
function createData(deep, breadth) {
var data = {};
var temp = data;

for (var i = 0; i < deep; i++) {
temp = temp["data"] = {};
for (var j = 0; j < breadth; j++) {
temp[j] = j;
}
}
return data;
}

验证JSON.stringify递归爆栈

1
2
JSON.stringify(createData(10000));
// VM97994:1 Uncaught RangeError: Maximum call stack size exceeded

自己实现 JSON.stringify

  • 首先一个简单的递归
  • 区分StringBooleanNumbernull
  • 过滤undefinedsymbolfunction
  • 循环引用警告

一个简单的递归

实现目标

  • 递归调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 数据类型判断
function getType(attr) {
let type = Object.prototype.toString.call(attr);
let newType = type.substr(8, type.length - 9);
return newType;
}

// 转换函数
function StringIfy(obj) {
// 如果是非object类型 or null的类型直接返回 原值的String
if (typeof obj !== "object" || getType(obj) === null) {
return String(obj);
}
// 声明一个数组
let json = [];
// 判断当前传入参数是对象还是数组
let arr = obj ? getType(obj) === "Array" : false;
// 循环对象属性
for (let key in obj) {
// 判断属性是否在对象本身上
if (obj.hasOwnProperty(key)) {
// 获取属性并且判断属性值类型
let item = obj[key];
// 如果为object类型递归调用
if (getType(obj) === "Object") {
// consoarrle.log(item)
item = StringIfy(item);
}
// 拼接数组字段
json.push((arr ? '"' : '"' + key + '": "') + String(item) + '"');
}
}
console.log(arr, String(json));
// 转换数组字段为字符串
return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
}

// 测试代码
StringIfy({ name: { name: "abc" } }); // "{"name": "{"name": "abc"}"}"
StringIfy([1, 2, 4]); // "["1","2","4"]"

在上面代码中我们基本的JSON序列化,可以序列化引用类型基本类型

区分数据类型

我说的区分的类型,是JSON.stringify再序列化时,像NumberBooleannull它是不会加上双引号的,只有在String类型或者Object中的key才会带双引号

  • 增加一个判断当前属性类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 。。。省略代码

// 转换函数
function StringIfy(obj) {
// 。。。省略代码
let IsQueto =
getType(item) === "Number" ||
getType(item) === "Boolean" ||
getType(item) === "Null"
? ""
: '"';
// 拼接数组字段
json.push((arr ? IsQueto : '"' + key + '": "') + String(item) + IsQueto);
// 。。。省略代
}

// 测试代码
StringIfy({ name: { name: "abc" } }); // "{"name": "{"name": "abc"}"}"
StringIfy([1, 2, 4]); // "[1,2,4]"

不处理部分值

  • 通过正则判断过滤Symbol|Function|Undefined
  • 跳过当前循环
1
2
3
4
5
6
7
8
9
10
11
12
if (/Symbol|Function|Undefined/.test(getType(item))) {
delete obj[key];
continue;
}
let test = {
name: 'name',
age: undefined,
func: function () {},
sym: Symbol('setter')
};
let newTest = StringIfy(test);
console.log(newTest); // {"name": "name"}

循环引用警告

  • 处理循环引用,警告并且退出循环
1
2
3
4
if (item === obj) {
console.error(new TypeError("Converting circular structure to JSON"));
return false;
}

Stringify总结

到此自己实现JSON.stringify到此结束了,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 数据类型判断
function getType(attr) {
let type = Object.prototype.toString.call(attr);
let newType = type.substr(8, type.length - 9);
return newType;
}
// 转换函数
function StringIfy(obj) {
// 如果是非object类型 or null的类型直接返回 原值的String
if (typeof obj !== "object" || getType(obj) === null) {
return String(obj);
}
// 声明一个数组
let json = [];
// 判断当前传入参数是对象还是数组
let arr = obj ? getType(obj) === "Array" : false;
// 循环对象属性
for (let key in obj) {
// 判断属性是否在对象本身上
if (obj.hasOwnProperty(key)) {
// console.log(key, item);
// 获取属性并且判断属性值类型
let item = obj[key];
if (item === obj) {
console.error(new TypeError("Converting circular structure to JSON"));
return false;
}
if (/Symbol|Function|Undefined/.test(getType(item))) {
delete obj[key];
continue;
}
// 如果为object类型递归调用
if (getType(item) === "Object") {
// consoarrle.log(item)
item = StringIfy(item);
}
let IsQueto =
getType(item) === "Number" ||
getType(item) === "Boolean" ||
getType(item) === "Null"
? ""
: '"';
// 拼接数组字段
json.push((arr ? IsQueto : '"' + key + '": "') + String(item) + IsQueto);
}
}
console.log(arr, String(json));
// 转换数组字段为字符串
return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
}
let aa = StringIfy([1, 2, 4]);
let test = {
name: "name",
age: undefined,
func: function() {},
sym: Symbol("setter")
};
let newTest = StringIfy(test);
console.log(aa, newTest);
var firstObj = {
name: "firstObj"
};
firstObj.newKey = firstObj;
StringIfy(firstObj);

JSON.parse 实现

有两种方法实现parse效果,第一种是eval实现,另一种是Function实现,下面直接开始。

eval 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ParseJson(opt) {
return eval("(" + opt + ")");
}

let aa = StringIfy([1, 2, 4]);
ParseJson(aa); // [1, 2, 4]

let test = {
name: "name",
age: undefined,
func: function() {},
sym: Symbol("setter")
};
let newTest = StringIfy(test);
console.log(ParseJson(newTest)); // {name: "name"}

可以看到上面的代码可以实现基本的反序列化。

避免在不必要的情况下使用 eval,eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你用 eval()运行的字符串代码被恶意方(不怀好意的人)操控修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。

Function 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ParseJsonTwo(opt) {
return new Function("return " + opt)();
}

let aa = StringIfy([1, 2, 4]);
ParseJson(aa); // [1, 2, 4]

let test = {
name: "name",
age: undefined,
func: function() {},
sym: Symbol("setter")
};
let newTest = StringIfy(test);
console.log(ParseJson(newTest)); // {name: "name"}

evalFunction 都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用。

处理 XSS

它会执行 JS 代码,有 XSS 漏洞。

如果你只想记这个方法,就得对参数 json 做校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;

var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;

var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
rx_one.test(
json.replace(rx_two, "@").replace(rx_three, "]").replace(rx_four, "")
);
) {
var obj = ParseJson(json); // ParseJson(json) or ParseJsonTwo(json)
}

Parse总结

其实无论在什么时候都不太推荐evalfunction,因为它很容造成入侵。
如果有兴趣可以去看一下JSON.parse 三种实现方式,它有涉及到递归实现,状态机实现,讲的也不错。

总结

本篇文章主要讲解了JSON.parse/JSON.stringify是怎么实现的深拷贝,并且深入了解一下JSON.parse/JSON.stringify深拷贝上的实现,其实还有怎么加速JSON序列化的速度,会在另一篇文章中讲解。最后自己也简单实现了一个ParseJson/StringIfy

参考

无敌秘籍之 — JavaScript手写代码
JSON.parse 三种实现方式