本文深入解析JavaScript中的深浅拷贝机制。首先区分基本与引用数据类型,随后通过示例详解浅拷贝与深拷贝的区别。重点对比JSON.stringify、递归实现、数组方法(concat/slice)及对象方法(Object.assign/展开运算符)的拷贝行为,揭示其仅处理第一层或忽略特殊类型的局限性,为开发者提供准确的拷贝方案选择依据。
JavaScript深浅拷贝基础概念
在探讨JavaScript中的深浅拷贝概念前,有必要先明确JS的数据类型分类,即基本数据类型与引用数据类型。深浅拷贝这一概念并不适用于基本数据类型,它专门针对引用数据类型展开讨论。
浅拷贝机制与示例
一、浅拷贝
所谓浅拷贝,仅复制了对象的引用地址,而未复制其实际值。在JavaScript中,最基础的浅拷贝方式便是使用“=”赋值操作符。
var obj1 = {
a:1,
b:[2,3,4],
c:{name:'tanj'},
fun:function(){
console.log('fun')
}
}
var obj2 = obj1
obj2.a = 666 /* 修改obj2的值,obj1的值也随之改变 */
console.log(obj1) /* {a: 666, b: Array(3), c: {…}, fun: ƒ} */
在上述代码示例中,当我们修改obj2的属性值时,obj1的对应属性值也会随之改变。这证明了使用“=”赋值操作符仅仅实现了浅拷贝,两个变量指向同一内存地址。
深拷贝实现方式
二、深拷贝
深拷贝则是对目标对象进行完全独立的复制。经过深拷贝操作后,生成的新对象与原对象在内存中完全分离,互不影响。
JSON序列化方法
1. 利用JSON.stringify与JSON.parse方法
JSON.stringify函数的作用是将一个JavaScript对象或值转换为JSON格式的字符串;
JSON.parse函数则负责将一个JSON字符串解析并转换回JavaScript对象。
var obj1 = {
a:1,
b:[2,3,4],
c:{name:'tanj'},
}
var obj2 = JSON.parse(JSON.stringify(obj1))
obj2.a = 12
console.log(obj1) /* {a: 1, b: Array(3), c: {…}} */
通过上述代码测试,修改obj2的属性并未影响obj1中的对应属性值。这表明利用JSON.parse结合JSON.stringify可以实现深拷贝的效果。
然而,这种方法真的完美无缺吗?让我们通过下面的例子来验证其局限性。
var obj1 = {
a:1,
b:[2,3,4],
c:{name:'tanj'},
fun:function(){
console.log('fun')
}
}
var obj2 = JSON.parse(JSON.stringify(obj1))
obj2.a = 12
console.log(obj1) /* {a: 1, b: Array(3), c: {…}, fun: ƒ} */
console.log(obj2) /* {a: 12, b: Array(3), c: {…}} */
在转换后的obj2中,原本存在的fun属性消失了。这是因为JSON.stringify在转换过程中会自动忽略值为undefined、function或symbol的属性。因此,当对象包含这些类型的属性时,该方法无法正确实现深拷贝。
递归实现深拷贝
2. 递归
function deepClone(source){
if(!isObject(source)) return source
var newObj = source instanceof Array? []:{}
for(let key in source){
if(source.hasOwnProperty(key)){
newObj[key] = isObject(source[key])?deepClone(source[key]):source[key]
}
}
return newObj
}
function isObject(x) {
return typeof x === 'object' && x != null
}
下面通过代码测试上述递归方法的实际效果:
var obj1 = {
a:1,
b:[2,3,4],
c:{name:'tanj'},
fun:function(){
console.log('fun')
}
}
var obj2 = deepClone(obj1)
obj2.a = 12
console.log(obj1) /* {a: 1, b: Array(3), c: {…}, fun: ƒ} */
从示例结果可见,修改obj2中a属性的值后,obj1中的a属性值保持不变。通过递归遍历对象属性,我们可以实现真正的深拷贝。
需要注意的是:上述递归方法尚未解决循环引用的问题。
var obj1 = {}
obj1.a = obj1
var obj2 = deepClone(obj1) /* 报错,栈溢出 */
console.log(obj2)
至于如何解决循环引用问题以及实现对Symbol类型属性的拷贝,将在后续内容中进一步完善。
其他常用拷贝方法分析
三、其他拷贝方法
数组内置方法
1. 数组中的concat()和slice()方法
我们知道数组提供了concat和slice两个方法,它们可以复制数组并返回一个新数组。以concat方法为例进行说明。
var arr = [1,2,3]
var arr2 = arr.concat()
arr2[2]=4
console.log(arr) /* [1, 2, 3] */
console.log(arr2) /* [1, 2, 4] */
改变arr2的值并未影响到原数组arr,但这是否意味着实现了数组的深拷贝?暂不下结论,结合下面的例子进一步分析。
var arr = [1,2,3,[4,5,6],{a:7}]
var arr2 = arr.concat()
arr2[3] = 444
arr2[4].a=8
console.log(arr) /* [1,2,3,[4,5,6],{a:8}] */
console.log(arr2) /* [1,2,3,444,{a:8}] */
我们直接修改arr2[3],并未引起arr的改变,但修改arr2[4].a时,arr中相应元素随之改变。对arr2数组元素直接赋值(如arr2[0]=***,arr2[1]=***,arr2[3]=***)不影响原数组arr,但修改数组中的[3,4,5]或{a:7}时,会造成原数组arr的改变。
结论:concat()方法仅对数组的第一层进行了深拷贝,嵌套层级仍共享引用。
同样地,数组的slice()方法也仅对数组第一层进行了深拷贝,深层嵌套对象依然共享引用。
对象展开与assign方法
2. Object.assign()和...展开运算符
var obj1 = {
a:1,
b:[2,3,4],
c:{name:'tanj'}
}
var obj2 = {...obj1}
obj2.a = 666
obj2.c.name = 'xinxin'
console.log(obj1) /* {a:1,b:[2,3,4],c:{name:'xinxin'}} */
可以看到,利用...展开运算符实现的是对象第一层的深拷贝,其后的嵌套属性依然拷贝的是引用值。
我们可以尝试使用Object.assign()方法:
var obj1 = {
a:1,
b:[2,3,4],
c:{name:'tanj'}
}
var obj2 = {}
Object.assign(obj2,obj1)
obj2.a = 666
obj2.b[0] = 0
console.log(obj1) /* {a:1,b:[0,3,4],c:{name:'tanj'} */
同样地,该方法只对对象的第一层进行了深拷贝。如果源对象的属性值(例如obj1中的b)是一个指向数组的引用,obj2只会拷贝该引用。因此,当改变obj2中b所指向的数组内容时,obj1中的对应数组也会发生改变。
我们可以手动实现一个类似效果,仅对对象的第一层进行深拷贝:
function shallowClone(source) {
const newObj = source.constructor === Array ? [] : {}
for (let keys in source) {
if (source.hasOwnProperty(keys)) {
newObj[keys] = source[keys]
}
}
return newObj
}
以上便是对JavaScript中深浅拷贝的详细分析,更多关于JavaScript 深浅拷贝的资料请关注本站其它相关文章!