JavaScript深浅拷贝的区别与实现

一直想梳理下工作中经常会用到的深拷贝的内容,然而遍览了许多的文章,却发现对深拷贝并没有一个通用的完美实现方式。因为对深拷贝的定义不同,实现时的edge case过多,在深拷贝的时候会出现循环引用等问题,导致JS内部并没有实现深拷贝,但是我们可以来探究一下深拷贝到底有多复杂,各种实现方式的优缺点,同时参考下常用库对其的实现。

引用类型

之所以会出现深浅拷贝的问题,实质上是由于JS对基本类型和引用类型的处理不同。基本类型指的是简单的数据段,而引用类型指的是一个对象,而JS不允许我们直接操作内存中的地址,也是不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。

在复制时也是一样,如果我们复制一个基本类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用类型时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西并不是对象本身,而是指向该对象的指针。所以我们复制引用类型后,两个变量其实指向同一个对象,改变其中一个对象,会影响到另外一个。

var num =
10
;
var obj =
{
name:
‘Nicholas’
}
var num2 = num;
var obj2 = obj;
obj.
name
=
‘Lee’
;
obj2.
name
;
// ‘Lee’

JavaScript深浅拷贝的区别与实现

浅拷贝

如果我们要复制对象的所有属性都不是引用类型时,可以使用浅拷贝,实现方式是遍历并复制,后返回新的对象。

function
shallowCopy
(
obj
)
{
var copy =
{}
;
// 只复制可遍历的属性
for
(
key
in
obj
)
{
// 只复制本身拥有的属性
if
(
obj.
hasOwnProperty
(
key
))
{
copy
[
key
]
= obj
[
key
]
;
}
}
return
copy;
}

如上面所说,我们使用浅拷贝会复制所有引用对象的指针,而不是具体的值,所以使用时一定要明确自己的需求,同时,浅拷贝的实现也是简单的。

JS内部实现了浅拷贝,如Object.assign(),其中个参数是我们终复制的目标对象,后面的所有参数是我们的即将复制的源对象,支持对象或数组,一般调用的方式为

var newObj = Object.
assign
({}
, originObj
)
;

深拷贝

如果我们需要复制一个拥有所有属性和方法的新对象,要用到深拷贝,JS并没有内置深拷贝方法,主要是因为:

  1. 深拷贝怎么定义?我们怎么处理原型?怎么区分可拷贝的对象?原生DOM/BOM对象怎么拷贝?函数是新建还是引用?这些edge case太多导致我们无法统一概念,造出大家都满意的深拷贝方法来。
  2. 内部循环引用怎么处理,是不是保存每个遍历过的对象列表,每次进行对比,然后再造一个循环引用来?这样带来的性能消耗可以接受吗。

解释一些常见的问题概念,防止有些同学不明白我们在讲什么。比如循环引用:

var obj =
{}
;
obj.
b
= obj;

这样当我们深拷贝obj对象时,会循环的遍历b属性,直到栈溢出。 我们的解决方案为建立一个[],每次遍历对象进行比较,如果[]中已存在,则证明出现了循环引用或者相同引用,我们直接返回该对象已复制的引用即可:

let hasObj =
[]
;
function
referCopy
(
obj
)
{
let copy =
{}
;
hasObj.
push
(
obj
)
;
for
(
let i
in
obj
)
{
if
(
typeof obj
[
i
]
===
‘object’
)
{
let index = hasObj.
indexOf
(
obj
[
i
])
;
if
(
index
>
-1
)
{
console.
log
(
‘存在循环引用或属性引用了相同对象’
)
;
// 如果已存在,证明引用了相同对象,那么无论是循环引用还是重复引用,我们返回引用可以了
copy
[
i
]
= hasObj
[
index
]
;
}
else
{
copy
[
i
]
=
referCopy
(
obj
[
i
])
;
}
}
else
{
copy
[
i
]
= obj
[
i
]
;
}
}
return
copy;
}

处理原型和区分可拷贝的对象:我们一般使用function.prototype指代原型,使用obj.__proto__指代原型链,使用enumerable属性表示是否可以被for ... in等遍历,使用hasOwnProperty来查询是否是本身元素。在原型链和可遍历属性和自身属性之间存在交集,但都不相等,我们应该如何判断哪些属性应该被复制呢?

函数的处理:函数拥有一些内在属性,但我们一般不修改这些属性,所以函数一般直接引用其地址即可。但是拥有一些存取器属性的函数我们怎么处理?是复制值还是复制存取描述符?

var obj =
{
age:
10
,
get
age
()
{
return
this
.
age
;
}
,
set
age
(
age
)
{
this
.
age
= age;
}
}
;
var obj2 = $.
extend
(
true
,
{}
, obj
)
;
obj2;
// {age: 10}

这个是我们想要的结果吗?大部分场景下不是吧,比如我要复制一个已有的Vue对象。当然我们也有解决方案:

function
copy
(
obj
)
{
var copy =
{}
;
for
(
var i
in
obj
)
{
let desc = Object.
getOwnPropertyDescriptor
(
obj, i
)
;
// 检测是否为存取描述符
if
(
desc.
set
||
desc.
get
)
{
Object.
defineProperty
(
copy, i,
{
get: desc.
get
,
set: desc.
set
,
configuarable: desc.
configuarable
,
enumerable:
true
})
;
// 否则为数据描述符,则复用下面的深拷贝方法,此处简写
}
else
{
copy
[
i
]
= obj
[
i
]
;
}
}
return
copy;
}

虽然边界条件很多,但是不同的框架和库都对该方法进行了实现,只不过定义不同,实现方式也不同,如jQuery.extend()只复制可枚举的属性,不继承原型链,函数复制引用,内部循环引用不处理。而lodash实现的更为,它实现了结构化克隆算法。 该算法的优点是:

  1. 可以复制 RegExp 对象。
  2. 可以复制 Blob、File 以及 FileList 对象。
  3. 可以复制 ImageData 对象。CanvasPixelArray 的克隆粒度将会跟原始对象相同,并且复制出来相同的像素数据。
  4. 可以正确的复制有循环引用的对象

依然存在的缺陷是:

  1. Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
  2. 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERROR 异常。
  3. 对象的某些特定参数也不会被保留
    • RegExp 对象的 lastIndex 字段不会被保留
    • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
    • 原形链上的属性也不会被追踪以及复制。

我们先来看看常规的深拷贝,它跟浅拷贝的区别在于,当我们发现对象的属性是引用类型时,进行递归遍历复制,直到遍历完所有属性:

var deepClone =
function
(
currobj
){
if
(
typeof currobj !==
‘object’
){
return
currobj;
}
if
(
currobj instanceof Array
){
var newobj =
[]
;
}
else
{
var newobj =
{}
}
for
(
var key
in
currobj
){
if
(
typeof currobj
[
key
]
!==
‘object’
){
// 不是引用类型,则复制值
newobj
[
key
]
= currobj
[
key
]
;
}
else
{
// 引用类型,则递归遍历复制对象
newobj
[
key
]
=
deepClone
(
currobj
[
key
])
}
}
return
newobj
}

这个的主要问题是不处理循环引用,不处理对象原型,函数依然是引用类型。上面描述过的复杂问题依然存在,可以说是简陋但是日常工作够用的深拷贝方式。

另外还有一种方式是使用JSON序列化,巧妙但是限制更多:

// 调用JSON内置方法先序列化为字符串再解析还原成对象
newObj = JSON.
parse
(
JSON.
stringify
(
obj
))
;

JSON是一种表示结构化数据的格式,只支持简单值、对象和数组三种类型,不支持变量、函数或对象实例。所以我们工作中可以使用它解决常见问题,但也要注意其短板:函数会丢失,原型链会丢失,以及上面说到的所有缺陷。

库实现

上面的两种方式可以满足大部分场景的需求,如果有更复杂的需求,可以自己实现。现在我们可以看一些框架和库的解决方案,下面拿经典的jQuery和lodash的源码看下,它们的优缺点上面都说过了:

jQuery.extend()

// 进行深度复制,如果个参数为true则深度复制,如果目标对象不合法,则抛弃并重构为{}空对象,如果只有一个参数则功能为扩展jQuery对象
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
// 个参数可以为true来确定进行深度复制
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
// 如果只有一个参数,扩展jQuery对象
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
// 阻止简单形式的循环引用
// var obj={}, obj2={a:obj}; $.extend(true, obj, obj2); 会形成复制的对象循环引用obj
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;
}
;

lodash _.baseClone()

/**
* 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
(
iuffer
(
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, baseClone, 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
)
;
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;
}

参考资料

  1. 知乎 JS的深拷贝和浅拷贝: https://www.zhihu.com/questio…
  2. Javascript之深拷贝: https://aepkill.github.io/201…
  3. js对象克隆之谜:http://b-sirius.me/2017/08/26…
  4. 知乎 JS如何完整实现深度Clone对象:https://www.zhihu.com/questio…
  5. github lodash源码:https://github.com/lodash/lod…
  6. MDN 结构化克隆算法:https://developer.mozilla.org…
  7. jQuery v3.2.1 源码
  8. JavaScript程序设计 第4章(变量、作用域和内存问题)、第20章(JSON)

本文章为一只派大星站长顾凌编写,更多文章请到一只派大星网站观看。

给TA打赏
共{{data.count}}人
人已打赏
未分类

1. 自媒体活动策划:高效变现技巧分享 2. 如何通过活动策划实现自媒体变现 3. 自媒体变现秘籍:活动策划全攻略 4. 策划优质活动,助力自媒体快速变现 5. 自媒体人必看:活动策划变现指南

2025-5-15 16:42:12

未分类

高通骁龙7+Gen2性能评测:架构解析与体验分析

2025-5-15 16:42:16

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索