一、基本类型和引用类型
1、ECMAScript 中的变量类型分为两类:
① 基本类型
undefined, null, 布尔值(Boolean), 字符串(String), 数值(Number),Symbol
② 引用类型
统称为Object类型。细分的话有:Object类型,Array类型,Date类型,Function类型等。
2、不同类型的存储方式:
① 基本数据类型 保存在 栈内存
形式如下:栈内存中分别存储着变量的标识符以及变量的值。
② 引用类型 保存在 堆内存 中
栈内存存储的是变量的标识符以及对象在堆内存中的存储地址。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从对应的堆内存中取得所需的数据。
3、不同类型的复制方式:
① 基本类型的复制
当你在复制基本类型的时候,相当于把值也一并复制给了新的变量。
② 引用类型的复制
当你在复制引用类型的时候,实际上只是复制了指向堆内存的地址。即原来的变量与复制的新变量指向了同一个东西。
二、所谓的深浅拷贝
1、仅仅是复制了引用(地址)。换句话说,复制了之后,原来的变量和新的变量指向同一个东西,彼此之间的操作会互相影响,为 浅拷贝(藕断丝连)。
2、而如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响,为 深拷贝。
3、深浅拷贝的主要区别就是:复制的是引用(地址)还是复制的是实例。
4、由深拷贝的定义来看,深复制要求如果源对象存在对象属性,那么需要进行递归复制,从而保证复制的对象与源对象完全隔离。然而还有一种可以说处在浅复制和深复制的粒度之间,也是jQuery的extend方法在deep参数为false时所谓的“浅复制”,这种复制只进行一个层级的复制。
三、实现深拷贝的方法
1、原理
不是简单的拷贝引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深拷贝的对象的引用不包含任何原有对象。
简单数据类型:直接拷贝值;
引用数据类型:在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址。修改其中一个,另一个不会发生改变。
2、方法一:JSON对象的 parse 和 stringify
JSON.parse(JSON.stringify(obj)) 目前比较常用的深拷贝方法之一
先将JS对象序列化为JSON字符串,再将JSON字符串反序列化为JS对象。改变对象的格式,使其不指向同一个对象。
原理:
JSON 对象是 ES5 中引入的新的类型(支持的浏览器为 IE8+);
parse 方法可以将 JSON 字符串反序列化成 JS 对象;stringify 方法可以将 JS 对象序列化成JSON 字符串。
借助这两个方法,也可以实现对象的深拷贝。
let bbb = [1,[2,3,4]]
let ccc = JSON.parse(JSON.stringify(bbb))
ccc[1][0] = 222
console.log(bbb, ccc) // [1,[2,3,4]] [1,[222,3,4]]
优点:
这个方法使用较为简单便捷,可以满足基本的深拷贝需求(80%),而且能够处理 JSON 格式能表示的所有数据类型。
缺点:
拷贝的对象的值中如果有 undefined、函数、symbol 这些类型,JSON.stringify 序列化之后的字符串中这个键值对会消失
拷贝 Date 引用类型会变成字符串
拷贝 RegExp 引用类型会变成空对象
对象中含有 NaN、Infinity 以及 -lnfinity,JSON 序列化的结果会变成 null
无法拷贝不可枚举的属性、无法拷贝对象的原型链
无法拷贝对象的循环应用,即对象成环(obj[key] = obj)
let a = [{
'aa': undefined,
'bb': [1, 2, 3],
'cc': null,
'dd': new Date(),
'ee': () => {}
}]
let b = JSON.parse(JSON.stringify(a))
console.log(a, b)
打印结果:
a的打印结果:
aa:undefined
bb:[1, 2, 3]
cc:null
dd:Wed Mar 13 2019 09:46:13 GMT+0800 (中国标准时间)
ee:ƒ ee()
b的打印结果:
bb:[1, 2, 3]
cc:null
dd:Wed Mar 13 2019 09:46:13 GMT+0800 (中国标准时间)
3、方法二: jQuery 中的 extend 复制方法
原理:jQuery中的 extend 方法可以用来扩展对象。这个方法可以传入一个参数 deep(true or false),表示是否执行深复制(如果是深复制则会执行递归复制)。
false:浅拷贝(只会进行一个层级的复制)即如果源对象中存在对象属性,那么复制的对象上也会引用相同的对象。这不符合深复制的要求,但又比简单的复制引用的复制粒度有了加深。
缺点:这个方法基本的思路就是如果碰到 array 或者 objec t的属性,那么就执行递归复制。这也导致对于 Date、Function等引用类型,jQuery 的 extend 也无法支持
let a=[0,1,[2,3],4],
b=$.extend(true,[],a);
a[0]=1;
a[2][0]=1;
console.log(a,b); // [1,1,[1,3],4] [0,1,[2,3],4]
4、函数库 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
5、手写实现深拷贝
原理:利用 递归 来实现深复制,对属性中所有引用类型的值,遍历到是基本类型的值为止。
function deepClone(obj) {
let objClone = Array.isArray(obj) ? [] : {};
if (obj instanceof Object === false) return obj;
for (let key in obj) {
const value = obj[key];
// for-in遍历的是原型链,需要用 hasOwnProperty 判断是否是自有属性
if (obj.hasOwnProperty(key)) {
if (value && typeof value === "object") {
// 递归复制
objClone[key] = deepClone(value);
} else {
// 简单复制
objClone[key] = value;
}
}
}
return objClone;
} else {
return obj;
}
}
console.log('使用自定义的深拷贝')
let arrA = [1, 2, 3]
let arrAcopy = deepClone(arrA)
arrAcopy[0] = 111
console.log(arrA, arrAcopy) // (3) [1, 2, 3] (3) [111, 2, 3]
let array2 = [1, [2, 3, 4]]
let array2Copy = deepClone(array2)
array2[0] = 111
console.log(array2, array2Copy) // [111,[2,3,4]] [1,[2,3,4]]
let stringA = 11
let aCopy = deepClone(stringA)
stringA = 22
console.log(stringA, aCopy) // 22 11
let stringB = null
let bCopy = deepClone(stringB)
console.log(bCopy) // null
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringfy 一样,还是有一些问题没有完全解决,例如:
- 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
- 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
- 对象的属性里面成环,即循环引用没有解决;
递归的深度太深,会引发栈内存的溢出
6、解决递归爆栈的问题
将待拷贝的对象放入栈中,循环直至栈为空。
https://www.yuque.com/12312wo/vvtmcs/zo3hfhzk45b7452s#PnQ3J
这样我们就解决了递归爆栈的问题,但是循环引用的问题依然存在。例如:当a对象的中的某属性值为a对象,这样就会造成循环引用。
7、解决循环引用的问题
使用暴力破解的方法来解决。
四、实现浅拷贝的方法
1、原理
复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响
一个新的对象对原始对象的属性值进行精确地拷贝:
如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值;
如果拷贝的是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。
2、方法一:直接赋值
let aa = [1,2,3]
let bb = aa
bb[1] = 22
console.log(aa,bb) // (3) [1, 22, 3] (3) [1, 22, 3]
3、方法二:ES6 拓展运算符 ...
将数组或者对象转为用逗号分隔的参数序列
语法:let cloneObj={...obj}
let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
let arr = [1,2,3,4]
let list = [...arr]
list[0] = '111'
console.log(arr,list)
// arr的打印结果:
// [1, 2, 3, 4]
// list的打印结果:
// ["111", 2, 3, 4]
ES6的拓展运算符,也叫解构赋值,也可以实现数据的拷贝,但是只遍历一层。
针对一维对象和数组可以看做是深拷贝,多维的就是浅拷贝
let arr = [{
name: '小草莓',
hobby: ['a', 'b', 'c']
}]
let list = [...arr]
list[0].hobby = ['a', 'b', 'c', 'd']
list[0].name = 'new 小草莓'
console.log(arr)
console.log(list)
// arr的打印结果:
// [{
// name: 'new 小草莓',
// hobby: ['a', 'b', 'c', 'd']
// }]
// list的打印结果
// [{
// name: 'new 小草莓',
// hobby: ['a', 'b', 'c', 'd']
// }]
缺点:
扩展运算符和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝 Symbol 类型的属性。
let obj={
name:"lala",
[Symbol('key1')]:"yyy"
}
Object.defineProperty(obj,'notShow',{
value:"不可枚举属性",
enumerable:false
})
let obj2={...obj}
console.log(obj2) //{name: 'lala', Symbol(key1): 'yyy'} 没有不可枚举属性
4、方法三:js的 slice() 方法
arrayObj.slice(start, [end])
原理:数组中的 slice() 方法返回指定截取的一段,原数组不改变。当 start 为0素,end 不填则默认复制整个数组。
let c = [1,2,3,4,5]
let d = c.slice(0)
d[2] = 333
console.log(c,d) // (5) [1, 2, 3, 4, 5] (5) [1, 2, 333, 4, 5]
let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false
缺点:Array 的 slice 和 concat 方法都会返回一个新的数组实例,但是这两个方法对于数组中的对象元素却没有执行深复制,而只是复制了引用了,因此这两个方法并不是真正的深复制
let cc = [1,[2,3,4]]
let ee = cc.slice(0)
ee[1][0] = 222
console.log(cc,ee) // [1,[222,3,4]] [1,[222,3,4]]
5、方法四:js的 concat() 方法
arrayObject.concat(arrayX,arrayX,......,arrayX)
原理:数组中的 concat() 方法用于合并两个或多个数组,返回合并之后的数组,原数组不改变。
let e = [1,2,3,4]
let f = e.concat()
f[1] = 222
console.log(e,f) // (4) [1, 2, 3, 4] (4) [1, 222, 3, 4]
let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false
缺点:Array的 slice 和 concat 方法都会返回一个新的数组实例,但是这两个方法对于数组中的对象元素却没有执行深复制,而只是复制了引用了,因此这两个方法并不是真正的深复制
let g = [1,[2,3,4]]
let h = g.concat()
h[1][0] = 222
console.log(g,h) // [1,[222,3,4]] [1,[222,3,4]]
6、方法五:Object.assign(target, …sources)
原理:把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
语法:Object.assign(target, ...sources)。
该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
用途:实现浅拷贝,也可以实现一维对象的深拷贝。
object.assign 是 ES6 中 Object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。
console.log('对象浅拷贝')
let arr = [1,2,3,4]
let obj1 = { a: 1, b: 2, c: 3 }
let obj2 = { a: 11, b: 22, c: 33, d:arr, e: 44 }
let obj3 = Object.assign({}, obj1, obj2)
obj1.a = 'aaa'
obj2.d[0] = 'bbb'
obj2.e = 'eee'
console.log(obj1, obj2, obj3)
// {a: "aaa", b: 2, c: 3}
// {a: 11, b: 22, c: 33, d: ['bb',2,3,4], e: 'eee'}
// {a: 11, b: 22, c: 33, d: ['bb',2,3,4], e: 44}
注意:
- 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
- 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
- 因为
null
和undefined
不能转化为对象,所以第一个参数不能为null
或undefined
,会报错。- 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。
let obj={
name:"lala",
[Symbol('key1')]:"yyy"
}
Object.defineProperty(obj,'notShow',{
value:"不可枚举属性",
enumerable:false
})
let obj2=Object.assign({},obj)
console.log(obj)
console.log(obj2) //{name: 'lala', Symbol(key1): 'yyy'} 没有不可枚举属性
7、手写实现浅拷贝
实现一个浅拷贝的大致思路分为两点:
- 对基础类型做一个最基本的一个拷贝;
- 对引用类型开辟一个新的存储,并且拷贝第一层对象属性。
function shallowCopy(val) {
if (!val || typeof val !== "object") {
return val;
}
let newVal = Array.isArray(val) ? [] : {};
for (let key in val) {
if (val.hasOwnProperty(key)) {
// for-in遍历的是原型链
// 需要用hasOwnProperty判断是否是自有属性
newVal[key] = val[key];
}
}
return newVal;
}
let arr1 = [1, 2];
const arr2 = shallowCopy(arr1);
arr1[1] = 22;
console.log(arr1, arr2); // [1, 22] [1, 2]
let arr11 = [1, [1, 2]];
const arr22 = shallowCopy(arr11);
arr11[1][0] = 11;
console.log(arr11, arr22); // [1, [11, 2]] [1, [11, 2]]
五、自己实现一个拷贝方法,通过传入deep参数来表示是否执行深拷贝
// util作为判断变量具体类型的辅助模块
var util = (function() {
var class2type = {};
[('Null',
'Undefined',
'Number',
'Boolean',
'String',
'Object',
'Function',
'Array',
'RegExp',
'Date')
].forEach(function(item) {
class2type['[object ' + item + ']'] = item.toLowerCase()
})
function isType(obj, type) {
return getType(obj) === type
}
function getType(obj) {
return class2type[Object.prototype.toString.call(obj)] || 'object'
}
return {
isType: isType,
getType: getType
}
})()
function copy(obj, deep) {
// 如果obj不是对象,那么直接返回值就可以了
if (obj === null || typeof obj !== 'object') {
return obj
}
// 定义需要的局部变量,根据obj的类型来调整target的类型
var i,
target = util.isType(obj, 'array') ? [] : {},
value,
valueType
for (i in obj) {
value = obj[i]
valueType = util.getType(value)
// 只有在明确执行深复制,并且当前的value是数组或对象的情况下才执行递归复制
if (deep && (valueType === 'array' || valueType === 'object')) {
target[i] = copy(value)
} else {
target[i] = value
}
}
return target
}
更过例子可参考:js 深克隆(考虑到类型检查,递归爆栈,相同引用,Date和Function等特殊类型克隆,原型克隆)_js克隆对象中的functiion的问题_一袋米要扛几楼_的博客-CSDN博客
六、手写实现深拷贝改进版
https://www.yuque.com/12312wo/vvtmcs/hnbcpp6iuku6g2qb