引子

在js中,如何复制一个对象? 大家的第一反应是直接使用赋值语句赋值嘛,比如

let a = {a: 1};
let b = a;

看看打印结果

console.log(a)  // {a: 1}
console.log(b)  // {a: 1}

打印出来都是{a: 1},很不错,但是这样真的是拷贝出来一份了吗? 我们再试下一下的操作:

a.b = 1;
console.log(a)  // {a: 1, b: 1}
console.log(b)  // {a: 1, b: 1}

这样只修改了a对象,但是b对象也紧跟着改变了,这是什么原因造成的呢?

js的类型

原来,在js中,存在两个类型的概念,分别是基本类型与引用类型,而基本类型和引用类型的最主要的区别便是在计算机的储存位置不同;

基本类型:NumberStringBoolennullundefinedSymbolBigint 引用类型:ObjectArrayRegExpDateFunction

基本类型储存在栈(stack)中,它具有以下特性:

  1. 基本类型的比较是它们的值的比较
  2. 在复制基本类型值的时候,会开辟出一个新的内存空间,将值复制到新的内存空间

而引用类型储存在堆(heap)中,它具有一下特性

  1. 引用类型的比较是他们地址的比较(也就是指针指向的内容是否一致)
  2. 引用类型值是保存在堆内存中的对象,变量保存的只是指向该内存的地址,在复制引用类型值的时候,其实只复制了指向该内存的地址

由此可以得出,基本类型的复制可以直接使用赋值语句,而引用类型想这样直接复制则会得到一个新的指针指向该地址,并不会复制出一个新的对象出来,那如果我们需要复制一个新的对象,应该怎么办呢?

js对象的拷贝

js的拷贝,分为浅拷贝与深拷贝

浅拷贝:

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

浅拷贝使用场景:

1. Object.assign()

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。注意,Object.assign()不是深拷贝,如果非要说的话,他只是拷贝对象的第一层基本类型,引用类型拷贝的还是个指针,我们看下下面的例子:

let obj1 = {a: 1,b: {c: 2}
}
let obj2 = Object.assign({}, obj1);
console.log(obj2);
// {//  a: 1,
//  b: {c: 2}
// } obj1.a = 3;
obj1.b.c = 4;
console.log(obj1);
// {//  a: 3,
//  b: { c: 4}
// } console.log(b);
// {//  a: 1,
//  b: {c: 4}
// }
// } 上面代码改变对象 obj1 之后,对象 obj2 的基本属性保持不变。但是当改变对象 obj1 中的对象 b 时,对象 obj2 相应的位置也发生了变化。
2. 展开运算符…
let obj1 = {a: 1,b: {c: 2}
}let obj2 = {...obj1}
console.log(obj2);  // {a: 1, b: {c: 2}}obj1.a = 3;
obj1.b.c = 4;console.log(obj1); // {a: 3, b: {c: 4}}
console.log(obj2); // {a: 1, b: {c: 4}}

由上面代码所见,展开运算符…实际效果和Object.assign一样。

3. Array.prototype.slice()、Array.prototype.concat()

let arr1 = [1,2,[3,4]];
let arr2 = arr1.slice(1);
console.log(arr2); // [2,[3,4]]arr1[1] = 5;
arr1[2][0] = 6;
console.log(arr1); // [1,5,[6,4]]
console.log(arr2); // [2,[6,4]]let arr1 = [1,[2,3]];
let arr2 = [4,5,6];
let arr3 = arr1.concat(arr2);
console.log(arr3); // [1,[2,3],4,5,6]arr1[0] = 7;
arr1[1][0] = 8;
console.log(arr1); // [7,[8,3]]
console.log(arr3); // [1,[8,3],4,5,6]

由上可知Array的sliceconcat方法也不是深拷贝,因此在处理复杂数组的时候需要注意这里。

深拷贝:

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

如上,既然引用对象存储的是指针,基本类型储存的是值,那么,我们可以把引用类型变成基本类型,在把这个基本类型转换成引用类型重新赋值,这样就达到了深拷贝引用类型的效果

let a = {a: 1};
let b = JSON.stringify(a);
let c = JSON.parse(b);console.log(a); // {a: 1}
console.log(b); // {a: 1}a.b = 2;console.log(a); // {a: 1, b: 2}
console.log(b)  // {a: 1}

这样,我们就得到了一个新的对象。一切看起来都很完美,但是,当对象比较复杂时,又发现了新的问题

let a = {a: "1",b: undefined,c: Symbol("dd"),fn: function() {return true;},
};
console.log(JSON.stringify(a)); // {a: 1}

emmm,明明a对象有3个值,但是为什么JSON.stringify后只出现了1个?

原来,JSON.stringify具有以下特性:

undefined、symbol 和函数这三种情况,会直接忽略
 let obj = {name: 'muyiy',a: undefined,b: Symbol('muyiy'),c: function() {}
}
console.log(obj);  // { name: "muyiy", a: undefined, b: Symbol(muyiy), c: ƒ () }let b = JSON.parse(JSON.stringify(obj));
console.log(b);  // {name: "muyiy"}
循环引用情况下,会报错
let obj = {a: 1,b: {c: 2,d: 3}
}
obj.a = obj.b;
obj.b.c = obj.a;let b = JSON.parse(JSON.stringify(obj));  // Uncaught TypeError: Converting circular structure to JSON

new Date 情况下,转换结果不正确

new Date();
// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)JSON.stringify(new Date());
// ""2018-12-24T02:59:25.776Z""JSON.parse(JSON.stringify(new Date()));
// "2018-12-24T02:59:41.523Z"

不能处理正则

let obj = {name: "muyiy",a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy", a: {}}

那么,我们该怎么避免这种情况呢?

其实,实现一个对象的深拷贝,可以把他分为两部分,即浅拷贝+递归,可以判断当前属性是否是对象,如果是对象的话就进行递归操作。

function deepClone1(obj) {var target = {};for(var key in obj) {if( typeof obj[key] === 'object') {target[key] = deepClone1(obj[key]);} else {target[key] = obj[key];}}return target;
}let obj1 = {a: 1, b: {c: 2}};
let obj2 = deepClone1(obj1);
console.log(obj2); // {a: 1, b: {c: 2}}obj1.a = 3;
obj1.b.c = 4;console.log(obj1); // {a: 3, b: {c: 4}}
console.log(obj2); // {a: 1, b: {c: 2}}

以上,就是一个深拷贝的简单实现,但是,面对复杂的,多类型的对象,以上的方法还是有诸多缺陷。

1.没有考虑null的情况

在js的设计中,object的前三位标志是000,而null在32位表示中也全是0,因此,typeof null 也会打印出object

function deepClone2(obj) {if (obj === null) return null; // 新增代码,判断obj是否为nullvar target = {};for(var key in obj) {if( typeof obj[key] === 'object') {target[key] = deepClone2(obj[key]);} else {target[key] = obj[key];}}return target;
}

2.没有考虑数组的兼容

在js中,typeof 数组 得到的也是一个object,需要针对数组在做处理

function deepClone3(obj) {if (obj === null) return null;var target = Array.isArray(obj) ? []: {}; // 新增代码,判断是否是数组for(var key in obj) {if( typeof obj[key] === 'object') {target[key] = deepClone3(obj[key]);} else {target[key] = obj[key];}}return target;
}
复制代码

3.没有考虑对象中循环引用的情况

其实解决循环引用的思路,就是在赋值之前判断当前值是否已经存在,避免循环引用,这里我们可以使用es6的WeakMap来生成一个hash表

function deepClone4(obj, hash = new WeakMap()) {if (obj === null) return null;if (hash.has(obj)) return hash.get(obj); // 新增代码,查哈希表var target = Array.isArray(obj) ? []: {};hash.set(obj, target); // 新增代码,哈希表设值for(var key in obj) {if( typeof obj[key] === 'object') {target[key] = deepClone4(obj[key], hash); // 传入hash表} else {target[key] = obj[key];}}return target;
}var a = {b: 1};
a.c = a;
console.log(a); // {b:1, c: {b: 1, c:{......}}}
var b = deepClone4(a);
console.log(b); // {b:1, c: {b: 1, c:{......}}}

如果在es5中,同样用数组也可以实现。

4.没有考虑Symbol

判断当前对象是否有Symbol,需要使用到方法Object.getOwnPropertySymbols()或者Reflect.ownKeys(),下面,我们使用Object.getOwnPropertySymbols()来实现一下Symbol的拷贝function deepClone5(obj, hash = new WeakMap()) {if (obj === null) return null;if (hash.has(obj)) return hash.get(obj);var target = Array.isArray(obj) ? []: {};hash.set(obj, target);// ============= 新增代码let symKeys = Object.getOwnPropertySymbols(obj); // 查找if (symKeys.length) { // 查找成功    symKeys.forEach(symKey => {if (typeof obj[symKey] === 'object') {target[symKey] = deepClone5(obj[symKey], hash); } else {target[symKey] = obj[symKey];}    });}// =============for(var key in obj) {if( typeof obj[key] === 'object') {target[key] = deepClone5(obj[key], hash);} else {target[key] = obj[key];}}return target;
}

5.es6中Map和Set的拷贝

由于typeof Map/Set对象 也为 object,因此,在此处我们需要使用Object.prototype.toString.call()方法,下面,我们需要对deepClone函数进行一下改造

function deepClone6(obj, hash = new WeakMap()) {// 判断是否为nullif (obj === null) return null;// 设置hash表,判断是否是循环引用if (hash.has(obj)) return hash.get(obj);// 判断Symbollet symKeys = Object.getOwnPropertySymbols(obj);if (symKeys.length) {symKeys.forEach(symKey =>{if (typeof obj[symKey] === 'object') {target[symKey] = deepClone6(obj[symKey], hash);} else {target[symKey] = obj[symKey];}});}// 判断是否是对象,如果不是对象,则直接返回,如果是对象,则继续执行if (typeof obj === 'object') {let target = null;let result;hash.set(obj, target);let objType = Object.prototype.toString.call(obj);switch (objType) {case '[object Object]':target = {};break;case '[object Array]':target = [];break;case '[object Map]':// 处理Map对象result = new Map();obj.forEach((value, key) =>{result.set(key, deepClone6(value, hash))}) return resultbreak;case '[object Set]':// 处理Set对象obj.forEach((value) =>{result.add(deepClone6(value, hash))}) return resultbreak;default:break;}} else {// 不是对象的情况return obj;}for (var key in obj) {if (typeof obj[key] === 'object') {target[key] = deepClone6(obj[key], hash);} else {target[key] = obj[key];}}return target;
}

6.Date对象,正则,以及函数

Date对象的复制可以直接返回一个新的new Date()对象,避免 setTime、setYear 等造成的引用改变,而正则,以及函数虽然是引用对象,也储存在堆里,但是一般情况下都不会给他们挂附加属性,所以这里一般情况下直接赋值就行

function deepClone7(obj, hash = new WeakMap()) {// 判断是否为nullif (obj === null) return null;// 设置hash表,判断是否是循环引用if (hash.has(obj)) return hash.get(obj);// 判断Symbollet symKeys = Object.getOwnPropertySymbols(obj);if (symKeys.length) {symKeys.forEach(symKey =>{if (typeof obj[symKey] === 'object') {target[symKey] = deepClone7(obj[symKey], hash);} else {target[symKey] = obj[symKey];}});}// 判断是否是对象,如果不是对象,则直接返回,如果是对象,则继续执行if (typeof obj === 'object' || typeof obj === 'function') {let target = null;let result;hash.set(obj, target);let objType = Object.prototype.toString.call(obj);switch (objType) {case '[object Object]':target = {};break;case '[object Array]':target = [];break;case '[object Map]':// 处理Map对象result = new Map();obj.forEach((value, key) =>{result.set(key, deepClone7(value, hash))}) return resultbreak;case '[object Set]':// 处理Set对象obj.forEach((value) =>{result.add(deepClone7(value, hash))}) return resultbreak;case '[object Date]':// 处理Date对象return new Date(obj)break;default:// 直接返回正则、函数return obj;break;}} else {// 不是对象的情况return obj;}for (var key in obj) {if (typeof obj[key] === 'object') {target[key] = deepClone7(obj[key], hash);} else {target[key] = obj[key];}}return target;
}

7.避免递归爆栈

由于上面的深拷贝都是使用的递归,我们都知道一般递归都会大量消耗内存,存在爆栈的可能。针对这个弊端,我们通常有两种解决思路,一种是尾递归,一种是把递归转化成深度遍历或者广度遍历。最后,简单提供一下思路:

function cloneDeep8(x) {const root = {};// 栈const loopList = [{parent: root,key: undefined,data: x,}];while(loopList.length) {// 广度优先const node = loopList.pop();const parent = node.parent;const key = node.key;const data = node.data;// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素let res = parent;if (typeof key !== 'undefined') {res = parent[key] = {};}for(let k in data) {if (data.hasOwnProperty(k)) {if (typeof data[k] === 'object') {// 下一次循环loopList.push({parent: res,key: k,data: data[k],});} else {res[k] = data[k];}}}}return root;
}

Javascript如何深拷贝对象相关推荐

  1. JavaScript 中的对象拷贝(深拷贝、浅拷贝)

    对象是 JavaScript 的基本块.对象是属性的集合,属性是键值对.JavaScript 中的几乎所有对象都是位于原型链顶部 Object 的实例. 介绍 如你所知,赋值运算符不会创建一个对象的副 ...

  2. 17个实用的JavaScript数组和对象的方法

    原文:Useful Javascript Array and Object Methods 作者:Robert Cooper 译者:Jim Xiao 前段时间,我收听了一个很棒的Syntax FM播客 ...

  3. Javascript中的对象拷贝(对象复制/克隆)

    Javascript中的对象拷贝(对象复制/克隆) 李俊才 CSDN:jcLee95 邮箱:291148484@163.com 1. 对象的引用 要说"拷贝"还要先说"引 ...

  4. 在 JavaScript 中复制对象

    ​​​​​​ 1.JavaScript中的浅拷贝对象 2.深度复制JavaScript中的对象 各种编程语言具有各种数据结构,允许你在内存中组织和存储数据.每个数据结构的工作方式都是独一无二的.例如, ...

  5. 创建健壮的isArray()函数(JavaScript中判断对象类型的种种方法)

    我们知道,JavaScript中检测对象类型的运算符有:typeof.instanceof,还有对象的constructor属性: 1) typeof 运算符 typeof 是一元运算符,返回结果是一 ...

  6. JavaScript强化教程——对象的值传递和引用传递

    2019独角兽企业重金招聘Python工程师标准>>> 本文为 H5EDU 机构官方 HTML5培训 教程,主要介绍:JavaScript强化教程--对象的值传递和引用传递 func ...

  7. 第一百一十四节,JavaScript文档对象,DOM进阶

    JavaScript文档对象,DOM进阶 学习要点: 1.DOM类型 2.DOM扩展 3.DOM操作内容 DOM自身存在很多类型,在DOM基础课程中大部分都有所接触,比如Element类型:表示的是元 ...

  8. 在JavaScript中删除对象

    本文翻译自:Deleting Objects in JavaScript I'm a bit confused with JavaScript's delete operator. 我对JavaScr ...

  9. 如何在Javascript中访问对象的第一个属性?

    本文翻译自:How to access the first property of an object in Javascript? Is there an elegant way to access ...

  10. 检查值是否是JavaScript中的对象

    如何检查值是否是JavaScript中的Object? #1楼 尝试这个 if (objectName instanceof Object == false) {alert('Not an objec ...

最新文章

  1. 移动互联网用户的心理需求【转载】
  2. Msdn 杂志 asp.net ajax 文章汇集
  3. cef使用缓存_CEF 文件下载功能实现
  4. Android动画开发——Animation动画效果
  5. Ubuntu 14.04 安装Visual studio Code
  6. 【渝粤教育】电大中专测量学 (2)作业 题库
  7. 三、Beautiful Soup解析库
  8. JVM 学习三:类加载器
  9. html 图片分散,纯js和CSS3分散式宝丽来图片画廊
  10. rms归一化_将FFT频谱幅度归一化为0dB
  11. javaweb中真分页案例
  12. 文件搭建后找不到变量/函数定义问题
  13. 深信服短信认证云信通短信配置说明
  14. 创业基础(第六章:创业资源及其管理) 来自高校:全国大学生创新创业实践联盟 分类:创新创业 学习规则:按序学习
  15. html广告倒计时代码,jquery实现可关闭的倒计时广告特效代码
  16. 怎么看计算机电源型号,电脑电源铭牌怎么看?台式机电源铭牌知识扫盲 拒绝虚标!...
  17. Javascript是什么,能干什么、JavaScript的发展史、JavaScript的使用、方式一:内部引用、方式二:外部引用、程序设计的基础、数据类型与变量、运算符
  18. Bayesian Learning via Stochastic Gradient Langevin Dynamics
  19. python下载某网站收费文档(一)——配合fiddler半自动版
  20. 基于JSP的“爱心宠物诊所”系统课程设计

热门文章

  1. ITSM群(48132184)讨论精选
  2. Docker 的部署方式
  3. PostgreSQL使用函数实现merge功能
  4. zabbix监控memcached
  5. 颠覆大数据分析之Storm简介
  6. iOS开发之runtime的运用-获取当前网络状态
  7. Android之Handler,举例说明如何更新UI
  8. Enterprise Library引起的A reference to 'System.Design' could not be added
  9. 13.高性能MySQL --- 云端的MySQL
  10. 13.GitLab api