JS深浅拷贝的基础知识
js实现深浅拷贝的理论基础是,不同的数据类型在内存中存储的方式不同
js数据类型分为:基本数据类型,引用数据类型
基本数据类型:string
、number
、boolean
、null
、undefined
、(Symbol)
引用数据类型:object
(object
、array
、function
、date
、RegExp
)
浅拷贝
基本数据类型的浅拷贝,不会有任何副作用和影响,实质是栈的 传值
var a = 100
var b = a
a = 200
console.log(a) // 200
console.log(b) // 100
引用数据类型的浅拷贝,修改任意一方,双方的值都会发生改变,实质是栈的 传址
var a = [1, 2, 3, 4, 5, 6]
var b = a
a[1] = 100
console.log(a) //[1, 100, 3, 4, 5, 6]
console.log(b) //[1, 100, 3, 4, 5, 6]
深拷贝
1 . 直接手动复制,赋值
(无任何副作用,就是很麻烦,递归实现深拷贝就是根据这个原理实现的)
obj1 = {a:1,b:2}
obj2 = {a: obj1.a, b: obj.b}
2 . 扩展运算符 ...
、Object.assign
这两个效果和缺点很相似,放在一起讲
(扩展运算符...
、Object.assign
缺点很明显,修改深层嵌套的对象属性时,源对象也会被修改,所以只适合修改第一层属性)
let obj = {
a: 1,
b: {
b1: 'b1',
},
c: [1, 2, 3],
d: function () {
console.log(0)
},
e: undefined,
}
// let newObj = Object.assign({}, obj)
let newObj = { ...obj }
newObj.a = 'a'
newObj.b.b1 = 2 // 缺点处
newObj.c = [1, 2, 3, 4]
console.log(obj)
// {a: 1, b: {…}, c: Array(3), e: undefined, d: ƒ}
// a: 1
// b: {b1: 2} // 缺点处
// c: (3) [1, 2, 3]
// d: ƒ ()
// e: undefined
console.log(newObj)
// {a: "a", b: {…}, c: Array(4), e: undefined, d: ƒ}
// a: "a"
// b: {b1: 2} // 缺点处
// c: (4) [1, 2, 3, 4]
// d: ƒ ()
// e: undefined
3 . JSON.parse(JSON.stringify(obj))
优点:可以解决展开运算符...
与Object.assign()
的缺陷,对多层嵌套也可以很好的拷贝
缺点:当obj
的key
对应的value
是Object
某些特殊类型时,如Date
对象、RegExp
对象、Function
函数时,会发现这种方法并不能完美的copy
虽然这种方法的缺点很明显,但仍是在已知返回数据
的情况下的最优选择,也是最简单最常用
的方法,一般接口返回的数据中也不会出现函数,日期对象等字段
let obj = {
a: 1,
b: {
b1: 'b1',
},
c: [1, 2, 3],
d: function () { // 为函数时,会出现丢失
console.log(0)
},
e: undefined, // 为undefined时,会出现丢失
f: RegExp('123'), // 为RegExp对象时,会变成{}空对象
g: new Date(), // 为Date对象时,会转换成时间
}
let newObj = JSON.parse(JSON.stringify(obj))
newObj.a = 'a'
newObj.b.b1 = 2
newObj.c[2] = 'c3'
console.log(obj)
console.log(newObj)
4 . 使用第三方的库,如 lodash
var objects = [{ 'a': 1 }, { 'b': 2 }];
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false
5 . 递归实现深拷贝
递归实现深拷贝的原理就是,利用递归,和手动赋值的形式,实现深拷贝,本质就是代码帮你执行了上面说到的第一种深拷贝的方法
实现思路:
(1.)如果是基本数据类型,直接返回
(2.)不是基本数据类型,判断是数组还是对象,并创建一个空数组或对象
(3.)如果是数组,循环数组对象,挨个将数组的值复制给刚才创建的空数组,如果子项是基本数据类型,直接复制,否则递归调用该方法
(4.)如果是对象,先用hasOwnProperty
检测其自身属性,再执行(3)
(5.)返回刚开始定义的数组或对象
知识点:
1 . JS 判断数据类型的三种方法,这里使用的是第三种
2 . Array.prototype.slice 参数为负
/**
* 判断变量的类型
* @param {object} value 变量值
*/
function checkType(value) {
// slice(start,end) 自start到end(end不包括)
// 序号从0开始,表示从第9位开始到倒数一位,也就是最后一位之前的截取
return Object.prototype.toString.call(value).slice(8, -1)
}
// [object Undefined]
// 截取后 Undefined
// console.log(checkType(undefined))
/**
* 深拷贝(递归)
* @param {*} sourceValue 需要拷贝的值
*/
function deepClone(sourceValue) {
// 如果传入的数据是简单类型(不是 {} & []),直接返回即可
if (typeof sourceValue !== 'object') {
return sourceValue
}
// 判断 传入参数的数据类型(object or array)
let targetType = checkType(sourceValue)
// 根据传入参数的数据类型,创建 初始存储结果的变量类型 {} or []
let result = targetType === 'Object' ? {} : []
// 遍历 sourceValue (for...in可以遍历数据和对象)
// 避免数组内有自定义属性,遍历数组使用 for...of,遍历对象 for...in
if (targetType === 'Array') {
// 传入参数是数组时,次数使用的是 forEach 遍历,当然,也可以使用 数组的其他遍历方法
sourceValue.forEach((value, index) => {
let itemType = checkType(value)
// 如果 value 是 数组 或 对象,则继续遍历
if (itemType === 'Object' || itemType === 'Array') {
result[index] = deepClone(value)
} else {
// 如果 value 是 基本数据类型 或者 函数,直接赋值即可
result[index] = value
}
})
} else {
// 传入参数是对象时
for (const key in sourceValue) {
// 遍历数组时,key 为数组的 下标
// 遍历对象时,key 为对象的 key
// hasOwnProperty 只能检验对象自身的属性,不能检验继承属性,也不能检验原型链上的属性
if (sourceValue.hasOwnProperty(key)) {
const item = sourceValue[key]
let itemType = checkType(item)
// 如果 value 是 数组 或 对象,则继续遍历
if (itemType === 'Object' || itemType === 'Array') {
result[key] = deepClone(item)
} else {
// 如果 value 是 基本数据类型 或者 函数,直接赋值即可
result[key] = item
}
}
}
}
// 返回 result 即可
return result
}