就得先从基本数据类型和引用数据类型开始了。
基本数据类型与引用数据类型
基本数据类型
string、number、boolean、undefined、null、symbol
也叫值类型,按值存储。存储变量时直接在栈内存中存储值本身。
传递方式:按值传递。
var a = 10;
var b = a;
b = 20;
console.log(a); // 10
console.log(b); // 20
简单数据类型传参只是把值传递了,值与值之间独立存在。在上述例子中,
1)声明变量a
则会在栈内存中开辟一个空间,存放a
的值10。
2)var b = a
,声明变量b
则在栈内存中开辟一个新的空间,并把a
的值10存放到这个空间。
3)b = 20
,找到b
在栈内存中对应的空间,把里面存储的值10改成20。
引用数据类型
通过new关键字创建的对象(系统对象、自定义对象),如Object、Array、Date、Function等
按地址存储。存储变量时存储的仅仅是地址(引用)。
在栈内存中变量保存的是一个指针,指向对应在堆内存中的地址。当访问引用类型的时候,要先从栈中取出该对象的地址指针,然后再从堆内存中取得所需的数据。
传递方式:按引用传递。因为指向的是同一个地址,所以当地址中的数据发生改变,指向该存放地址的所有变量都会发生改变。
function fn(xx){
console.log(xx.name);
xx.name = "Han Meimei";
console.log(xx.name);
}
let p = {
name: 'Li Ming'
}
console.log(p.name) // Li Ming
fn(p); // Li Ming //Han Meimei
console.log(p.name) // Han Meimei
1)声明复杂对象p,在栈中开辟一个空间存放p的地址,地址指向堆中的真实数据:
2)fn(p), 传参即为xx=p。由于p存的是地址,因此xx=p是把p的地址传给xx,xx和p则指向了同一个地址:
3)xx.name = "Han Meimei",改变的是xx的地址所指向的堆内存中存放的数据,因此也同时改变了p:
浅拷贝和深拷贝的区别
-
浅拷贝:
浅拷贝只拷贝一层。如果属性是基本类型,拷贝的就是基本类型的值;如果一个键的值是复合类型的值(数组、对象、函数),拷贝的则是这个值的引用(地址),而不是这个值的副本,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
只是将数据中存放的引用拷贝下来,依旧指向同一个存放地址。
-
深拷贝:
深拷贝是将一个对象从内存中完整的拷贝一份出来, 从堆内存中开辟一个新的区域存放新对象, 且修改新对象不会影响原对象。
将数据中所有的数据拷贝下来,而不是引用,若对拷贝下来的数据进行修改,并不会影响原数据。
浅拷贝
使用下面这些函数得到的都是浅拷贝:
Object.assign
- 使用扩展运算符实现的复制
-
Array.prototype.slice()
,Array.prototype.concat()
// 需求: 浅拷贝一份 obj 到对象 targetObj
const obj = {
id:001,
name:'andy',
msg:{
age:18
}
}
// 需求:浅拷贝一份 arr 到对象 targetArr
let arr = [1, 2, { name:'qq' }]
1:Object.assign(对象)
Object.assign()
方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
const targetObj = {}
Object.assign(targetObj, obj)
console.log(targetObj); // {id: 1, name: "andy", msg: {age: 18}}
2:使用扩展运算符(对象)
扩展运算符(...
)用于取出参数对象的所有可遍历属性,并将其复制到当前对象之中。
const targetObj = { ...obj };
// 或
const { ...targetObj } = obj; //解构赋值
console.log(targetObj); // {id: 1, name: "andy", msg: {age: 18}}
3:for...in(对象)
const targetObj = {}
for (const key in obj) {
targetObj[key] = obj[key] // 主要是因为这一步,对于复杂类型,存放的都是指针,赋值的时候得到的也是指针
}
console.log(targetObj); // {id: 1, name: "andy", msg: {age: 18}}
因为浅拷贝的是引用(地址),因此对于复合类型的值(数组、对象、函数),原本和副本都指向同一地址,改变一个会影响另一个。如下代码中,对于基本数据类型,改变targetObj.name
, 并不影响源对象,而属性msg
是个对象,拷贝的是这个对象的引用(地址),因此targetObj.msg.age = 8
也同时改变了obj.msg.age
。
targetObj.name = 'mike';
targetObj.msg.age = 8;
console.log(targetObj); // {id: 1, name: "mike", msg: {age: 8}}
console.log(obj); // {id: 1, name: "andy", msg: {age: 8}}
4:使用扩展运算符(数组)
let targetArr = [...arr]
5:Array.prototype.slice()
let targetArr = arr.slice(0)
6:Array.prototype.concat()
let targetArr = arr.concat()
同样地,对于数组的每一项拷贝的都是引用,对于值类型,原本和副本互相独立,而对于复杂类型,改变一个会影响另一个。
targetArr[0] = 88;
targetArr[2].name = 'test';
console.log(arr); // [1, 2, { name:'test' }]
console.log(targetArr); // [88, 2, { name:'test' }]
深拷贝
深拷贝的实现
JSON.parse(JSON.stringify())
- 手写递归函数
- 函数库lodash
实现1:使用JSON.parse和JSON.stringify
const targerObj = JSON.parse(JSON.stringify(obj))
// JSON.stringify()首先把obj转化成字符串的形式,字符串在存储的时候会单独开辟空间,再通过JSON.parse()转回成对象,这个过程就断掉了和原始值之间的关系
JSON.parse(JSON.stringify())
存在以下问题:
- 无法解决循环引用问题
- 无法拷贝特殊的对象,比如:RegExp, Date, Set, Map等在序列化的时候会丢失。
- 无法拷贝函数
这是因为利用JSON.stringify( )
序列化时,所有函数及原型成员都会被有意忽略,不体现在结果中。此外,值为undefined
的任何属性也都会被跳过。得到的正则也不再是正则了。
const obj = {
id: 1,
name: undefined,
fn: function (a, b) {
return a + b
},
msg: {
age: 18
},
color: ['pink', 'red'],
}
console.log(JSON.stringify(obj)) // {"id":1,"msg":{"age":18},"color":["pink","red"]}
实现2:手写递归函数
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
实现3:函数库lodash的_.cloneDeep方法
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
总结
和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含对象 | |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |