其实深拷贝和浅拷贝的主要区别就在于其内存中的存储类型不同.
堆和栈都是内存中划分出来用于存储的区域.
栈会自动扥配内存空间,由紫铜自动释放
堆则是动态分配的内存,大小不定也不会自动释放.
js 的数据类型大方向上来说有两种:
当一个变量存放基本数据类型时与复杂的数据类型时分别存在以下的特点:
其实当解释器寻找引用值时,会首先检索其在栈中的地址引用,取得地址后从堆中获得对象。
其实当解释器寻找引用值时,会首先检索其在栈中的地址引用,取得地址后从对中获得对象.
如下图:
通过上面的分析,我们可以知道的是基本数据类型存放在栈中,引用数据类型存放堆中.
JavaScript中的原始值(null,undefined,布尔值,数字和字符串)与对象(包括数组和函数)有这根本却别.
原始值是不可更改:任何方法都无法更改一个原始值.对于数字和布尔值来说就不那么明显了,因为字符串看起来像由字符串组成的数组,我们期望可以通过指定索引来假改字符串中的字符.
实际上,javascript 是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。
例如下面的代码:
var str = 'abc';
str[1] = 'f';
console.log(str); //abc
后面会写一篇博客专门介绍为什么这些数据不可变
基本类型的比较是值的比较
var a = 1;
var b = 1;
console.log(a === b); //true
比较的时候最好使用严格等,因为 == 是会进行类型转换的,比如:
var a = 1;
var b = true;
console.log(a == b);//true
引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针(js中实际上没有指针这个概念,为了理解我称他为指针),这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况开进行特定的分配,例如。
var person1 = {
name:'why'};
var person2 = {
name:'gh'};
var person3 = {
name:'xxg'};
let person = {
name : 'why'
};
person['name'] = 'gh';
console.log(person) //{name :'gh'}
let arr = [1,2,3,4];
arr[3] = 5;
console.log(arr); //[1,2,3,5]
引用类型的比较是引用的比较
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是说变量 a 和变量 b 指向的不是同一个对象,所以他们是不相等的。
了解了基本数据类型和引用数据类型的基本区别之后,我们很快就能明白传值与传址的区别了.
在我们进行赋值操作的时候,基本数据类型的赋值(=)是在内存中新开辟一段栈内存,然后再把值赋值到新的栈中.
var a = '李华';
var b = a;
console.log(a,b); //李华 李华
b = '韩梅梅';
console.log(a,b) //李华 韩梅梅
所以说,基本类型的赋值的两个变量是两个独立相互不影响的变量。
但是引用类型的赋值是传址。只是改变指针的指向,例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。例如:
var a = {
}; // a保存了一个空对象的实例
var b = a; // a和b都指向了这个空对象
a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'
b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22
console.log(a == b);// true
我们先来看看下面的代码:
var a = 5;
var b = a;
var obj = {
name : 'gh'};
var obj1 = obj;
对于以上的代码,变量的内存分配是在栈中,所以变量不可以存放堆中的对象,所以赋值就会出现两种情况:
基本数据类型:将变量num的值拷贝一份存储在numCopy中
赋值数据类型:将变量obj中直接存储的引用拷贝一份存储在objCopy中
对于上面的代码,变量的内存分配是在栈中,所以变量不可以存放对中的对象,所以赋值就会有下面两种情况:
所以Number类型的变量num通过简单的赋值得到了两份值(在栈内有不同的存储空间),而复杂数据类型obj通过赋值仅仅是得到了两份引用(在栈内有不同的内存空间),而实际的对象只有一份(在堆内存中是同一个值)
所以我们可以知道,要拷贝一份复杂数据类型远没有我们想象的那样简单。
这也就引出了我们今天的话题,深拷贝和浅拷贝。
值得我们注意的是,我们平时想下面的操作只能叫做赋值,和深浅拷贝完全没有关系,只有当我们真的需要两份相同的值而又对他们进行不同的操作的时候才会有深浅拷贝的说法.
var a = 5;
var b = a;
//这仅仅是一个赋值操作
我们应该注意的一点,深拷贝和浅拷贝只是针对与Object 和 Array 这样的引用数据类型的,而简单数据类型由于情况比较简单,就是拷贝一份,所以不存在深拷贝和浅拷贝的说法
var obj = {
name :'why',age : 18,hobby :['电影','阅读','音乐']};
var o = obj;
//改变对象中的简单数据类型观察是否有影响
o.age = 12;
console.log(obj.age,o.age); //12
//改变对象中的简单数据类型观察是否有影响
o.hobby[1] = '吃嘛嘛香';
console.log(obj.hobby); // ["电影", "吃嘛嘛香", "音乐"]
console.log(o.hobby); // ["电影", "吃嘛嘛香", "音乐"]
两个对象共享同一份内存空间,不管修改简单数据类型数据还是复杂类型数据,都会对另一份数据产生影响.
我们来看看下面浅拷贝的代码:
var obj = {
name :'why',age : 18,hobby :['电影','阅读','音乐']};
var o = {
};
//浅拷贝一份obj
for(let k in obj) {
o[k] = obj[k];
}
console.log(o); //{name :'why',age : 18,hobby :['电影','阅读','音乐']};
//改变对象中的简单数据类型观察是否有影响
o.age = 12;
console.log(obj.age,o.age); // 18 12
//改变复杂数据类型观察
o.hobby[1] = '超级爱吃火锅';
console.log(obj.hobby); //["电影", "超级爱吃火锅", "音乐"]
console.log(o.hobby); //['电影','阅读','音乐']
通过上面的示例我们可以知道:
hobby
上的第一个元素,新旧对象都发生了改变。类型 | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |
到这里我们对赋值和浅拷贝都有了一定的了解,那么到底什么是深拷贝我们是不是有了那么一丢丢的感觉.
通过下面的代码来实现深拷贝:
var obj = {
name :'why',age : 18,hobby :['电影','阅读','音乐']};
var o = {
};
for(let k in obj) {
let item = obj[k];
if(item instanceof Array) {
console.log(11)
o[k] = [];
for(index in item) {
o[k][index] = item[index];
}
}else {
o[k] = item;
}
}
console.log(o) //{name :'why',age : 18,hobby :['电影','阅读','音乐']};
//修改对象中的简单数据类型观察
o.name = 'gh';
console.log(obj.name, o.name); //why gh
//修改复杂数据类型观察
o.hobby[1] = '看你变了没';
console.log(obj.hobby); // ["电影", "阅读", "音乐"]
console.log(o.hobby); //["电影", "看你变了没", "音乐"]
通过上面的代码我们可以发现:
如下图所示:
所以我们可以这样认为,浅拷贝只是肤浅的拷贝一个对象,而不拷贝对象的所有,新旧对象存在共享同一块堆内存的情况。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享堆内存,修改新对象不会改到原对象。
所以我们在这里给出神浅拷贝的定义:
浅拷贝 : 只拷贝一层,更深层次对象级别的只能拷贝引用
深拷贝: 拷贝多层,每一级别的数据都会拷贝
//封装深拷贝函数
function deepCopy(newObj, oldObj) {
for (k in oldObj) {
let item = oldObj[k];
//判断旧对象的属性是不是数组
if (item instanceof Array) {
//初始化newObj对象对用的属性为数组
newObj[k] = [];
//再次调用深拷贝函数
deepCopy(newObj[k], item)
//判断旧对象的属性是不是对象
} else if (item instanceof Object) {
//初始化newObj对象对用的属性为对象
newObj[k] = {
};
//再次调用深拷贝函数
deepCopy(newObj[k], item)
//否则就是普通类型数据,直接进行拷贝
} else {
newObj[k] = item;
}
}
}
针对上面的封装,我们提出一个问题???
为什么要先判断是不是Array类型而不是先判断是不是Object类型.
console.log(Array instanceof Object) //true
因为数组判断是否为对象类型时也会为true,这是因为intanceof
进行查找时也会查找原型链,所以为true.