本文不深究js数据类型的存储,以下探讨仅在了解底层原理基础之上
深拷贝
深拷贝:深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
以下不支持,源数组的属性值是一个 json 对象时,并且直接使用索引修改某一 json 属性这种情况(不支持数组索引修改)
类似这样:
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
经过copy i 得到 c 之后
c[0].kk = 3
以下支持,源数组的属性值是一个 json 对象时,并且直接使用索引修改某一 json 属性这种情况
-
lodash 引入 _.cloneDeep 完美解决
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
let c = _.cloneDeep(i)
console.log('相等吗==>', i === c) // false
c.kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
c[0].kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
-
JSON.parse 和 JSON.stringify 组合使用,可以解决 (有弊端)
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
let c = JSON.parse(JSON.stringify(i))
console.log('相等吗==>', i === c) // false
c.kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
c[0].kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
弊端如下:
它的主要缺点是,只限于处理可被 JSON.stringify() 编码的值。
JSON.stringify() 将编码 JSON 支持的值。包含 Boolean,Number,String,以及对象,数组。其他任何内容都将被特殊处理。
undefined,Function,Symbol 时,它被忽略掉 Infinity,NaN 会被变成 null Date 对象会被转化为 String (默认调用date.toISOString())
问:为什么JSON.stringify() 编码 JSON 支持的值那么少呢?
因为JSON是一个通用的文本格式,和语言无关。设想如果将函数定义也stringify的话,如何判断是哪种语言,并且通过合适的方式将其呈现出来将会变得特别复杂。特别是和语言相关的一些特性,比如JavaScript中的Symbol。
-
自己造轮子,完美解决深拷贝
如果源数组是个对象的引用,也不会拷贝这个对象的引用到新的数组。(支持数组索引修改)
function copy(obj) {
if (!obj || typeof obj !== 'object') {
return
}
var newObj = obj.constructor === Array ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object') {
newObj[key] = copy(obj[key])
} else {
newObj[key] = obj[key]
}
}
}
return newObj
}
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
let c = copy(i)
console.log('相等吗==>', i === c) // false
c.kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
c[0].kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
浅拷贝
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
-
Object.assign(target, source)
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
let c = Object.assign(i)
console.log('相等吗==>', i === c) // true
c.kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
c[0].kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
-
直接赋值(=)
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
let c = i
console.log('相等吗==>', i === c) // true
c.kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
c[0].kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
-
[...] es6解构赋值
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
let [...c] = i
console.log('相等吗==>', i === c) false
c.kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
c[0].kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
疑问:既然是深拷贝,为什么面对通过数组索引修改属性的时候就会影响到原来数据呢?
实际上这种 copy 方法只是拷贝了最外层的数据,对于包含的内层对象还是修改的是栈内存中原对象的副本,并没有修改指向堆内存中的同一个对象的指针。(粗略解释,可以戳文末详细了解一下)
-
concat()
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
let c = i.concat()
console.log('相等吗==>', i === c) false
c.kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
c[0].kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
-
slice
let i = [{
aa: 1,
bb: 2
},{
aa: 1,
bb: 2
}]
let c = i.slice(0)
console.log('相等吗==>', i === c) // false
c.kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]
c[0].kk = 3
console.log('i==>', i) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
console.log('c==>', c) // [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]
-
自己造轮子,完美解决浅拷贝
function copy(obj) {
if (!obj || typeof obj !== 'object') {
return
}
var newObj = obj.constructor === Array ? [] : {}
for (var key in obj) {
newObj[key] = obj[key]
}
return newObj
}
深拷贝和浅拷贝的区别与解释
在javaScript中,有两种数据类型,一种是基本数据类型,一种是引用数据类型。
基本数据 类型是按值访问,也就是说在操作基本数据类型时,直接修改的是变量的值。
而 引用类型 呢,直接按引用来访问的。也就是说js的引用类型(对象类型),是保存在内存中的,JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间,所以我们实际操作的是对象的引用。
因此们赋值对象的时候,是将对象的引用赋值给了另一个对象,实际上变量指的是同一个内存地址中对象的值,这就使得改变其中一个对象的值,另一个也会跟着改变,这就是 浅拷贝。
而深拷贝呢,会开辟一个新的内存地址来存放新的对象的值,两个对象对应两个不同的地址,修改其中一个另一个不会受到影响
总结
-- | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |
参见 sunshine小小倩 的博客详细了解 js数据类型的存储