数据类型
在开始拷贝之前,我们从JavaScript的数据类型和内存存放地址讲起。
数据类型分为基本数据类型 和引用数据类型
基本数据类型主要包括undefined,boolean,number, string,null。
基本数据类型主要存放在栈(stack),存放在栈中的数据简单,大小确定。存放在栈内存中的数据是直接按值存放的,是可以直接访问的。
基本数据类型的比较是值的比较,只要它们的值相等就认为它们是相等的。
let a = 1
let b = 1
console.log(a === b) // true
这里使用严格相等,主要是为了==会进行类型转换。
let a = true
let b = 1
console.log(a == b) // true
引用数据类型也就是对象类型Object type,比如object,array, function,date等。
引用数据类型是存放在堆(heap)内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。
引用数据类型的比较是引用的比较
所以我们每次对js中的引用类型进行操作的时候,都是操作其保存在栈内存中的指针,所以比较两个引用数据类型,是看它们的指针是否指向同一个对象。
let foo = {a: 1, b: 2}
let bar = {a: 1, b: 2}
console.log(foo === bar) // false
虽然变量foo和变量bar所表示的内容是一样的,但是其在内存中的位置不一样,也就是变量foo和bar在栈内存中存放的指针指向的不是堆内存中的同一个对象,所以它们是不相等的。
栈和堆的区别
其实浅拷贝和深拷贝的主要区别就是数据在内存中的存储类型不同。
栈和堆都是内存中划分出来用来存储的区域。
栈(stack) 是自动分配的内存空间,由系统自动释放;
堆(heap) 则是动态分配的内存,大小不定也不会自动释放。
浅拷贝
如果你的对象只有值类型的属性,可以用ES6的新语法“解构赋值”或者Object.assign(...)实现拷贝
//浅拷贝
let obj = {foo: 'foo', bar: 'bar'}
let shallowCopy = { ...obj } // {foo: 'foo', bar: 'bar'}
//浅拷贝
let obj = {foo: 'foo', bar: 'bar'}
let shallowCopy = Object.assign({}, obj) // {foo: 'foo', bar: 'bar'}
我们接着来看下浅拷贝和赋值(=) 的区别
let obj = {foo: 'foo', bar: 'bar'}
let shallowCopy = { ...obj } // {foo: 'foo', bar: 'bar'}
let obj2 = obj // {foo: 'foo', bar: 'bar'}
shallowCopy.foo = 1
obj2.bar = 1
console.log(obj) // {foo: 'foo', bar: 1}
可以看出赋值得到的obj2和最初的obj指向的是同一对象,改变数据会使原数据一同改变。
而浅拷贝得到的shallowCopy则将obj的第一层数据对象拷贝到了,和源数据不指向同一对象,改变不会使原数据一同改变。
深拷贝
但是Object.assign(...)方法只能进行对象的一层拷贝。对于对象的属性是对象的对象,他不能进行深层拷贝。
迷糊了吧?直接代码解释
let foo = {a: 0, b: {c: 0}}
let copy = { ...foo }
copy.a = 1
copy.b.c = 1
console.log(copy) // {a: 1, b: {c: 1}}
console.log(foo) // {a: 0, b: {c: 1}}
可以看到,使用Object.assign(...)方法拷贝的copy对象的二层对象发生改变的时候,依然会使原数据一同改变。
这里,对存在子对象的对象进行拷贝的时候,就是深拷贝了。
浅拷贝:将B对象拷贝到A对象中,不包括B里面的子对象
深拷贝:将B对象拷贝到A对象中,包括B里面的子对象
深拷贝实现的方法:
这里只说几种常用方法
1.JSON.parse(JSON.stringify( ))
let foo = {a: 0, b: {c: 0}}
let copy = JSON.parse(JSON.stringify(foo))
copy.a = 1
copy.b.c = 1
console.log(copy) // {a: 1, b: {c: 1}}
console.log(foo) // {a: 0, b: {c: 0}}
2.递归拷贝
function deepCopy (initialObj, finalObj) {
let obj = finalObj || {}
for (let i in initialObj) {
if (typeof initialObj[i] === 'object') {
obj[i] = (initialObj[i].constructor === Array) ? [] : {}
arguments.callee(initialObj[i], obj[i])
} else {
obj[i] = initialObj[i]
}
}
return obj
}
var foo = {a: 0, b: {c: 0}}
var str = {}
deepCopy(foo, str)
str.a = 1
str.b.c = 1
console.log(str) // {a: 1, b: {c: 1}}
console.log(foo) // {a: 0, b: {c: 0}}
上述代码确实可以实现深拷贝,但是当遇到两个互相引用的对象,会出现死循环的情况。
为了避免相互引用的对象导致死循环的情况,则应该在遍历的时候判断是否相互引用对象,如果是则退出循环。
改进版代码如下:
function deepCopy (initialObj, finalObj) {
let obj = finalObj || {}
for (let i in initialObj) {
let prop = initialObj[i] // 避免相互引用导致死循环,如initialObj.a = initialObj的情况
if (prop === obj) {
continue
}
if (typeof prop === 'object') {
obj[i] = (prop.constructor === Array) ? [] : {}
arguments.callee(prop, obj[i])
} else {
obj[i] = prop
}
}
return obj
}
var foo = {a: 0, b: {c: 0}}
var str = {}
deepCopy(foo, str)
str.a = 1
str.b.c = 1
console.log(str) // {a: 1, b: {c: 1}}
console.log(foo) // {a: 0, b: {c: 0}}
3.使用Object.create( )方法
function deepCopy (initialObj, finalObj) {
let obj = finalObj || {}
for (let i in initialObj) {
let prop = initialObj[i] // 避免相互引用对象导致死循环,如initialObj[i].a = initialObj的情况
if (prop === obj) {
continue
}
if (typeof prop === 'object') {
obj[i] = (prop.constructor === Array) ? [] : Object.create(prop)
} else {
obj[i] = prop
}
}
return obj
}
let foo = {a: 0, b: {c: 0}}
let str = {}
deepCopy(foo, str)
str.a = 1
str.b.c = 1
console.log(str) // {a: 1, b: {c: 1}}
console.log(foo) // {a: 0, b: {c: 0}}
4.jQuery
jQuery提供了一个$.extend可以实现深拷贝
var $ = require('jquery)
let foo = {a: 0, b: {c: 0}}
let str = $.extend(true, {}, foo)
str.a = 1
str.b.c = 1
console.log(str) // {a: 1, b: {c: 1}}
console.log(foo) // {a: 0, b: {c: 0}}
5.lodash
另一个很热门的函数库lodash,也有提供_.cloneDeep用来深拷贝
var _ = require('lodash)
let foo = {a: 0, b: {c: 0}}
let str = _.cloneDeep(foo)
str.a = 1
str.b.c = 1
console.log(str) // {a: 1, b: {c: 1}}
console.log(foo) // {a: 0, b: {c: 0}}
局限性
所有深拷贝的方法并不适用于所有类型的对象。当然还有其他的坑,像是如何拷贝原型链上的属性?如何拷贝不可枚举属性等等。
虽然lodash是最安全的通用深拷贝方法,但如果你自己动手,可能会依据需求写出最适合你的更高效的深拷贝的方法:
// 适用于日期的简单深拷贝的例子
function deepCopy(obj) {
let copy
// 处理三种简单的引用数据类型加上undefined和null
if (obj == null || typeof obj != "object") return obj
// 处理Date
if (obj instanceof Date) {
copy = new Date()
copy.setTime(obj.getTime())
return copy
}
// 处理Array
if (obj instanceof Array) {
copy = []
for(let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i])
}
return copy
}
// 处理Function
if (obj instanceof Function) {
copy = function() {
return obj.apply(this, arguments)
}
return copy
}
// 处理Object
if (obj instanceof Object) {
copy = {}
for (let attr in obj) {
if(obj.hasOwnProperty(attr)) copy[attr] = deepCopy(obj[attr])
}
return copy
}
throw new Error("无法深拷贝" +obj.constructor+ "类型的数据")
}
快乐拷贝😁
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。