这是我参与更文挑战的第21天,活动详情查看: 更文挑战
前言
在工作中我们经常会给变量赋值另一个变量,也经常为了不污染原始数据,需要对原始数据做拷贝处理,拷贝又分浅拷贝和深拷贝;你们知道哪些方法是浅拷贝?哪些方法是深拷贝?如果让你手写一个深拷贝,你可以很快写出来吗?下面来讲讲它们。
数据类型
在讲讲拷贝之前,我们先讲讲js的数据类型:
一共有2
种,分为基本类型
和引用类型
- 基本类型:
String
,Number
,Boolean
,Undefined
,Null
,Symbol
(新引入类型),BigInt
(新引入类型) - 引用类型:
Object
(其它的比如Array
,Function
等都属于Object
类型)
基本类型的值是存储在栈内存中,占据固定大小,不可变;
引用类型的值是存储在堆内存中,大小不定,可变,它在栈内存中存储的是固定大小的地址(不可变),指向堆内存中的值;
我们对数据操作一般有三种方式, 赋值
, 浅拷贝
, 深拷贝
。下面一一来分析。
赋值
对基本类型的赋值,就是在栈内存中分配一块新的空间给新的变量,二者互相没有影响。
对引用类型的赋值,就是在栈内存重新复制一份地址给新的变量,用以指向同一个堆内存的值。所以如果堆内存的值改变,二者的值都会改变。
下面可以通过例子来看一下:
let a = '答案cp3'
let b = a
console.log(a, b) // 答案cp3 答案cp3
b = 123
console.log(a, b) // 答案cp3 123
复制代码
由此可见,基本类型的赋值 改变新的变量不会影响原来的变量
let obj = {name: ''}
let obj1 = obj
console.log(obj, obj1) // {name: ''} {name: ''}
obj.name = '答案cp3'
console.log(obj, obj1) // {name: '答案cp3'} {name: '答案cp3'}
复制代码
由此可见,引用类型的赋值,如果改变新的变量,会影响原来的变量,因为他们栈内存指向的是同一个对象值;
浅拷贝
浅拷贝 主要是针对引用类型,是对引用类型的一次复制。如果属性值是基本类型,则是复制基本类型的值,如果属性值是引用类型,则是复制引用类型的栈地址;如果改变对应的堆内存中的值,则双方都会改变。
下面看个例子:
let obj = {name: '答案', info: {age: 18}}
let obj1 = Object.assign({}, obj)
obj1.name = '答案cp3'
obj1.info.age = 20
console.log(obj1, obj)
// obj1: {name: "答案cp3", info: {age: 20}}
// obj: {name: "答案", info: {age: 20}}
复制代码
可以看到浅拷贝如果属性是基本类型,则改变不会影响对方,如果是引用类型,则会影响对方。
深拷贝
深拷贝,也是针对引用类型,是指把引用类型的完全复制,不管属性值是基本类型还是引用类型,深拷贝出来的对象,不管怎么改变自身的属性,不会影响原来的对象,二者是两个独立的互不影响的。
深拷贝一般有2种方式。
-
JSON.stringify
先把对象字符串化,最后再调用
JSON.parse
解析成对象let obj = {name: '答案', info: {age: 18}} let obj1 = JSON.parse(JSON.stringify(obj)) obj1.name = '答案cp3' obj1.info.age = 20 console.log(obj1, obj) // obj1: {name: "答案cp3", info: {age: 20}} // obj: {name: "答案", info: {age: 18}} 复制代码
可以看到是深拷贝是成功的,不管你改变哪个值,都不会影响原来的对象。
但是
JSON.stringify
有个缺点,他对一些属性值就不能拷贝,比如undefined
,function
这些,不能很好的兼容。let obj = {name: undefined, info: {age: 18}} let obj1 = JSON.parse(JSON.stringify(obj)) console.log(obj1, obj) // obj1:{info:{age: 18}} name被忽略了 // obj:{name: undefined, info: {age: 18}} 复制代码
let obj = {name: function () {}, info: {age: 18}} let obj1 = JSON.parse(JSON.stringify(obj)) console.log(obj1, obj) // obj1:{info:{age: 18}} name被忽略了 // obj:{name: function () {}, info: {age: 18}} 复制代码
-
递归遍历复制
这个是值得推荐的,基本能兼容各个数据类型。
function deepClone (target) { if (typeof target !== 'object' || target === null) return target const copyTarget = Array.isArray(target) ? [] : {} for (let key in target) { if (typeof target[key] === 'object') { copyTarget[key] = deepClone(target[key]) } else { copyTarget[key] = target[key] } } return copyTarget } let obj = {name: '答案', info: {age: 18}} let obj1 = deepClone(obj) obj1.name = '答案cp3' obj1.info.age = 20 console.log(obj1, obj) // obj1:{name: ‘答案cp3’, info:{age: 20}} // obj:{name: ‘答案’, info: {age: 18}} let obj = {name: undefined, info: {age: 18}} let obj1 = deepClone(obj) console.log(obj1, obj) // obj1:{name: undefined, info:{age: 18}} // obj: {name: undefined, info: {age: 18}} let obj = {name: function (){}, info: {age: 18}} let obj1 = deepClone(obj) console.log(obj1, obj) // obj1:{name: function (){}, info:{age: 18}} // obj: {name: function (){}, info: {age: 18}} 复制代码
主要原理是利用遍历对象,如果属性值是基本类型,则直接赋值,如果是引用类型,则递归遍历,直到属性值是基本类型。
这里还要考虑循环利用的情况,如果不考虑的话,可能会导致死循环,栈溢出
这时候需要用到
Map
这种数据结构,因为他可以用对象做key, 把已经循环遍历的引用对象用Map
存储起来,下次循环遍历的时候通过Map
判断,如果Map
的key有这个对象则直接return,不继续遍历了。代码如下:
function deepClone (target, map = new Map()) { if (typeof target !== 'object' || target === null) return target if (map.get(target)) return map.get(target) const copyTarget = Array.isArray(target) ? [] : {} map.set(target, copyTarget) for (let key in target) { if (typeof target[key] === 'object') { copyTarget[key] = this.deepClone(target[key], map) } else { copyTarget[key] = target[key] } } return copyTarget } let obj = {name: function (){}, info: {age: 18}} obj.obj = obj let obj1 = deepClone(obj) console.log(obj1, obj) // obj1: {info: {…}, obj: {…}, name: ƒ} // obj: {info: {…}, obj: {…}, name: ƒ} 复制代码
运行成功~
总结
以上就是我总结的js的赋值,浅拷贝和深拷贝的知识,希望对你们有帮助~