深浅拷贝的简单介绍
数据类型
- 基本数据类型的特点:直接存储在栈(stack)中的数据
- String, Number, Boolean, Null, Undefined,Symbol
- Symbol是es6新增的,一般用于去设置对象的属性名,因为它产生的值是唯一的
// 和 Symbol() 不同的是,用 Symbol.for() 方法创建的的 symbol 会被放入一个全局 symbol 注册表中。 // Symbol.for() 并不是每次都会创建一个新的 symbol,它会首先检查给定的 key 是否已经在注册表中了。 // 假如是,则会直接返回上次存储的那个。否则,它会再新建一个。 console.log(Symbol() === Symbol()); // false console.log(Symbol(1) === Symbol(1)); // false console.log(Symbol.for() === Symbol.for()); // true console.log(Symbol.for(1) === Symbol.for(1)); // true console.log(Symbol.for(2) === Symbol.for(1)); // false
- 和 Symbol() 不同的是,用 Symbol.for() 方法创建的的 symbol 会被放入一个全局 symbol 注册表中。
- Symbol.for() 并不是每次都会创建一个新的 symbol,它会首先检查给定的 key 是否已经在注册表中了。
- 假如是,则会直接返回上次存储的那个。否则,它会再新建一个。
Symbol.for("foo"); // 创建一个 symbol 并放入 symbol 注册表中,键为 "foo" Symbol.for("foo"); // 从 symbol 注册表中读取键为"foo"的 symbol Symbol.for("bar") === Symbol.for("bar"); // true,证明了上面说的 Symbol("bar") === Symbol("bar"); // false,Symbol() 函数每次都会返回新的一个 symbol var sym = Symbol.for("mario"); sym.toString(); // "Symbol(mario)",mario 既是该 symbol 在 symbol 注册表中的键名,又是该 symbol 自身的描述字符串
- 引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。
当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
注意!!!
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的
浅拷贝
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
- 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
- 如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
浅拷贝实现方法
-
Object.assign()
var obj = { a: {a: "kobe", b: 39} }; var copyObj = Object.assign({}, obj); copyObj.a.a = "wade"; console.log(obj.a.a); // wade
Object.assign()
方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象Object.assign()
进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身
-
[...obj]
let obj1 = { name: 'yang', res: { value: 123 } } let {...obj2} = obj1 obj2.res.value = 456 console.log(obj2) // {name: "yang", res: {value: 456}} console.log(obj1) // {name: "yang", res: {value: 456}} obj2.name = 'haha' console.log(obj2) // {name: "haha", res: {value: 456}} console.log(obj1) // {name: "yang", res: {value: 456}}
[...obj]
是es6新增的展开运算符,作用和Object.assign()
一样
-
Array.prototype.concat()
let arr = [1, 3, { username: 'kobe' }]; let arr2=arr.concat(); arr2[2].username = 'wade'; console.log(arr); // [ 1, 3, { username: 'wade' } ]
-
Array.prototype.slice()
let arr = [1, 3, { username: ' kobe' }]; let arr3 = arr.slice(); arr3[2].username = 'wade' console.log(arr); // [ 1, 3, { username: 'wade' } ]
注意!!!
- Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组
- 数组拷贝原则
- 如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里,则两个对象引用都引用了同一个对象,如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变
- 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里,在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组
- 总结:拷贝的值是引用就会有影响,不是引用就没有影响
深拷贝
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
深拷贝的实现方法
JSON.parse(JSON.stringify())
- 用
JSON.stringify
将对象转成JSON字符串,再用JSON.parse()
把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝 - 可以实现数组或对象深拷贝,但不能处理函数,因为
JSON.stringify()
方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数
let arr = [1, 3, { username: ' kobe' }]; let arr4 = JSON.parse(JSON.stringify(arr)); arr4[2].username = 'duncan'; console.log(arr, arr4); // [ 1, 3, { username: ' kobe' } ] [ 1, 3, { username: 'duncan' } ]
- 用
- 函数库
lodash
- 该函数库也有提供
_.cloneDeep
用来做Deep Copy
var _ = require('lodash'); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f); // false
- 该函数库也有提供
- 手撕递归实现
- 递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
- 直接写有难度我们进行分步写
- 递归遍历对象属性
function deepCopy (obj){ if(obj === null || typeof obj !== Object) return obj; let copyObj = Array.isArray(obj) ? [] : {}; Object.keys(obj).forEach(x => { copyObj[key] = deepCopy(obj[key]); }) return copyObj; }
- 解决循环引用
先解决解决循环遍历问题, 解决办法是将对象,对象属性存储在数组中查看下次遍历时有无已经遍历过的对象,有则直接返回, 否则继续遍历
function deepCopy (obj, cache = []){ if(obj === null || typeof obj !== Object) return obj; const item = cache.filter(item => item.original === obj)[0]; if (item) return item.copy; let copyObj = Array.isArray(obj) ? [] : {}; cache.push({ original: obj, copyObj }) Object.keys(obj).forEach(key => { copyObj[key] = deepCopy(obj[key], cache); }) return copyObj; }
- 解决特殊值
对于最终的几个对象的处理,可以判断类型, 重新new一个返回就可以了
function deepCopy (obj, cache = []){ if(obj === null || typeof obj !== Object) return obj; if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj); if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj); if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj); const item = cache.filter(item => item.original === obj)[0]; if (item) return item.copy; let copyObj = Array.isArray(obj) ? [] : {}; cache.push({ original: obj, copyObj }) Object.keys(obj).forEach(key => { copyObj[key] = deepCopy(obj[key], cache); }) return copyObj; }
- 解决函数引用相同
(1) 还存在一个问题, 就是函数引用了同一个内存地址, 对于这个问题大部分都是直接返回或者返回为对象,解决这个问题需要用eval函数,函数这里分两种: 普通函数和箭头函数, 区分这两者只需要看有无prototype, 有prototype属性就属于普通函数, 没有就是箭头函数。
(2) 普通函数属于函数声明, 不能直接使用eval, 需要用小括号包起来形成函数表达式, 而箭头函数本身就是函数表达式。
function copyFunction(func) { let fnStr = func.toString(); return func.prototype ? eval(`(${fnStr})`) : eval(fnStr); } function deepCopy (obj, cache = []) { if (typeof obj === 'function') { return copyFunction(obj); } if(obj === null || typeof obj !== Object) return obj; if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj); if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj); if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj); const item = cache.filter(item => item.original === obj)[0]; if (item) return item.copy; let copyObj = Array.isArray(obj) ? [] : {}; cache.push({ original: obj, copyObj }) Object.keys(obj).forEach(key => { copyObj[key] = deepCopy(obj[key], cache); }) return copyObj; } deepCopy($obj).func === $obj.func // false
- 综合写法
- 因为需要考虑参数对象和参数对象的每个数据项的数据类型可能包括函数、正则、日期、ES6新对象且必须考虑循环引用问题,所以需要引入ES6新对象Map并且详细的判断数据类型,核心步骤有:
- 首先判断对象参数是否为“null”,是则返回“null”
- 判断对象参数数据类型是否为“object”,不是则返回该参数
- 获取到对象参数的构造函数名,判断是否为函数、正则、日期、ES6新对象其中之一,如果是则直接返回通过该参数对象对应的构造函数生成的新实例对象
- 当以上条件判断之后函数依然没有结束时继续进行以下操作
- 在Map对象中获取当前参数对象,如果能获取到,则说明这里为循环引用并返回Map对象中该参数对象的值
- 如果在Map对象中没有获取到对应的值,则保存该参数对象到Map中,作为标记
- 根据该参数的数据类型是否为数组创建新对象
- 遍历该对象参数,将每一项递归调用该函数本身的返回值赋给新对象
const _completeDeepClone = (target, map = new Map()) => { if(target === null) return target if(typeof target !== 'object') return target const constructor = target.constructor if(/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return new constructor(target) if(map.get(target)) return map.get(target) map.set(target, true) const cloneTarget = Array.isArray(target) ? [] : {} for(prop in target) { if(target.hasOwnProperty(prop)) { cloneTarget[prop] = _completeDeepClone(target[prop], map) } } return cloneTarget }