全面聊聊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 ...
- 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_09-SpringSecurityOauth2研究-Oauth2密码模式授权
密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌. 测试如下: Post请求:htt ...
- STL函数适配器
一:适配器简介 C++中有三类适配器,分别是容器适配器,迭代器适配器和函数适配器,这里主要介绍函数适配器. (一)函数适配器简介 STL中已经定义了大量的函数对象,但是有时候需要对函数返回值进行进一步 ...
- 【转】python selenium2 中的显示等待WebDriverWait与条件判断expected_conditions举例
#coding=utf-8 from selenium import webdriver from selenium.webdriver.common.by import By from seleni ...
- No WebApplicationContext found: no ContextLoaderListener registered
修改前运行报错:No WebApplicationContext found: no ContextLoaderListener registered? <web-app> <dis ...
- vue路由传参的三种方式
方式一 通过query方式传参 这种情况下 query传递的参数会显示在url后面 this.$router.push({ path: '/detail', query: { id: id } }) ...
- 成为java架构师的技能
0: 数据结构算法 数组.链表.堆.栈.队列.Hash表.二叉树等; 算法思想:递推.递归.穷举.贪心.分治.动态规划.迭代.分枝界限; 排序查找 B+/B-数.红黑树.图等; 图的深度优先搜索.图的 ...
- 关于AES加密,以及各种分组加密
http://blog.csdn.net/searchsun/article/details/2516191
- 【k8s第三步】Kubernetes-Dashboard仪表盘【已修正错误】
⒈下载描述文件 wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta6/aio/deploy/recommen ...
- 有关Linux服务器的一些配置
1.Redis部署 1.版本 redis-3.0.72.上传解压 3.编译 make && make install 问题:/bin/sh: cc: command not found ...