全面聊聊JavaScript的浅拷贝和深拷贝
一、背景
首先我们可以看下面这段简单的代码:
var obj = {name:'程序猿',sex:'男'};
var arr = ['程序猿','程序媛'];
var copyobj = obj
copyobj .name = '设计狗'
console.log(obj) // {name: "设计狗", sex: "男"}
console.log(copyobj) //{name: "设计狗", sex: "男"}
console.log(copyobj === obj) //true
// 以上修改copyobj的时候也修改了obj
var copyarr = arr
copyarr [0] = '设计狗'
console.log(arr) // ["设计狗", "程序媛"]
console.log(copyarr) // ["设计狗", "程序媛"]
console.log(copyarr=== arr) //true
// 以上修改copyarr 的时候也修改了arr
var obj2 = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr2 = ['程序猿','程序媛',['男','女']];
var obj2 = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr2 = ['程序猿','程序媛',['男','女']];
var copyobj2 = obj2
copyobj2.msg.sex= '人妖'
console.log(obj2) // {name:'程序猿',msg:{sex:'人妖',age:'20'}};
console.log(copyobj2) //{name:'程序猿',msg:{sex:'人妖',age:'20'}};
console.log(copyobj2 === obj2) //true
// 以上修改copyobj2的时候也修改了obj2
var copyarr2 = arr2
copyarr2[2][0]= '人妖'
console.log(arr2) // ['程序猿','程序媛',['人妖','女']];
console.log(copyarr2) // ['程序猿','程序媛',['人妖','女']];
console.log(copyarr2 === arr2) //true
// 以上修改copyarr2 的时候也修改了arr2
显然,有时候我们需要克隆一份数据并修改,但是并不想影响原来的数据,特别是在现在前端使用双向数据绑定的情况下,经常有这样的需求,因此,浅拷贝和深拷贝就产生了。
二、关于栈内存和堆内存
了解JavaScript的浅拷贝和深拷贝之前,我们先了解下栈内存和堆内存。首先,任何程序的运行都是要占用内存的,而为了减少程序对内存的占用,运行程序的载体(比如浏览器)都会有个垃圾回收机制,这个机制把内存分成了栈内存与堆内存。
当我们声明了全局基础变量或者是执行一个普通的方法的时候,基础变量会直接占用栈内存,而方法会建立自己的一个局部栈内存来占用全局的栈内存,方法中也可以申明局部基础变量来占用自己的栈内存,当方法执行完毕则会释放所有基础变量占用的内存,当然,闭包等特殊的方法除外。
当我们声明了一个对象或者数组的时候,对象或者数组会被分成两部分进行内存存储,一部分是对象或数组在栈内存中的地址,或叫指针;另一部分是对象或者数组在堆内存中的值。地址(指针)指向对应的值。这样,当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。
比如下面两个声明在内存中是如图所示的:
var a = 5; var b = {name:'hao'}; var c = ['hao','feng']

二、关于JavaScript的数据类型
JavaScript的数据类型可分为基本类型和引用类型。
var a = 5;
var d = a;
console.log(d); //
d = 8;
console.log(d); //
console.log(a); //
上面的代码中,a是基本数据类型(Number), d是a的一个副本,它们两者都占有不同位置但相等的内存空间,只是它们的值相等,若改变其中一方,另一方不会随之改变。
2、对于引用类型,可根据一维对象数组跟多维对象数组分为浅拷贝和深拷贝
浅拷贝,可以简单理解为对整个一维对象进行复制或对多维数组对象中的某一值进行复制
深拷贝,可以简单理解为对整个数组对象进行复制,无论该数组对象有多少层级
(1)通过变量复制实现一维数组对象的浅拷贝或多维数组对象的浅拷贝
// 变量复制实现一维数组对象的浅拷贝
var obj = {name:'程序猿',sex:'男'};
var arr = ['程序猿','程序媛'];
var e = obj.name;
console.log(e); // 程序猿
e = '搬砖农';
var f = arr[0];
console.log(f); // 程序猿
f = '设计狗';
console.log(obj); // {name: "程序猿", sex: "男"}
console.log(e); // 搬砖农
console.log(arr); // ["程序猿", "程序媛"]
console.log(f); // 设计狗
(2)通过es6新增的Object.assign来复制对象
var obj = {name:'程序猿',sex:'男'};
var arr = ['程序猿','程序媛'];
var copyobj =Object.assign({}, obj);
var copyarr=Object.assign({}, arr);
console.log(obj); // {name:'程序猿',sex:'男'}
console.log(copyobj); //{name:'程序猿',sex:'男'}
console.log(obj===copyobj); // false,说明堆内存新克隆了值,在堆内存中,值的长相一致且占用大小一致,但所在位置不一致的值是不相等的(类似双胞胎)
console.log(obj.msg === copyobj.msg) // false
console.log(arr); // ['程序猿','程序媛'];
console.log(copyarr); // {0: "程序猿", 1: "程序媛"}
// tips由上可以看出Object.assgin用在数组上会将数据对象化,因此需要转化为数组,此时就基本不是整个数组的复制了
var arr2= []
for (var key in copyarr){
arr2.push(copyarr[key])
}
console.log(arr2) // ['程序猿','程序媛'];
console.log(arr2 === arr) // false 说明堆内存新建立了值并压入了属性值
(3)通过数组的slice()和concat()方法来复制数组
var arr = ['程序猿','程序媛'];
// var copyarr = arr.concat()
var copyarr = arr.slice()
console.log(copyarr)
console.log(copyarr === arr) // false,说明堆内存新复制了一份值
copyarr[0] = '设计狗'
console.log(copyarr) // ["设计狗", "程序媛"]
console.log(arr) // ['程序猿','程序媛']
(4)通过jquey中的$.extend({}, obj); 不做案例
基本实现浅拷贝的方式有以上几种,到目前为止都是以一维的数组对象进行拷贝,如果用以上方法对多维数组对象进行拷贝,会出现什么情况呢,我们可以看看。
// 变量复制
var obj = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr = ['程序猿','程序媛',['男','女']];
var e = obj.msg.sex;
console.log(e); // 男
e = '性别不详';
var f = arr[2][0];
console.log(f); // 男
f = '人妖';
console.log(obj); // {name:'程序猿',msg:{sex:'男',age:'20'}}
console.log(e); // 性别不详
console.log(arr); // ['程序猿','程序媛',['男','女']]
console.log(f); // 人妖 // 由上可以看出跟变量复制对一维数组的拷贝没有什么区别
// Object.assign
var obj = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr = ['程序猿','程序媛',['男','女']];
var copyobj =Object.assign({}, obj);
var copyarr=Object.assign({}, arr); console.log(obj); // {name:'程序猿',msg:{sex:'男',age:'20'}}
console.log(copyobj); //{name:'程序猿',msg:{sex:'男',age:'20'}}
console.log(obj.name===copyobj.name) // true,克隆了但是跟变量复制的性质一致,为true
console.log(obj.msg===copyobj.msg); // true,说明堆内存没有新克隆值
console.log(arr); // ['程序猿','程序媛',['男','女']]
console.log(copyarr); // {0: "程序猿", 1: "程序媛",2:[['男','女']]} var obj2 = {msg:{sex:'男',age:'20'},name:'程序猿'};
var copyobj2 =Object.assign({}, obj2);
console.log(copyobj2.msg === obj2.msg); // true,说明堆内存没有新克隆值
copyobj2.msg.sex = '性别不详'
console.log(copyobj2) // {msg:{sex:'性别不详',age:'20'},name:'程序猿'}
console.log(obj2) // {msg:{sex:'性别不详',age:'20'},name:'程序猿'};
// 数组的slice()与concat()
var arr = ['程序猿','程序媛',['男','女']];
// var copyarr = arr.concat()
var copyarr = arr.slice()
console.log(copyarr === arr) // false,说明外围最大的数组新复制了一份值
console.log(copyarr[2] === arr[2]) // true,说明堆内存没有新复制了一份值
copyarr[0] ='设计狗'
console.log(copyarr) // ['设计狗','程序媛',['男','女']];
console.log(arr) // ['程序猿','程序媛',['男','女']]; copyarr[2][0] = '人妖'
console.log(copyarr) // ['设计狗','程序媛',['人妖','女']];
console.log(arr) // ['程序猿','程序媛',['人妖','女']]; // 由上可以看出数组的slice()与concat()实现不了对多维数组的克隆
以上的操作如下图所示:

3、对于多维的数组对象,我们可以通过以下几种方式进行深拷贝
(1)使用JSON.parse(JSON.stringify(obj)),JOSN对象中的stringify把一个js对象序列化为一个JSON字符串,parse可以把JSON字符串反序列化为一个js对象,通过这两个方法,也可以实现对象的深复制。
// JSON.parse(JSON.stringify(obj))
var obj = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr = ['程序猿','程序媛',['男','女']]; var copyobj = JSON.parse(JSON.stringify(obj));
var copyarr = JSON.parse(JSON.stringify(arr)); console.log(copyobj) // {name:'程序猿',msg:{sex:'男',age:'20'}}
console.log(copyobj === obj) // false
console.log(copyobj.msg === obj.msg) // false copyobj.msg.sex = '人妖'
console.log(copyobj) // {name:'程序猿',msg:{sex:'人妖',age:'20'}}
console.log(obj) // {name:'程序猿',msg:{sex:'男',age:'20'}} console.log(copyarr) // ['程序猿','程序媛',['男','女']]
console.log(copyarr === arr) // false
console.log(copyarr[2] === arr[2]) // false copyarr[2][0] = '人妖'
console.log(copyarr) // ['程序猿','程序媛',['人妖','女']]
console.log(arr) // ['程序猿','程序媛',['男','女']]
但是,这种方法是有缺陷的,如果数组对象中包含undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
var obj = {
name:'程序猿',
msg:{sex:'男',age:'20'},
fn:function(){console.log('我是IT人')},
other: undefined
};
var arr = ['程序猿','程序媛',['男','女'],function(){console.log('我是IT人')},undefined];
var copyobj = JSON.parse(JSON.stringify(obj));
var copyarr = JSON.parse(JSON.stringify(arr));
console.log(copyobj) // {name:'程序猿',msg:{sex:'男',age:'20'}};
console.log(copyarr) // ['程序猿','程序媛',['男','女'],null,null]
(2)使用递归,其中涉及使用到Object.keys(),返回当前对象的属性的集合(第三方jquery的$.extend原理)
function deepCopy(obj) {
// 创建一个新对象
let result = {}
// 获取对象的属性的集合
let keys = Object.keys(obj),
key = null, // 声明的result对象的key
temp = null; // 用于判断循环所得的当前对象的属性值是否为对象
// 循环获取到的属性集合
for (let i = 0; i < keys.length; i++) {
// 将循环所得的key值赋给result的key值
key = keys[i];
//// 判断循环所得的当前对象的属性值是否为对象,是的话递归方法
temp = obj[key];
// 如果字段的值也是一个对象则递归操作
if (temp && typeof temp === 'object') {
result[key] = deepCopy(temp);
} else {
// 否则直接赋值给新对象
result[key] = temp;
}
}
return result;
}
var obj = {
name:'程序猿',
msg:{sex:'男',age:'20'},
fn:function(){console.log('我是IT人')},
other: undefined
};
var arr = ['程序猿','程序媛',['男','女'],function(){console.log('我是IT人')},undefined];
var copyobj = deepCopy(obj);
console.log(copyobj); // {name:'程序猿',msg{sex:'男',age:'20'},fn:(),other: undefined}
console.log(copyobj === obj ) // false
copyobj.msg.sex = '人妖';
console.log(copyobj) //{name:'程序猿',msg:{sex:'妖',age:'20'},fn:(),other: undefined}
console.log(obj) // {name:'程序猿',msg:{sex:'男',age:'20'},fn:(),other: undefined}
copyobj.fn(); // 我是IT人
var copyarr = deepCopy(arr );
console.log(copyarr); // {0: "程序猿", 1: "程序媛", 2:
{0: "男", 1: "女"}, 3: ƒ(), 4: undefined}
console.log(copyarr[3]) // ƒ (){console.log('我是IT人')}
当然,这种递归的方法也不是万能的,只是比前面的转化更为完善,如果递归的是一个被引用的对象,那么会导致死循环递归导致爆栈,比如:
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
解决方法在此不做解释,前辈给了这么种解决方法,供我们参考使用:判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,改造下前面的deepCopy方法
function deepCopy(obj, parent = null) {
// 创建一个新对象
let result = {};
let keys = Object.keys(obj),
key = null,
temp= null,
_parent = parent;
// 该字段有父级则需要追溯该字段的父级
while (_parent) {
// 如果该字段引用了它的父级则为循环引用
if (_parent.originalParent === obj) {
// 循环引用直接返回同级的新对象
return _parent.currentParent;
}
_parent = _parent.parent;
}
for (let i = 0; i < keys.length; i++) {
key = keys[i];
temp= obj[key];
// 如果字段的值也是一个对象
if (temp && typeof temp=== 'object') {
// 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
result[key] = DeepCopy(temp, {
originalParent: obj,
currentParent: result,
parent: parent
});
} else {
result[key] = temp;
}
}
return result;
}
四、总结
对于数组对象的拷贝,一般情况下常用JSON.parse(JSON.stringify(obj))即可
全面聊聊JavaScript的浅拷贝和深拷贝的更多相关文章
- javascript篇-浅拷贝与深拷贝
理解javascript 的浅拷贝与深拷贝,首先看一下js的数据类型: js有5种基本数据类型:undefined,null,boolean,number,string 还有一种复杂的数据类型(也叫引 ...
- 关于JavaScript的浅拷贝和深拷贝
在 JS 中有一些基本类型像是Number.String.Boolean,而对象就是像这样的东西{ name: 'Larry', skill: 'Node.js' },对象跟基本类型最大的不同就在于他 ...
- JavaScript中浅拷贝和深拷贝的区别和实现
深拷贝和浅拷贝的区别 浅拷贝(shallow copy):只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一块内存: 深拷贝(deep copy):复制并创建一个一摸一样的对象,不共 ...
- JavaScript中浅拷贝和深拷贝的区别
JavaScript数据类型 基础数据类型:保存在栈内存中的简单数据段 ,有undefined,boolean,number,string,null 引用数据类型:Array,object,Funct ...
- javascript的浅拷贝和深拷贝
1.浅拷贝:复制一份引用,所有引用对象都指向一份数据,并且都可以修改这份数据. 2.深拷贝(复杂):复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制. 这里画一个简单的图来加深理解: ...
- JavaScript 数据结构与算法之美 - 栈内存与堆内存 、浅拷贝与深拷贝
前言 想写好前端,先练好内功. 栈内存与堆内存 .浅拷贝与深拷贝,可以说是前端程序员的内功,要知其然,知其所以然. 笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScri ...
- Javascript中的浅拷贝和深拷贝
很多开发语言中都有浅拷贝和深拷贝的说法,这里简单区分一下它们在Javascript中的区别,以及jQuery中深拷贝的实现. 在谈浅拷贝和深拷贝之前,先要屡清楚Javascript中的按值访问和按引用 ...
- javascript中的浅拷贝和深拷贝(拷贝引用和拷贝实例)
作者:千锋教育链接:https://www.zhihu.com/question/23031215/answer/326129003来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请 ...
- JS面试题-<变量和类型>-JavaScript浅拷贝与深拷贝
前言 最开始了解到深浅拷贝是因为准备面试,但那个时候因为在学校做的项目比较少需求也比较简单,所以没有在项目中遇到这类问题,所以对这个问题就属于知道这个知识点,看过相关内容,却没有自己的总结,也没有深入 ...
随机推荐
- python mysql使用问题
(deeplearning2) userdeMBP:ageAndGender user$ python Python |Anaconda, Inc.| ( , ::) [GCC Compatible ...
- CGI "Internal Server Error"
在安裝 CGI 程式時如果設定錯誤,便會看到 "500 Internal Server Error" 訊息,一般常見的錯誤可以用以下方法解決: 1. CGI 程式的權限需要設定為 ...
- jQuery学习四——效果
1.显示,隐藏: <!DOCTYPE html> <html> <head> <title>jquery事件</title> </he ...
- html页面设置自动刷新
在中添加如下: <meta HTTP-EQUIV="REFRESH" CONTENT="1"> 其中CONTENT对应刷新的间隔时间,这里为1秒. ...
- 定期删除IIS日志文件
服务器中由于监控的需要会经常生成很多日志文件,比如IIS日志文件(C:\inetpub\logs\LogFiles),一个稍微有流量的网站,其日志每天可以达到上百兆,这些文件日积月累会严重的占用服务器 ...
- 网页布局(html+css基础)
一.如何用CSS进行网页布局 二.网页布局基础 三.网页简单布局之结构和表现原则 四.CSS Sprite雪碧图应用 什么叫布局? 网页的特点: 网页自适应宽度: 网页长度无限延长: (分栏也叫分列, ...
- redis相关操作命令
一.相关命令列表: 命令原型 时间复杂度 命令描述 返回值 KEYS pattern O(N) 时间复杂度中的N表示数据库中Key的数量.获取所有匹配pattern参数的Keys.需要说明的是,在我们 ...
- 【Linux内核】编译与配置内核(x86)
[Linux内核]编译与配置内核(x86) https://www.cnblogs.com/jamesharden/p/6414736.html
- 【转帖】史上最全PostgreSQL体系结构
史上最全PostgreSQL体系结构 2019年07月16日 18:00:00 Enmotech 阅读数 35 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出 ...
- 内网环境下搭建maven私服小技巧
背景 最近接手一个其他公司的项目,因为工程中使用了maven,而且里面有很多他们自己封装很多自己的构件(就是jar.war等等),需要将他们maven私服迁移到我们的私服上去,因为网络环境不通,所以不 ...