要理解 JavaScript中浅拷贝和深拷贝的区别,首先要明白JavaScript的数据类型
1、数据类型
JavaScript有两种数据类型,基础数据类型和引用数据类型
- 基础数据类型:保存在栈内存中的简单数据段 ,有
undefined,Boolean,Number,String,null
- 引用数据类型(对象Object):数组、函数、正则、表达式、Date
如图所示:a1 = 0;a2 = 'this is str';a3 = null
存放在栈内存中var c =[1,2,3]
与var d = {m:20}
变量名与内存地址存储在栈内存中,[1,2,3]
与{m:20}
作为对象存储在堆内存中 基础数据类型的复制(如var a = 20 var b = a
)
var m ={a:10, b:20} var n = m
)
m.a = 80 ; console.log(n.a) // 80怎么样使引用数据类型有各自独立的内存空间
为什么基本数据类型保存在栈中,而引用数据类型保存在堆中?
1.堆比栈大,栈比堆速度快;
2.基本数据类型比较稳定,而且相对来说占用的内存小;
3.引用数据类型大小是动态的,而且是无限的,引用值的大小会改变,不能把它放在栈中,否则会降低变量查找的速度,因此放在变量栈空间的值是该对象存储在堆中的地址,地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响;
4.堆内存是无序存储,可以根据引用直接获取;
按引用访问:js不允许直接访问保存在堆内存中的对象,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值;
2、浅拷贝
浅拷贝只复制对象的第一层属性
2.1 只复制第一层的浅拷贝
function simpleCopy(obj1) {
var obj2 = Array.isArray(obj1) ? [] : {};
for (let i in obj1) {
obj2[i] = obj1[i];
}
return obj2;
}
var obj1 = {
a: 1,
b: 2,
c: {
d: 3
}
}
var obj2 = simpleCopy(obj1);
obj2.a = 3;
obj2.c.d = 4;
alert(obj1.a); // 1
alert(obj2.a); // 3
alert(obj1.c.d); // 4
alert(obj2.c.d); // 4
2.2 Object.assign()实现浅拷贝及一层的深拷贝
let obj1 = {
a: {
b: 1
},
c: 2
}
let obj2 = Object.assign({},obj1)
obj2.a.b = 3;
obj2.c = 3
console.log(obj1.a.b); // 3
console.log(obj2.a.b); // 3
console.log(obj1.c); // 2
console.log(obj2.c); // 3
2.3 展开语法 ...
let a = {
name: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = {...a};
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }
a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "55"}
// }
2.4 Array.prototype.slice()
slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。
let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]
a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]
console.log(b);
// ["1", [4, 3]]
2.5 Array.prototype.concat()
创建当前数组的副本,并将接收的参数添加到副本的末尾,并返回一个新数组(没有参数即返回副本本身)
var arr = [1,5,8,7,4];
var arr2 = arr.concat();//[1,5,8,7,4]
3、深拷贝
深拷贝可以对对象的属性进行递归复制
3.1 用JSON.stringify
把对象转成字符串,再用JSON.parse
把字符串转成新的对象。
function deepClone(obj){
var _obj = JSON.stringify(obj);
var cloneObj = JSON.parse(_obj);
return cloneObj;
}
var a = [1,2,3,4];
var b = deepClone(a);
a[0] = 8;
console.log(a,b);
但是该方法有以下几个问题:
1、会忽略 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"}
2、循环引用情况下,会报错。
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
3、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 date = (new Date()).valueOf();
// 1545620645915
JSON.stringify(date);
// "1545620673267"
JSON.parse(JSON.stringify(date));
// 1545620658688
4、不能处理正则
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: {}}
3.2 jQuery.extend()
$.extend( [deep ], target, object1 [, objectN ] )
第一个参数deep的值默认为false,当deep为true时,进行深拷贝。
jQuery源码的解决方式:
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,
//这里需要判断传进来的参数,如果第一个布尔值是true则会赋值给参数deep进行深拷贝
target = arguments[ 0 ] || {},
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
// Skip the boolean and the target
//判断完是否深拷贝,将后续参数传给target,准备真正开始处理要拷贝的参数
target = arguments[ i ] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
//如果target(此处target已经不是布尔值了,而是接受扩展的对象了)不是obj类型也不是function就赋一个空对象
if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
target = {};
}
// Extend jQuery itself if only one argument is passed
//i是1或2(取决于有没有第一个布尔参数),length是参数个数,如果只有一个参数,那么会扩展的是jQuery本身
if ( i === length ) {
target = this;
i--;
}
for ( ; i < length; i++ ) {
// Only deal with non-null/undefined values
//只处理不是空的参数
if ( ( options = arguments[ i ] ) != null ) {
// Extend the base object
for ( name in options ) {
//被扩展对象赋给src
src = target[ name ];
//需拷贝的剔除null的内容赋给copy
copy = options[ name ];
// Prevent never-ending loop
当被扩展的对象和拷贝的内容一样时直接跳过这次循环
if ( target === copy ) {
continue;
}
// Recurse if we're merging plain objects or arrays
//当需要深拷贝且拷贝的对象又是对象或数组需要进行递归,知道拷贝的是简单对象为止
if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
( copyIsArray = jQuery.isArray( copy ) ) ) ) {
//因为后面还要再调用这个深拷贝函数,要进行参数设置
if ( copyIsArray ) {
//上一次判断是否是数组为true,下一次还要判断设置回false
copyIsArray = false;
//再一层深拷贝需要被扩展的对象赋给clone
clone = src && jQuery.isArray( src ) ? src : [];
} else {
//和上面一样,这里是针对对象的设置
clone = src && jQuery.isPlainObject( src ) ? src : {};
}
// Never move original objects, clone them
//递归的方法,再调用jQuery.extend()又进行一次深拷贝
target[ name ] = jQuery.extend( deep, clone, copy );
// Don't bring in undefined values
//如果需拷贝的已经是个简单类型(除undefined外)直接赋值就行
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// Return the modified object
//返回值到需扩展的对象里
return target;
};
3.3 lodash.cloneDeep()
3.4 采用递归的方法复制拷贝对象
function deepClone(obj) {
let objClone = Array.isArray(obj) ? [] : {};
if(obj && typeof obj === "object") {
for(key in obj) {
if(obj.hasOwnProperty(key)) {
if(obj[key] && obj[key] ==="object") {
objClone[key] = deepclone(obj[key]);
}else {
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
var a = [1,2,3,4];
var b = deepClone(a);
a[0] = 8;
console.log(a,b);
总结:
-
浅拷贝(shallow copy):只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一 块内存;
-
深拷贝(deep copy):复制并创建一个一摸一样的对象,不共享内存,修改新对象,旧对象保持不变;
-
实现深拷贝主要有2种方法:
-
(1)递归
-
(2)
JSON.stringify
结合JSON.parse