前言
JavaScript 系列文章:JavaScript进阶
变量存储类型
要理解深浅拷贝,先要熟悉变量存储类型,分为基本数据类型
(值类型)和引用数据类型
(复杂数据类型)。基本数据类型的值是直接存在栈内存的,而引用数据类型的栈内存保存的是内存地址,值保存在堆内存中。
变量存储类型 | 值 | 地址值 | 例子 | 基本数据类型 | 存储在栈 中 | string、bool、number、undefined、null、symbol(ES6新增) | 引用数据类型 | 存储在堆 中 | 存储在栈 中 | 数组、对象、函数、正则 |
---|
基本数据类型
-
1、使用
typeof()
判断null
,打印的是object
,但是null
是基本数据类型。基本数据类型存储的是值,是没有函数可以调用的,比如调用null.toString()
就会报错 -
2、
null
和undefined
的区别null
:js 的关键字,是一个特殊的对象值,表示空值
,typeof运算是object
undefined
:预定义的全局变量,表示未定义
,用typeof运算是undefined
-
3、关于
Symbol
,可以去看文章 Symbol 是不是构造函数?
引用数据类型
引用类型用typeof运算后,会输出object
检测数据类型的方法
- 1.typeof
typeof是检测数据类型的运算符,输出的字符串就是对应的类型。有以下局限性1) | 1)typeOf(null) 输出的是object ,但 null 是基本数据类型 | 2)无法区分对象具体是什么类型,比如typeof([1]) 和typeof({1}) 输出的都是object |
---|
-
2.instanceOf
检查某个实例是否属于某个类
-
3.constructor
获取当前实例的构造器,详见 constructor 属性是否只读?
-
4.Object.prototype.toString.call
获取当前实例所属类的信息,详见 通过 call 改变 this 指向来进行类型判断
赋值、浅拷贝、深拷贝的区别
从生成的新对象与原数据是否指向同一对象
,以及改变新对象是否会导致原数据发生改变行比较
(根据对象的第一层属性是 基本数据类型 和 引用数据类型分类)。
操作类型 | 指向同一对象 | 第一层属性是基本类型 | 第一层属性是引用类型 | 赋值 | 是 | 改变会 导致原数据改变 | 改变会 导致原数据改变 | 浅拷贝 | 是 | 改变不会 导致原数据改变 | 改变会 导致原数据改变 | 深拷贝 | 是 | 改变不会 导致原数据改变 | 改变不会 导致原数据改变 |
---|
赋值
- 1.基本数据类型赋值
基本数据类型的赋值操作是值引用
,相互之间没有影响
var a = '谷底飞龙';
var b = a; // 将 a 赋值给 b
a = '天下无敌'; // 修改 a 的值为 '天下无敌'
console.log(b); // 打印 b 的值,仍为 '谷底飞龙'
- 2.引用数据类型赋值
引用数据类型的赋值是地址引用
,两个变量指向同一个地址,相互之间有影响
var a = {
name: '谷底飞龙'
};
var b = a; // 将 a 赋值给 b
a.name = '天下无敌'; // 修改 a.name 的值为 '天下无敌'
console.log(b.name); // 打印 b.name 的值,变为 '天下无敌'
在开发过程中,我们有时候并不希望引用数据类型的赋值操作中的两个变量相互影响,这就需要浅拷贝
和深拷贝
浅拷贝
如果属性是基本数据类型,拷贝的就是基本类型的值。 如果属性是引用数据类型,拷贝的是引用类型的内存地址
如何实现一个浅拷贝?
为了更直观的理解浅拷贝及其特点,我们来手动实现一个浅拷贝函数
// 对 obj 进行浅拷贝
function shallowCopy(obj) {
if (typeof obj === 'object' && obj !== null) {
let copy = Array.isArray(obj) ? [] : {};
// 遍历原对象 obj,将第一层属性赋值给新对象
for (var p in obj) {
copy[p] = obj[p]
}
// 返回的新对象就是浅拷贝后的对象
return copy
} else {
// 如果是基本类型,直接返回
return obj
}
}
我们可以看到,浅拷贝只拷贝了原数据的第一层属性。现在来验证下前面表格中的内容:
- 如果第一层属性是基本数据类型,改变新对象
不会
导致原数据发生改变; - 如果第一层属性是引用数据类型,改变新对象
会
导致原数据发生改变
// 原数据是对象
var obj = {
color: 'red',
person: {
name: '谷底飞龙',
age: 28,
},
}
// 浅拷贝
var copy = shallowCopy(obj);
// 改变原数据第一层属性 color (基本数据类型)
copy.color = 'yellow';
// 改变原数据第一层属性 person(引用数据类型)
copy.person.name = '天下无敌';
// 原数据中 color 仍为 “red“,但是 name 会被修改为 “天下无敌”
console.log(obj);
执行后的结果如下:
如果把原数据换成数组,效果也是一样的,如下
// 原数据是数组
var obj = [
'red',
{
name: '谷底飞龙',
age: 28,
},
]
// 浅拷贝
var copy = shallowCopy(obj);
// 改变原数据第一层属性 color (基本数据类型)
copy[0] = 'yellow';
// 改变原数据第一层属性 person(引用数据类型)
copy[1].name = '天下无敌';
// 原数据中 color 仍为 “red“,但是 name 会被修改为 “天下无敌”
console.log(obj);
执行后的结果如下:
如果原数据是基本数据类型呢?
- 之前讲
赋值
的时候,提到浅拷贝
和深拷贝
是为了解决引用数据类型(复杂数据类型)赋值存在的问题才引入的。所以如果原数据是基本数据类型,一般只需要赋值操作就行。如果用上面实现的浅拷贝函数shallowCopy()
来拷贝基本数据类型,直接返回原数据。
有哪些常用的浅拷贝?
在实际开发中,我们很少需要自己去写一个浅拷贝函数,这里例举几个常用的浅拷贝
1.对象 Object.assign()
在 ES6 提供了Object.assign(target, ...sources)
来实现浅拷贝,第一个参数 target
是目标对象,后面的参数...sources
是原对象。我们现在来改造下前面的案例
// 原数据是对象
var obj = {
color: 'red',
person: {
name: '谷底飞龙',
age: 28,
},
}
// 改用 Object.assign() 实现浅拷贝
var copy = Object.assign({},obj);
// 改变原数据第一层属性 color (基本数据类型)
copy.color = 'yellow';
// 改变原数据第一层属性 person(引用数据类型)
copy.person.name = '天下无敌';
// 原数据中 color 仍为 “red“,但是 name 会被修改为 “天下无敌”
console.log(obj);
执行结果与shallowCopy()
效果一样
注:
Object.assign() 一般用于对象的浅拷贝,用来处理数组时,会把数组视为对象。
// Object.assign 把数组视为属性名为 0、1、2 的对象
// 源数组的 0 号属性 7 覆盖了目标数组的 0 号属性1,以此类推
Object.assign([1,2,3], [7,8]); // [7,8,3]
Object.assign([1,2], [6,7,8]); // [6,7,8]
2.数组 Array.concat()
对数组的浅拷贝,可以使用Array.concat()
,这也是合并数组比较常用的方式。我们把之前的案例修改下
// 原数据是数组
var obj = [
'red',
{
name: '谷底飞龙',
age: 28,
},
]
// 改用 [].concat() 实现浅拷贝
var copy = [].concat(obj);
// 改变原数据第一层属性 color (基本数据类型)
copy[0] = 'yellow';
// 改变原数据第一层属性 person(引用数据类型)
copy[1].name = '天下无敌';
// 原数据中 color 仍为 “red“,但是 name 会被修改为 “天下无敌”
console.log(obj);
执行结果与shallowCopy()
效果一样
3.扩展运算符 { ...obj }
使用扩展运算符{ ...obj }
可以对数组和对象进行浅拷贝,我们这里用原数据是对象的情况来做案例,你也可以自己把原数据换成数组试试。
// 原数据是对象
var obj = {
color: 'red',
person: {
name: '谷底飞龙',
age: 28,
},
}
// 改用扩展运算符 {...obj} 实现浅拷贝
var copy = {...obj};
// 改变原数据第一层属性 color (基本数据类型)
copy.color = 'yellow';
// 改变原数据第一层属性 person(引用数据类型)
copy.person.name = '天下无敌';
// 原数据中 color 仍为 “red“,但是 name 会被修改为 “天下无敌”
console.log(obj);
执行效果与其它浅拷贝方式的是一样的
深拷贝
浅拷贝只对第一层属性进行拷贝,会存在一个问题:如果第一层属性是引用类型,拷贝的是地址,浅拷贝会导致原数据被修改。那怎么解决这个问题呢,这就需要深拷贝
了。
如何实现一个深拷贝?
1.浅拷贝+递归
浅拷贝只对第一层属性进行拷贝,深拷贝需要拷贝到最后一层(直到属性是基本类型为止)
// 递归浅拷贝
function recursiveShallowCopy(obj) {
var copy = Array.isArray(obj) ? [] : {};
for (let p in obj) {
if (typeof obj[p] === 'object') {
// 对象类型,继续递归浅拷贝
copy[p] = recursiveShallowCopy(obj[p]);
} else {
copy[p] = obj[p];
}
}
return copy;
}
// 深拷贝
function deepCopy(obj) {
if (typeof obj === 'object' && obj !== null) {
// 如果是引用类型,进行递归浅拷贝
return recursiveShallowCopy(obj);
} else {
// 如果是基本类型,直接返回
return obj;
}
}
我们现在用上面创建的深拷贝函数 deepCopy()
来验证下前面表格总结的内容:
- 深拷贝时,无论第一层属性是基本类型还是引用类型,修改新对象都不会影响原数据。
// 原数据是对象
var obj = {
color: 'red',
person: {
name: '谷底飞龙',
},
}
// 改用 deepCopy() 实现深拷贝
var copy = deepCopy(obj);
// 改变原数据第一层属性 color (基本数据类型)
copy.color = 'yellow';
// 改变原数据第一层属性 person(引用数据类型)
copy.person.name = '天下无敌';
// 原数据中 color 仍为 “red“,name 仍为 “谷底飞龙”
console.log(obj);
执行上面案例,打印结果如下:
对数组的操作结果也是一样的,这里就不重复写了。
2.JSON.parse(JSON.stringify())
这是目前比较常用的深拷贝方式。我们把之前的案例修改为用 JSON.parse(JSON.stringify())
来实现深拷贝验证下:
// 原数据是对象
var obj = {
color: 'red',
person: {
name: '谷底飞龙',
},
}
// 改用 JSON.parse(JSON.stringify()) 实现深拷贝
var copy = JSON.parse(JSON.stringify(obj));
// 改变原数据第一层属性 color (基本数据类型)
copy.color = 'yellow';
// 改变原数据第一层属性 person(引用数据类型)
copy.person.name = '天下无敌';
// 原数据中 color 仍为 “red“,name 仍为 “谷底飞龙”
console.log(obj);
执行结果与用递归+浅拷贝
实现的deepCopy()
效果是一样的
深拷贝的注意事项
使用JSON.parse(JSON.stringify()
实现深拷贝有一些缺陷:
先来验证下前 3 个缺陷:
// 原数据是对象
var obj = {
color: 'red',
person: {
name: null,
age: function(){}, // 被丢失
country: undefined, // 被丢失
love: Symbol(), // 被丢失
time: new Date(),// 转换成 字符串
height: /[1-9][0-9]?/, // 转换成空对象 {}
},
}
// 使用 JSON.parse(JSON.stringify() 实现深拷贝
var copy = JSON.parse(JSON.stringify(obj));
console.log(copy)
执行后,会发现原对象中的属性 age(函数)、country(undefined)、love(Symbol)被丢失
,time(Date)被转换成字符串
,height(正则)被转换为空对象 {}
。打印结果如下
我们再来验证下使用JSON.parse(JSON.stringify())
实现深拷贝,会抛弃原对象的constructor
,都会被转换成Object
function Person() {
this.name = '谷底飞龙'
}
var obj = {
person: new Person()
}
console.log('原对象:')
console.log(obj)
var copy = JSON.parse(JSON.stringify(obj))
console.log('新对象:')
console.log(copy)
深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 Object
。执行结果如下:
使用JSON.parse(JSON.stringify()
实现深拷贝的前 4 个缺陷,可以通过递归+浅拷贝
的深拷贝方式解决,但递归+浅拷贝
不能解决第 5 点循环引用的问题。
参考
- 详细解析赋值、浅拷贝和深拷贝的区别
- javascript深拷贝、浅拷贝和循环引用深入理解
结语
写文章不易,花了我足足两天时间写的,如果你喜欢这篇文章,可以帮忙点个赞
~
也可以关注我的公众号 「谷底飞龙」~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!