深拷贝与浅拷贝的概念只存在于引用类型。
1、基本类型
基本的数据类型有:undefined,boolean,number,string,null。 基本类型存放在栈区,访问是按值访问的,就是说你可以操作保存在变量中的实际的值。
当基本类型的数据赋值时,赋得是实际的值,a和b是没有关联关系的,b由a复制得到,相互独立。(字面量的才是基本类型)
var a=10;
var b=a;
console.log(a+','+b); // 10,10
a++;
console.log(a+','+b) // 11,10
2、引用类型
引用类型指的是对象。可以拥有属性和方法,并且我们可以修改其属性和方法。引用对象存放的方式是:在栈中存放对象变量标示名称和该对象在堆中的存放地址,在堆中存放数据。
对象使用的是引用赋值。当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在堆中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
传值:
只会在调用函数的这个作用域中起作用。
传值调用的情况是这样的:实参把值传入
堆栈
然后发生传递过程,形参
接受
这个值,也可以
改变
这个值,形参可以在自身的函数中有很多变量,可以进行运算,改变他们的值,但问题的关键是,这些变量开辟的内存空间都是在
堆栈中
的,在调用结束的一瞬间堆栈全
都释放弹栈
了,所有的堆栈的内存空间都没了,存放的数据也就跟着消失了。这个就是传值不影响实参的根本原因。
在
函数调用中
发生的数据
传送是单向
的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。
传引用:
真正的以
地址
的方式传递参数
传递以后,行参和实参都是同一个对象,只是他们名字不同而已
,对行参的修改将影响实参的值。
它其实和传值基本一样的传送过程,但是关键就在于在刚开辟堆栈的时候,它
放入
的是由主调函数放进来的
实参
变量的
地址
,被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量,那么形参在修改的时候,
修改
的就是实参
地址所对应的值
,也就是实参的值,虽然随着堆栈的消失,这个实参地址和形参都消失了,但
修改的内容却不在堆栈所开辟的内存中
,它一直存在着,而且这个内存就是原来用来存放实参的。
传参时,基本类型传值,引用类型传引用。(重新赋值后改变)
例子
var
a
=
{ n:
1
};
var
b
=
a;
a.x
=
a
=
{ n:
2
};
console
.
log
(a.x);
console
.
log
(b.x);
首先是
var a = {n:1};
var b = a;
在这里a指向了一个对象{n:1}(我们姑且称它为对象A),b指向了a所指向的对象,也就是说,在这时候a和b都是指向对象A的:
这一步很好理解,接着继续看下一行非常重要的代码:
a.x = a = {n:2};
我们知道js的赋值运算顺序永远都是从右往左的,不过由于“.”是优先级最高的运算符,所以这行代码先“计算”了a.x。
这时候发生了这个事情——a指向的对象{n:1}新增了属性x(虽然这个x是undefined的):
从图上可以看到,由于b跟a一样是指向对象A的,要表示A的x属性除了用a.x,自然也可以使用b.x来表示了。
接着,依循“从右往左”的赋值运算顺序先执行 a={n:2} ,这时候,a指向的对象发生了改变,变成了新对象{n:2}(我们称为对象B):
接着继续执行 a.x=a,很多人会认为这里是“对象B也新增了一个属性x,并指向对象B自己”
但实际上并非如此,由于一开始js已经先计算了a.x,便已经解析了这个a.x是对象A的x,所以在同一条公式的情况下再回来给a.x赋值,也不会说重新解析这个a.x为对象B的x。
所以 a.x=a 应理解为对象A的属性x指向了对象B:
那么这时候结果就显而易见了。当
console.log(a.x)
的 时候,a是指向对象B的,但对象B没有属性x。没关系,当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。但当查找到达原型链的顶部 - 也就是 Object.prototype - 仍然没有找到指定的属性B.prototype.x,自然也就输出undefined;
2014年阿里面试题
实现深拷贝的方法:
数组:slice()、
concat、Array.from() ;
对象:Object.assign()
经过验证,我们发现JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题。只能祭出大杀器
:递归
function
deepCopy
(
obj
) {
// 创建一个新对象
let
result
=
{}
let
keys
=
Object
.
keys
(obj),
key
=
null
,
temp
=
null
;
for
(
let
i
=
0
; i
<
keys.length; i
++
) {
key
=
keys[i];
temp
=
obj[key];
// 如果字段的值也是一个对象则递归操作
if
(temp
&&
typeof
temp
===
'object'
) {
result[key]
=
deepCopy
(temp);
}
else
{
// 否则直接赋值给新对象
result[key]
=
temp;
}
}
return
result;
}
var
obj1
=
{
x: {
m:
1
},
y:
undefined
,
z
:
function
add
(
z1
,
z2
) {
return
z1
+
z2
},
a:
Symbol
(
"foo"
)
};
var
obj2
=
deepCopy
(obj1);
obj2.x.m
=
2
;
console
.
log
(obj1);
//{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console
.
log
(obj2);
//{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}
我们也可以用第三方库:jquery的$.extend和lodash的_.cloneDeep来解决深拷贝。
循环引用拷贝
var
obj1 = {
x:
1
,
y:
2
};
obj1.z = obj1;
var
obj2 = deepCopy(obj1);
此时如果调用刚才的deepCopy函数的话,会陷入一个循环的递归过程,从而导致爆栈。jquery的$.extend也没有解决。解决这个问题也非常简单,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,修改一下代码:
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;
}
var
obj1 = {
x
:
1
,
y
:
2
};
obj1.z = obj1;
var
obj2 = deepCopy(obj1);
console
.log(obj1);
//太长了去浏览器试一下吧~
console
.log(obj2);
//太长了去浏览器试一下吧~
至此,已完成一个支持循环引用的深拷贝函数。当然,也可以使用lodash的_.cloneDeep噢~。
参考文献:
1、作者:吴胜斌 https://www.simbawu.com/article/search/9
2、https://www.cnblogs.com/chris-oil/p/4862638.html
3、https://www.cnblogs.com/cench/p/6019453.html