JavaScript 中的深拷贝和浅拷贝

关于深拷贝和浅拷贝的定义:

  • 浅拷贝 - 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 深拷贝 - 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

使用 JSON.parse(JSON.stringify()) 来实现深拷贝

可以使用 JSON.parse(JSON.stringify(obj)) 来实现对目标对象的深拷贝,但是这种方法有很多缺陷:

  • 如果 undefinedSymbol 和 函数是对象的属性值或以Symbo值为键的属性值,拷贝的对象会丢失这些属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const originObj = {
    name: 'Trump',
    age: 94,
    phone: undefined,
    idNo: Symbol('1900'),
    speak: () => console.log('I am Trump'),
    [Symbol.for('weight')]: 120,
    }

    const copyObj = JSON.parse(JSON.stringify(originObj));
    console.log(copyObj); // { name: 'Trump', age: 94 }
  • 无法处理循环引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const team = {
    teamName: 'MAGA',
    }

    const originObj = {
    name: 'Trump',
    age: 94,
    phone: undefined,
    idNo: Symbol('1900'),
    speak: () => console.log('I am Trump'),
    [Symbol.for('weight')]: 120,
    team,
    }

    team.leader = originObj;

    const copyObj = JSON.parse(JSON.stringify(originObj));
    console.log(copyObj); // TypeError: Converting circular structure to JSON

一个比较全面的深拷贝实现

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
const TYPE_OBJECT = '[object Object]';
const TYPE_MAP = '[object Map]';
const TYPE_SET = '[object Set]';
const TYPE_ARRAY = '[object Array]';
const TYPE_ARGUMENTS = '[object Arguments]';
const TYPE_BOOLEAN = '[object Boolean]';
const TYPE_DATE = '[object Date]';
const TYPE_NUMBER = '[object Number]';
const TYPE_STRING = '[object String]';
const TYPE_SYMBOL = '[object Symbol]';
const TYPE_ERROR = '[object Error]';
const TYPE_REGEXP = '[object RegExp]';
const TYPE_FUNCTION = '[object Function]';
const TYPE_UNDEFINED = '[object Undefined]';

const forEach = (array, iterator) => {
let index = -1;
let length = array.length;
while(++index < length) {
iterator(array[index], index);
}
return array;
}

const isObj = (value) => {
const type = typeof value;
return value !== null && (type === 'object' || type === 'function');
}

const getType = (value) => {
return Object.prototype.toString.call(value);
}

const canCopyTags = [TYPE_OBJECT, TYPE_ARRAY, TYPE_MAP, TYPE_SET, TYPE_ARGUMENTS];

const getInit = (value) => {
const ctor = value.constructor;
return new ctor();
}

const copyReg = (value) => {
const regFlags = /\w*$/;
const newReg = new value.constructor(value.source, regFlags.exec(value));
newReg.lastIndex = value.lastIndex;
return newReg;
}

const copySymbol = (value) => {
return Object(Symbol.prototype.valueOf.call(value));
}

const copyFunction = (value) => {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
if (value.prototype) {
const body = bodyReg.exec(value.toString());
const param = paramReg.exec(value.toString());
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(value.toString());
}
}

const copyOther = (value, type) => {
const ctor = value.constructor;
switch(type) {
case TYPE_BOOLEAN:
case TYPE_NUMBER:
case TYPE_STRING:
case TYPE_ERROR:
case TYPE_DATE:
return new ctor(value);
break;
case TYPE_REGEXP:
return copyReg(value);
break;
case TYPE_SYMBOL:
return copySymbol(value);
break;
case TYPE_FUNCTION:
return copyFunction(value);
break;
default:
return null;
}
}

const copy = (origin, relationMap = new WeakMap()) => {
if (!isObj(origin)) return origin;

const type = getType(origin);
let target;
if (canCopyTags.includes(type)) {
target = getInit(origin);
} else {
return copyOther(origin, type);
}

if (relationMap.get(origin)) return relationMap.get(origin);
relationMap.set(origin, target);

if (type === TYPE_SET) {
origin.forEach((value) => {
target.add(copy(value));
});
return target;
}

if (type === TYPE_MAP) {
origin.forEach((value, key) => {
target.set(key, copy(value));
});
return target;
}

const keys = type === TYPE_ARRAY ? undefined : Object.keys(origin);
forEach(keys || origin, (value, key) => {
if (keys) {
key = value;
}
target[key] = copy(origin[key], relationMap);
});

return target;
}

const originObj = {
name: 'Trump',
age: 94,
phone: undefined,
idNo: Symbol('1900'),
speak: () => console.log('I am Trump'),
[Symbol.for('weight')]: 120,
team: {
leader: 'Ywank',
office: {
address: 'white house'
}
},
employee: [
{ name: 'pense', },
{ name: 'fox', },
],
}

originObj.president = originObj;

console.log(copy(1)); // 1
console.log(copy(undefined)); // undefined
console.log(copy(/\d/)); // /\d/
console.log(copy(new Date())); // 2018-05-16T08:06:45.361Z

const someFun = function(num1, num2) {
console.log(num1 + num2);
return num1 + num2;
}

const copyFun = copy(someFun);
console.log(copyFun(1, 3)); // 4

const someSet = new Set();
someSet.add(1);
console.log(copy(someSet)); // Set(1) { 1 }

const copyObj = copy(originObj);
console.log(originObj);
console.log(copyObj);
copyObj.speak(); // I am Trump

为什么使用 while 实现遍历

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
const ARRAY_SIZE = 10000000;
const LARGE_ARRY = new Array(ARRAY_SIZE).fill(1);

// for...in
let forInCopy = 0;
console.time('FOR_IN');
for (let key in LARGE_ARRY) {
forInCopy += LARGE_ARRY[key];
}
console.timeEnd('FOR_IN');

// for
let forCopy = 0
console.time('FOR');
for (let index = 0; index < LARGE_ARRY.length; index += 1) {
forCopy += LARGE_ARRY[index];
}
console.timeEnd('FOR');

// while
let whileCopy = 0;
let index = 0;
console.time('WHILE');
while(index < ARRAY_SIZE) {
whileCopy += LARGE_ARRY[index];
index += 1;
}
console.timeEnd('WHILE');

// forEach
let forEachCopy = 0
console.time('FOR_EACH');
LARGE_ARRY.forEach((value) => {
forEachCopy += value;
});
console.timeEnd('FOR_EACH');
遍历方式 for...in for forEach while
100 0.103ms 0.012ms 0.027ms 0.007ms
10 0.088ms 0.005ms 0.022ms 0.003ms
1000 0.313ms 0.036ms 0.052ms 0.028ms
10000 1.332ms 0.299ms 0.316ms 0.252ms
100000 14.167ms 1.691ms 1.977ms 1.428ms
1000000 140.967ms 2.928ms 14.94ms 3.012ms
10000000 2.748s 13.197ms 141.273ms 15.944ms
20000000 7.824s 22.389ms 254.557ms 28.07ms
30000000 17.107s 33.414ms 375.766ms 44.301ms

综合来看,在百万量级的数据下,使用 while 进行遍历所耗费的时间最少。

判断引用类型

1
2
3
4
const isObj = (value) => {
const type = typeof value;
return value !== null && (type === 'object' || type === 'function');
}

获取数据类型

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

1
2
3
const getType = (value) => {
return Object.prototype.toString.call(value);
}

JSON.stringify 的解析

JSON.stringify(value[, replacer [, space]]) 方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。

Note 1

JSON.stringify 方法对对象进行序列化时,如果对象的属性是 undefined/Symbol 值或任意函数时,不会对其进行处理。

这会导致被序列化的对象中的属性并不会按照原定的顺序被输出。

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
const someObj = {
_id: 'id',
productCode: 'P28120068',
productDetail: {
name: 'Star Insure',
price: 1200,
amount: 1000000
},
created_at: 'Fri Nov 13 2020 09:25:31 GMT+0800 (China Standard Time)',
updated_at: 'Fri Nov 13 2020 09:25:31 GMT+0800 (China Standard Time)',
nullValue: null,
undefinedValue: undefined, // JSON.stringify 不会序列化 undefined
symbolValue: Symbol('TypeScript'), // JSON.stringify 不会序列化 Symbol
arrowLog: () => console.log(this), // JSON.stringify 不会序列化 箭头函数
funLog: function() { console.log(this) }, // JSON.stringify 不会序列化 函数
}

/**
* Note 1 : undefined、Symbol、任何函数作为对象的属性值时,JSON.stringify 不对其(忽略)进行序列化
* {
* "_id":"id",
* "productCode":"P28120068",
* "productDetail":{"
* name":"Star Insure",
* "price":1200,
* "amount":1000000
* },
* "created_at":"Fri Nov 13 2020 09:25:31 GMT+0800 (China Standard Time)",
* "updated_at":"Fri Nov 13 2020 09:25:31 GMT+0800 (China Standard Time)",
* "nullValue":null
* }
*/
console.log(JSON.stringify(someObj));

Note 2

JSON.stringify 方法对数组进行序列化时,如果数组元素的值是 undefinedSymbol 值或任意函数时,会被序列化为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const someArr = [
null,
undefined, // JSON.stringify 将数组中的 undefined 序列化为 null
Symbol('TypeScript'), // JSON.stringify 将数组中的 Symbol 序列化为 null
() => console.log(this), // JSON.stringify 将数组中的 箭头函数 序列化为 null
function() { console.log(this) } // JSON.stringify 将数组中的 函数 序列化为 null
]

/**
* Note 2 : undefined, Symbol, 任意函数作为数组的元素时,JSON.stringify 将其序列化为 null
* [null,null,null,null,null]
*/
console.table(JSON.stringify(someArr));

Note 3

如果 undefinedSymbol 值或任意函数作为单独的值被 JSON.stringify 序列化为 undefined

1
2
3
4
5
6
/**
* Note 3 : undefined, Symbol, 任意函数作为单独的值时,JSON.stringify 序列化会返回 undefined
*/
console.log(JSON.stringify(undefined)); // undefined
console.log(JSON.stringify(Symbol('TypeScript'))); // undefined
console.log(JSON.stringify(() => console.log(this))); // undefined

Note 4

如果目标值有 toJSON 方法,则 toJSON 来决定 JSON.stringify 返回的值。

1
2
3
4
5
console.log(JSON.stringify({
name: 'Jimmy',
age: 20,
toJSON: () => 'this is a toJSON function',
})); // this is a toJSON function

Note 5

NaN, InInfinitynull 会被JSON.stringify 序列化为 null

1
2
3
4
5
6
7
8
9
/**
* NaN, InInfinity 及 null 会被 JSON.stringify 序列化为 null
*/
console.log(NaN); // NaN
console.log(JSON.stringify(NaN)); // null
console.log(Infinity); // Infinity
console.log(JSON.stringify(Infinity)); // null
console.log(null); // null
console.log(JSON.stringify(null)); // null

Note 6

数字、字符串和布尔值的包装值在被 JSON.stringify 序列化时会解包成对应的基础值。

1
2
3
const packArr = [ new Number(1), new String('TypeScript'), new Boolean(false)];
console.log(packArr); // [ [Number: 1], [String: 'TypeScript'], [Boolean: false] ]
console.log(JSON.stringify(packArr)); // [1,"TypeScript",false]

Note 7

JSON.stringify 在序列化对象时,仅会序列化可枚举的属。

1
2
3
4
5
const enumerableObj = Object.create(null, {
name: { value: 'jack', enumerable: true },
age: { value: 20, enumerable: false }
});
console.log(JSON.stringify(enumerableObj)); // { name: 'jack' }

Note 8

如果 JSON.stringify 的目标对象发生循环引用时,会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 对于相互引用的对象,会报错
* TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'refA' -> object with constructor 'Object'
--- property 'refB' closes the circle
*/
const objA = {
name: 'Object A',
refB: null,
};

const objB = {
name: 'Object B',
refA: objA,
};

objA.refB = objB;

Note 9

Symbol 值为键的属性值,在被 JSON.stringify 序列化时会被忽略。

1
2
3
4
5
6
7
8
9
/**
* 以 Symbol 值为键的属性值,在被 JSON.stringify 序列化时会被忽略
*/
const symbolObj = {
name: 'Symbol',
[Symbol.for('Age')]: 20,
};
console.log(symbolObj); // { name: 'Symbol', [Symbol(Age)]: 20 }
console.log(JSON.stringify(symbolObj)); // {"name":"Symbol"}

Note 10

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
const someObj = {
_id: 'id',
productCode: 'P28120068',
productDetail: {
name: 'Star Insure',
price: 1200,
amount: 1000000
},
created_at: 'Fri Nov 13 2020 09:25:31 GMT+0800 (China Standard Time)',
updated_at: 'Fri Nov 13 2020 09:25:31 GMT+0800 (China Standard Time)',
nullValue: null,
undefinedValue: undefined, // JSON.stringify 不会序列化 undefined
symbolValue: Symbol('TypeScript'), // JSON.stringify 不会序列化 Symbol
arrowLog: () => console.log(this), // JSON.stringify 不会序列化 箭头函数
funLog: function() { console.log(this) }, // JSON.stringify 不会序列化 函数
}

/**
* 当 JSON.stringify 的第二个参数是函数时, 可单独对某个值进行处理
*/
console.log(JSON.stringify(someObj, (k, v) => {
console.log(v)
if (typeof v === 'undefined') return 'undefined';
return JSON.stringify(v);
}));

JSON.stringify 的第二个参数是数组时,可在数组中加入属性名,来规定返回哪些属性的序列化值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const someObj = {
_id: 'id',
productCode: 'P28120068',
productDetail: {
name: 'Star Insure',
price: 1200,
amount: 1000000
},
created_at: 'Fri Nov 13 2020 09:25:31 GMT+0800 (China Standard Time)',
updated_at: 'Fri Nov 13 2020 09:25:31 GMT+0800 (China Standard Time)',
nullValue: null,
undefinedValue: undefined, // JSON.stringify 不会序列化 undefined
symbolValue: Symbol('TypeScript'), // JSON.stringify 不会序列化 Symbol
arrowLog: () => console.log(this), // JSON.stringify 不会序列化 箭头函数
funLog: function() { console.log(this) }, // JSON.stringify 不会序列化 函数
}

/**
* 当 JSON.stringify 的第二个参数是数组时,可在数组中加入属性名,来规定返回哪些属性的序列化值
* 但当值是 undefined、Symbol、任何函数时无效
*/
console.log(JSON.stringify(someObj, ['productCode', 'arrowLog'])); // { productCode: 'P28120068' }