本文详细解析JavaScript中对象与数组的赋值、浅拷贝及深拷贝机制。通过对比Object.assign、concat、slice等方法的局限性,阐述JSON.parse与递归实现深拷贝的原理及处理函数的注意事项,帮助开发者准确掌握内存引用关系。
本文通过具体实例,详细解析JavaScript中的赋值操作、浅拷贝以及深拷贝(涵盖数组和对象)的实现原理与区别。
浅拷贝与深拷贝的概念仅适用于Object和Array这类引用数据类型。
浅拷贝
浅拷贝仅复制基本类型的数据值。对于引用类型数据,复制后仍共享同一内存地址,这种拷贝方式被称为浅拷贝(或浅复制)。
浅拷贝仅复制指向对象的指针,而不复制对象本身,导致新旧对象共享同一块内存空间。相比之下,深拷贝会创建一个完全独立的新对象,新对象与原对象不共享内存,修改新对象不会影响原对象。
赋值与浅拷贝的区别
将对象赋值给新变量时,实际复制的是栈内存中的地址引用,而非堆内存中的数据。这意味着两个变量指向同一个存储空间,对任一变量的修改都会反映在另一个变量上,二者保持联动。
浅拷贝创建一个新对象,并精确复制原始对象属性值的一份副本。若属性为基本类型,则复制其值;若属性为引用类型(内存地址),则复制该地址。因此,修改其中一个对象引用的地址,会影响另一个对象。默认的对象拷贝构造函数仅执行浅拷贝,即逐个成员拷贝,仅复制对象空间而不复制其资源。
通过以下两个示例,对比赋值操作与浅拷贝对原对象产生的不同影响。
// 对象赋值
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

// 浅拷贝
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj3 = shallowCopy(obj1);
obj3.name = "lisi";
obj3.language[1] = ["二","三"];
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
console.log('obj1',obj1)
console.log('obj3',obj3)

在上述示例中,obj1为原始数据,obj2通过赋值操作获得,obj3通过浅拷贝获得。我们可以清晰观察到对原始数据的影响,具体对比如下表所示:

1、Object.assign()
Object.assign() 方法可将任意多个源对象自身的可枚举属性拷贝至目标对象,并返回目标对象。然而,Object.assign() 执行的是浅拷贝,它拷贝的是对象属性的引用而非对象本身。
var obj = { a: {a: "kobe", b: 39} };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "wade";
console.log(obj.a.a); //wade
注意:当对象仅有一层结构时,此方法表现为深拷贝。
let obj = {
username: 'kobe'
};
let obj2 = Object.assign({},obj);
obj2.username = 'wade';
console.log(obj);//{username: "kobe"}
2、Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2=arr.concat();
arr2[2].username = 'wade';
console.log(arr)
修改新对象会同步影响原对象:

3、Array.prototype.slice()
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr);
同样,修改新对象也会同步影响原对象:

关于Array的slice和concat方法的补充说明:这两个方法均不修改原数组,而是返回一个包含原数组元素浅复制的新数组。
原数组元素的拷贝遵循以下规则:
-
若元素为对象引用(非实际对象实例),slice会拷贝该引用至新数组。此时,两个数组中的引用指向同一对象。若被引用的对象发生改变,新数组和原数组中的该元素也会随之改变。
-
对于字符串、数字及布尔值(非String、Number或Boolean对象实例),slice会拷贝这些值至新数组。在新数组中修改这些基本类型的字符串、数字或布尔值,不会影响原数组。
上述描述可能较为晦涩,我们通过修改上述示例进行具体说明:
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[1] = 2
console.log(arr,arr3);

4.ES6
let {...b} = a; let [...b] = a; //当object只有一层的时候,是深拷贝
深拷贝
深拷贝是指在计算机中开辟一块全新的内存地址用于存放复制的对象。(即对属性中所有引用类型的值,递归遍历直到遇到基本类型的值为止。)
实现方式
1、JSON.parse(JSON.stringify())
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)

原理:利用JSON.stringify将对象转换为JSON字符串,再通过JSON.parse()将字符串解析回对象。这一来一回的过程生成了新对象,且新对象会开辟新的栈空间,从而实现深拷贝。
虽然该方法可实现数组或对象的深拷贝,但无法处理函数。
let arr = [1, 3, {
username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)

这是因为 JSON.stringify() 方法用于将JavaScript值(对象或数组)转换为JSON字符串,它不接受函数作为输入。
2、手写递归方法
递归方法实现深度克隆的原理是:遍历对象和数组,直到内部均为基本数据类型,然后再进行复制,从而实现深度拷贝。
//定义检测数据类型的功能函数
function checkedType(target) {
return Object.prototype.toString.call(target).slice(8, -1)
}
//实现深度克隆---对象/数组
function clone(target) {
//判断拷贝的数据类型
//初始化变量result 成为最终克隆的数据
let result, targetType = checkedType(target)
if (targetType === 'object') {
result = {}
} else if (targetType === 'Array') {
result = []
} else {
return target
}
//遍历目标数据
for (let i in target) {
//获取遍历数据结构的每一项值。
let value = target[i]
//判断目标结构里的每一值是否存在对象/数组
if (checkedType(value) === 'Object' ||
checkedType(value) === 'Array') { //对象/数组里嵌套了对象/数组
//继续遍历获取到value值
result[i] = clone(value)
} else { //获取到value值是基本的数据类型或者是函数。
result[i] = value;
}
}
return result
}
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《JavaScript数组操作技巧总结》、《JavaScript遍历算法与技巧总结》、《javascript面向对象入门教程》、《JavaScript数学运算用法总结》、《JavaScript数据结构与算法技巧总结》及《JavaScript错误与调试技巧总结》
希望本文所述对大家JavaScript程序设计有所帮助。