上周在开发任务中遇到对象的深浅拷贝了,顺便看了下公司的封装的底层类库,又重新把这块整了一下,做个总结,服务端我是用.net的,就不用C#写了哈,不太通用,面试这道题必备。
JavaScript变量的存储
JS中的变量是在存储在栈内存和堆内存中的。
-
基本类型存储在栈内存中,栈中直接存放数据
-
引用类型变量分配在栈内存中,但是栈内存中只存放指向堆内存特定数据空间的指针(地址),实际数据在堆中存放
赋值和深浅拷贝
深浅拷贝只针对于引用类型,此处的赋值也考虑引用类型。
-
赋值:将原对象栈中的指向堆内存的地址赋值给新对象,两个对象指向堆中的同一块地址,所以将会产生连锁反应
-
浅拷贝:按位拷贝,创建一个新的对象,遍历旧对象的每一个属性,如果是值类型直接复制地址,如果是引用类型,复制旧对象该引用类型存放堆内存的地址。所以,修改新对象的值类型属性,旧对象不会发生变化;修改新对象的引用类型属性,旧对象会发生变化。
-
深拷贝:遍历旧对象的每一个属性,遇到引用类型的数据时,就重新创建一个新的对象,将旧对象的每个属性复制到新对象的属性中,若遇到该属性中仍旧含有引用类型属性,继续创建新对象复制,直到遍历全为值类型为止;新旧对象完全没有任何的关联。
实现方式
浅拷贝
手工实现
let foo = {
name: "zhangsan",
age: 15,
list: [1, 2, 3]
}
let f1 = shallowCopy(foo)
f1.name = "lisi"
f1.list[2] = 'wo'
console.log(foo)
console.log(f1)
function shallowCopy(instance) {
let newInstance = {}
for (let key in instance) {
if (!newInstance.hasOwnProperty(key)) {
newInstance[key] = instance[key]
}
}
return newInstance
}
Object.Assign(target,source)
parm1:target 目标对象 source源对象 assign方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象
注:target中只有一级属性对象深拷贝,含有二级属性及更深级属性都是对象浅拷贝
- 只有一级属性
let foo = { name: 'jack', age: 12, } //这里的一级属性 指的是name age let assignInstance = Object.assign({}, foo) //修改值类型数据 assignInstance.age = 18
- 二级属性 浅拷贝
-
更高级属性 浅拷贝
let foo = { name: 'jack', age: 12, list: [1, [2, 22], 3, 4] } //这里的一级属性 指的是name age list let assignInstance = Object.assign({}, foo) //修改值类型数据 assignInstance.age = 30 //修改引用类型数据 修改list的某一项中的数组 属于对象的三级属性 assignInstance.list[1] = ['二'] console.log('foo', foo) console.log('assignInstance', assignInstance) console.log(foo.list === assignInstance.list) console.log(foo.list[1] === assignInstance.list[1])
数组相关的三个不修改原数组的方法
这个不太好直接定义是浅拷贝还是深拷贝,如果真的要分的话,也要分情况。比如说,数组的每一项元素都是值类型,那么数组调用这几个方法就是深拷贝(实际也可以看做浅拷贝嘛,都是基本类型)。当数组中还有数组的情况下,数组调用这几个方法就是浅拷贝(这个毫无争议)。 总的就是说,遍历数组每一项,遇到值类型会将值类型数据复制一份到新数组的原位置中,遇到数组会将原数组这一项实际堆中的地址复制一份给新数组的这一项存储栈中,不会深层次的遍历拷贝。
1.Array.slice()数组切片方法
- 一层的情况
let testArr = [1, 2, 3] let newArr = testArr.slice() newArr[1] = '二' console.log('testArr', testArr) console.log('newArr', newArr)
- 多层级的情况
let testArr = [1, [2, 22], 3] let newArr = testArr.slice() newArr[1][1] = '二十二' console.log('testArr', testArr) console.log('newArr', newArr)
2.Array.concat()数组合并方法
3.Array.map()数组映射方法
后两个情况与第一个方法情况一致,不再赘述。
注意map函数参数要传递一个function,可以写成Array.map(x=>x)映射一份原数组。
深拷贝
- 使用序列化和反序列化() JSON.stringify和JSON.parse()
let foo = { name: 'zhangsan', age: 30, son: { name: 'xiaozhang', age: 5 } } let newFoo = JSON.parse(JSON.stringify(foo)) newFoo.name = 'ZhangSan' newFoo.son.name = 'XiaoZhang' console.log('foo', foo) console.log('newFoo', newFoo)
弊端: function不能被拷贝;我们可以打印下序列化后的字符串,里面就没有包含show方法。
-
使用轮子,Lodash.cloneDeep() 简单、高效
Lodash库是一个非常高效的原生JavaScript库,重写了许多原生的方法必须map、fill、contact等等
-
jQuery.extend([deep], target, object1, [objectN])
https://jquery.cuishifeng.cn/jQuery.extend.html 文档讲的很清楚哈 -
手写递归
const TypeName = Object.freeze({ ARRAY: 'Array', OBJECT: 'Object' }) class Util { static getTypeFullName(obj) { return Object.prototype.toString.call(obj).slice(8, -1) } static deepClone(source) { const _ = this.getTypeFullName(source) let temp = null switch (_) { case TypeName.ARRAY: temp = [] break; case TypeName.OBJECT: temp = {} break; default: break; } for (const key in source) { if (!temp.hasOwnProperty(key)) { const typeName = this.getTypeFullName(source[key]) if (typeName === TypeName.ARRAY || typeName === TypeName.OBJECT) { temp[key] = this.deepClone(source[key]) } else { temp[key] = source[key] } } } return temp } } const foo = { name: 'zhangsan', age: 18, list: [1, 2, 3], son: { list: [4, [5, 6], 7] } } const newFoo = Util.deepClone(foo) newFoo.list[1] = '二' newFoo.son.list[1][0] = '五' console.log('foo', foo) console.log('newFoo', newFoo)