logo

javascript中的深浅拷贝

深拷贝(deep copy)与 浅拷贝(shallow copy)的区别在于对复合数据类型的复制,如果只是复制复合数据引用就是浅拷贝,如果是重新在新的内存空间中完全复制复杂对象则是深拷贝。
深拷贝主要目的用途就是拷贝一份不受原来对象影响的对象,用于数据传递(postMessage)或者数据持久化(IndexDB)等等。

浅拷贝(shallow copy)

浅拷贝很好实现,其特点就是共享对象变化。

对象拷贝

Object.assign()

ES6 中拓展的拓展方法,可以将其他对象属性复制到拓展到目标对象上。只能拷贝对象自身属性,不能拷贝原型链上的属性(继承属性)

1
2
3
4
let a = { a: 1, b: 1, c: [1, 2, 3] };
let b = Object.assign({}, a);
a.c.push(4);
b.c; //[1,2,3,4]

搞一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function shallowCopy(obj) {
let copyObj = {};
/*不包含原型链属性*/
Object.keys(obj).forEach(item => {
copyObj[item] = obj[item];
});

/* for(var key in obj){
if(obj.hasOwnProperty(prop)){ //不包含原型链属性
copyObj[key] = obj[key]
}
}
*/

return copyObj;
}

数组拷贝

数组实例的 slice,concat 等等方法都可实现。

1
2
let a = [1, 2, 3];
let b = a.slice();

深拷贝(deep copy)

深拷贝相对浅拷贝相对要复杂的许多,主要是 edge case 非常多,有很大原因是因为我们不知道我们拷贝的属性值是什么类型:

  • 循环引用对象的处理
  • RegExp/Map/Set/Array 等等的原生对象处理
  • Symbol 类型数据的拷贝
  • DOM,BOM 对象处理
  • Function 处理
  • 对象原型链的处理
  • 不可枚举属性的处理
  • so on…

jQuery.extend([deep],target,object1[,object2…])

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
jQuery.extend = jQuery.fn.extend = function() {
var options,
name,
src,
copy,
copyIsArray,
clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;

/* Handle a deep copy situation */
if (typeof target === "boolean") {
deep = target;

/* Skip the boolean and the target */
target = arguments[i] || {};
i++;
}

/* Handle case when target is a string or something (possible in deep copy) */
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {};
}

/* Extend jQuery itself if only one argument is passed */
if (i === length) {
target = this;
i--;
}

for (; i < length; i++) {
/* Only deal with non-null/undefined values */
if ((options = arguments[i]) != null) {
/* Extend the base object */
for (name in options) {
src = target[name];
copy = options[name];

/* Prevent never-ending loop */
if (target === copy) {
/*避免拷贝自身*/
continue;
}

/* Recurse if we're merging plain objects or arrays */
if (
deep &&
copy &&
(jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))
) {
if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];
} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}

/* Never move original objects, clone them */
target[name] = jQuery.extend(deep, clone, copy); //递归遍历拷贝

/* Don't bring in undefined values */
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
/* Return the modified object */
return target;
};

从上面代码我们可以了解到 jQuery 实现的深复制并没有去处理 edge cases,个人认为主要是深克隆的应用场景就很有限,jQuery 只对 planinObject 进行深拷贝,各种 edge cases 都没有被处理,例如原生对象只是浅复制。

_.deepClone

部分核心代码如下:

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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
var Stack = require("./_Stack"),
arrayEach = require("./_arrayEach"),
assignValue = require("./_assignValue"),
baseAssign = require("./_baseAssign"),
baseAssignIn = require("./_baseAssignIn"),
cloneBuffer = require("./_cloneBuffer"),
copyArray = require("./_copyArray"),
copySymbols = require("./_copySymbols"),
copySymbolsIn = require("./_copySymbolsIn"),
getAllKeys = require("./_getAllKeys"),
getAllKeysIn = require("./_getAllKeysIn"),
getTag = require("./_getTag"),
initCloneArray = require("./_initCloneArray"),
initCloneByTag = require("./_initCloneByTag"),
initCloneObject = require("./_initCloneObject"),
isArray = require("./isArray"),
isBuffer = require("./isBuffer"),
isMap = require("./isMap"),
isObject = require("./isObject"),
isSet = require("./isSet"),
keys = require("./keys");

/** Used to compose bitmasks for cloning. */
var CLONE_DEEP_FLAG = 1,
CLONE_FLAT_FLAG = 2,
CLONE_SYMBOLS_FLAG = 4;

/** `Object#toString` result references. */
var argsTag = "[object Arguments]",
arrayTag = "[object Array]",
boolTag = "[object Boolean]",
dateTag = "[object Date]",
errorTag = "[object Error]",
funcTag = "[object Function]",
genTag = "[object GeneratorFunction]",
mapTag = "[object Map]",
numberTag = "[object Number]",
objectTag = "[object Object]",
regexpTag = "[object RegExp]",
setTag = "[object Set]",
stringTag = "[object String]",
symbolTag = "[object Symbol]",
weakMapTag = "[object WeakMap]";

var arrayBufferTag = "[object ArrayBuffer]",
dataViewTag = "[object DataView]",
float32Tag = "[object Float32Array]",
float64Tag = "[object Float64Array]",
int8Tag = "[object Int8Array]",
int16Tag = "[object Int16Array]",
int32Tag = "[object Int32Array]",
uint8Tag = "[object Uint8Array]",
uint8ClampedTag = "[object Uint8ClampedArray]",
uint16Tag = "[object Uint16Array]",
uint32Tag = "[object Uint32Array]";

/** Used to identify `toStringTag` values supported by `_.clone`. */
var cloneableTags = {};
cloneableTags[argsTag] = cloneableTags[arrayTag] = cloneableTags[
arrayBufferTag
] = cloneableTags[dataViewTag] = cloneableTags[boolTag] = cloneableTags[
dateTag
] = cloneableTags[float32Tag] = cloneableTags[float64Tag] = cloneableTags[
int8Tag
] = cloneableTags[int16Tag] = cloneableTags[int32Tag] = cloneableTags[
mapTag
] = cloneableTags[numberTag] = cloneableTags[objectTag] = cloneableTags[
regexpTag
] = cloneableTags[setTag] = cloneableTags[stringTag] = cloneableTags[
symbolTag
] = cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = cloneableTags[
uint16Tag
] = cloneableTags[uint32Tag] = true;
cloneableTags[errorTag] = cloneableTags[funcTag] = cloneableTags[
weakMapTag
] = false;

/**
* The base implementation of `_.clone` and `_.cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone.
* @param {boolean} bitmask The bitmask flags.
* 1 - Deep clone
* 2 - Flatten inherited properties
* 4 - Clone symbols
* @param {Function} [customizer] The function to customize cloning.
* @param {string} [key] The key of `value`.
* @param {Object} [object] The parent object of `value`.
* @param {Object} [stack] Tracks traversed objects and their clone counterparts.
* @returns {*} Returns the cloned value.
*/
function baseClone(value, bitmask, customizer, key, object, stack) {
var result,
isDeep = bitmask & CLONE_DEEP_FLAG,
isFlat = bitmask & CLONE_FLAT_FLAG,
isFull = bitmask & CLONE_SYMBOLS_FLAG;

if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value);
}
if (result !== undefined) {
return result;
}
if (!isObject(value)) {
return value;
}
var isArr = isArray(value);
if (isArr) {
result = initCloneArray(value);
if (!isDeep) {
return copyArray(value, result);
}
} else {
var tag = getTag(value),
isFunc = tag == funcTag || tag == genTag;

if (isBuffer(value)) {
return cloneBuffer(value, isDeep);
}
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
result = isFlat || isFunc ? {} : initCloneObject(value);
if (!isDeep) {
return isFlat
? copySymbolsIn(value, baseAssignIn(result, value))
: copySymbols(value, baseAssign(result, value));
}
} else {
if (!cloneableTags[tag]) {
return object ? value : {};
}
result = initCloneByTag(value, tag, isDeep);
}
}
/* Check for circular references and return its corresponding clone.
利用栈来检测对象的循环引用 */
stack || (stack = new Stack());
var stacked = stack.get(value);
if (stacked) {
return stacked;
}
stack.set(value, result);

if (isSet(value)) {
value.forEach(function(subValue) {
result.add(
baseClone(subValue, bitmask, customizer, subValue, value, stack)
);
});

return result;
}

if (isMap(value)) {
value.forEach(function(subValue, key) {
result.set(
key,
baseClone(subValue, bitmask, customizer, key, value, stack)
);
});

return result;
}

var keysFunc = isFull
? isFlat
? getAllKeysIn
: getAllKeys
: isFlat
? keysIn
: keys;

var props = isArr ? undefined : keysFunc(value);
arrayEach(props || value, function(subValue, key) {
if (props) {
key = subValue;
subValue = value[key];
}
/* Recursively populate clone (susceptible to call stack limits).*/
/* 递归遍历复制 */
assignValue(
result,
key,
baseClone(subValue, bitmask, customizer, key, value, stack)
);
});
return result;
}

module.exports = baseClone;

从上面代码我们可以了解到 lodash 的拷贝考虑了更多的情况,不仅仅考虑到循环引用,各种原生对象的拷贝(采用结构化克隆算法 ),甚至提供了自定一参数来实现自定义拷贝策略,功能很强大。

JSON.parse()/JSON.stringify()

通过 JSON 对象我们可以加将 对象转换为 JSON 字符串,或者将 JSON 字符串转换为对象。
JSON 是一种语法,与 javascript 字面量对象有一定交集,但是 JSON 方法只适用于 JSON 格式,能转换的对象就需要符合 JSON 格式。
如果数据属性值为 Function,Symbol,非字面量的 Object…等类型会被忽略,详情见MDN JSON
如果对象中包含循环引用,则会抛出 TypeError: circular structure to JSON。

1
2
3
function copyByJSON(obj) {
return JSON.parse(JSON.stringfiy(obj));
}

搞一个

只拷贝 PlainObject

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
function deepClone(target) {
/* 建立被拷贝对象与拷贝对象建立联系,解决对象循环引用的问题 */
let objMap = new WeakMap();
let isPlainObject = obj =>
Object.prototype.toString.call(obj) === "[object Object]";

function clone(copyTarget) {
if (!isPlainObject(copyTarget)) {
return copyTarget;
}
/* 检查关联 */
if (objMap.has(copyTarget)) {
return objMap.get(copyTarget);
}

let copy = Array.isArray(copyTarget) ? [] : {};
/* 存储关联 */
objMap.set(copyTarget, copy);

for (let key in copyTarget) {
if (copyTarget.hasOwnProperty(key)) {
let value = copyTarget[key];
if (isPlainObject(value)) {
copy[key] = clone(value);
} else {
copy[key] = value;
}
}
}
return copy;
}

return clone(target);
}

var obj = {
a: 1
};
obj.self = obj;
var copyObj = deepClone(obj);
console.log(copyObj === copyObj.self);
//true

拷贝不可枚举属性

通过getOwnPropertyNames属性获取所有属性进行拷贝

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
function deepClone(target) {

let objMap = new WeakMap();
let isPlainObject = obj=>Object.prototype.toString.call(obj) === "[object Object]";

function clone(copyTarget) {
if (copyTarget === null || typeof copyTarget !== "object") {
return copyTarget;
}
if (objMap.has(copyTarget)) {
return objMap.get(copyTarget)
}

let copy = Array.isArray(copyTarget) ? [] : {}
objMap.set(copyTarget, copy)

Object.getOwnPropertyNames(copyTarget).forEach(key=>{
if (copyTarget.hasOwnProperty(key)) {
let value = copyTarget[key];
if (isPlainObject(value)) {
copy[key] = clone(value)
} else {
copy[key] = value;
}
}

})
return copy;
}
return clone(target);
}

var obj = {
a: 1,
}
obj.self = obj

Object.defineProperty(obj,'test',{enumerable:false,value:'test'})

var copyObj = deepClone(obj)

console.log(copyObj.test)
//test

RegExp,Date 等对象的拷贝

对原生对象处理可以重新新建一个值相同的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let isRegExp = reg=>Object.prototype.toString.call(reg)==='[object RegExp]' 
let isDate = date=>Object.prototype.toString.call(date) === '[object Date]'
function cloneRegExp(reg) {
var pattern = reg.valueOf();
var flags = "";
flags += pattern.global ? "g" : "";
flags += pattern.ignoreCase ? "i" : "";
flags += pattern.multiline ? "m" : "";
return new RegExp(pattern.source, flags);
}

function cloneDate(date){
return new Date(date.valueOf())
}

最后

从上面可以看到对象的深拷贝要考虑的edge case还有非常多,这里只是试着解决几种情况。
在实际应用上,非要用深拷贝的场景十分有限,往往使用深拷贝能解决的问题能有更好的解决方案。
但是清楚的了解深拷贝能增进我们的对javascript基础的理解掌握,Bye。

参考链接

JavaScript 如何完整实现深度 Clone 对象?
深入剖析 JavaScript 的深复制