一般而言,我们对于拷贝
的理解就是拷贝之后会获得两个初始内容完全一样的个体。这两个个体是完全独立的。
为什么会存在深浅拷贝的差别?
JS当中数据类型分为
- 基本类型(undefined、null、string、number、boolean、symbol(ES6))
- 引用类型(Object、Array、RegExp、Date、Function)
基本类型值指的是那些保存在栈内存中的简单数据段,即这种值是完全保存在内存中的一个位置。包含Number,String,Boolean,Null,Undefined ,Symbol。
基本数据类型是按照数值来引用,且也是按值来比较,所以拷贝一个基本数据类型的话,只需要拷贝它的数值。
var a = 2;
var b = a;
a = 3;
console.log(a,b);//3,2
可以看到,基本数据类型通过赋值就可以被拷贝,拷贝之后,a和b就是两个独立的个体,他们的变化不会互相影响。所以对于基本数据类型来说,没有什么深拷贝和浅拷贝的差别。
引用类型值指的是那些保存在堆内存中的对象,所以引用类型的值保存的是一个指针,这个指针指向存储在堆中的一个对象。引用类型统称为Object类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等。
但是对于引用类型,因为引用类型的具体内容数据是存在在堆当中的,堆中的数据不能直接按照数值引用,对象中实际保存的是引用类型数据的地址信息,而不是它具体的内容和数值。
如果我们只是,简单的拷贝下引用类型,那只是拷贝了它的引用地址,所以会出现这种情况。
var a = {
test:1
};
var b = a;
b.test = 2;
console.log(a,b);
// {test:2},{test:2}
我们会发现,b修改了test的数值之后,连a的test数值也被修改了。
因为a对象中保存的其实只是{test:1}
的数据的一个引用地址,所以b=a
只是把a保存的引用地址赋值给b了。所以b其实拿到的是一个引用地址和a指向的是同一组数据。
所以当b拿着从a拷贝过来的地址,找到了test属性对应的数值,并且修改了它。等a在引用的时候,a保存的也是同一个地址,但是这个地址对应的数据,已经被b修改过了,所以a输出的数值就变成
{test:2}
那从严格的角度来说,赋值对于引用类型的拷贝只是一种浅层次的拷贝,仅拷贝了数据引用地址,但是实际的数据内容还是共享的,即我们常见的“浅拷贝”
因此,对于引用类型,就衍生出了一个问题:
能否让引用类型的拷贝版本的数据内容跟原来的对象完全独立开,互相不影响?
也就是我们常说的“深拷贝”
「JavaScript」带你彻底搞清楚深拷贝、浅拷贝和循环引用
JS常见浅拷贝方式
数组浅拷贝
-
for...in
let refArr = [{test:1},{test:2}];
let refCopy = [];
for(let i = 0; i
-
Array.prototype.slice()
let arr = [1,2,3,4];
let copy = arr.slice();
arr[0] = 0;
console.log(arr,copy);
//[0,2,3,4],[1,2,3,4]
let refArr = [{test:1},{test:2}];
let refCopy = refArr.slice();
refArr[0].test = 0;
console.log(refArr,refCopy);
//[{test:0},{test:2}],[{test:0},{test:2}]
slice不改变原数组的内容,如果内容是基本数据类型,可以正常拷贝
如果数组内容是引用类型,则进行浅拷贝
MDN-Array/slice
-
Array.prototype.concat()
和slice类似
let arr = [1,2,3,4];
let copy = arr.concat();
arr[0] = 0;
console.log(arr,copy);
//[0,2,3,4],[1,2,3,4]
let refArr = [{test:1},{test:2}];
let refCopy = refArr.concat();
refArr[0].test = 0;
console.log(refArr,refCopy);
//[{test:0},{test:2}],[{test:0},{test:2}]
MDN-/Array/concat
注意:如果数组的元素都是基本数据类型,这几种方法无所谓深拷贝浅拷贝,因为都是返回一个新的数组,所以得到的都是互不干扰的新对象。
当数组元素包含引用类型的时候,引用类型拷贝后只拷贝了引用地址,所以还是浅拷贝。
Object浅拷贝
-
Object.assign(target,source)
assign方法是把source对象上的key-val属性和数值添加到target对象上
Symbol类型也可以被拷贝
let obj1 = { a: 0 , b: { c: 0}};
let obj2 = Object.assign({}, obj1);
obj1.b.c = 1;
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 1}}
MDN-Object/assign
扩展运算符 ...
//数组
let dd = [{test:1},{test:2}];
let b = [...dd];
dd[0].test = 0;
console.log(b,a);
//[{test:0},{test:2}],[{test:0},{test:2}]
//对象
let obj1 = { a: 0 , b: { c: 0}};
let obj2 = {...obj1}
obj1.b.c = 1;
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 1}}
总结:
- 上述这些拷贝方式,共性都是会把引用类型的每个元素值赋给另外一个对象。
- 每个元素直接赋值,对于这种一层数据的引用类型数据,如
[1,2,3,4]
和{a:2,b:3}
这样的数据类型,可以实现深拷贝 - 但是如果是引用类型的元素也是引用类型,那直接赋值拷贝的就是引用地址,所以只能是浅拷贝。
深拷贝的实现方法
思路一:JSON.parse(JSON.stringify(obj))
let arr = [
{
level_one:{
level_two:0
}
},
{
level_one:{
level_two:1
}
},
];
let arrCopy = JSON.parse(JSON.stringify(arr));
arr[0].level_one.level_two = 1;
console.log(arrCopy);
//[{level_one:{level_two: 0}},level_one:{level_two: 1}}]
- JSON.stringify(obj)会调用obj上的
toJSON
方法,把对象转换成符合JSON格式的特殊的string- 如果obj里面包含循环引用的对象比如
a = { test:a}
,则会报错
- 如果obj里面包含循环引用的对象比如
-
局限性
- Object里面不可枚举的属性会被忽略,无法拷贝
-
Symbol
会被直接忽略 - 不能完美拷贝
undefined,function,Symbol,Date,NaN,Infinity
let objNoWork = {
a:undefined,
b:function(){
console.log('I am function');
},
c:NaN,
d:Symbol('d'),
e:new Date('2019-01-01')
}
objNoWork[Symbol('f')] = 'Symbol value';
let obj = JSON.parse(JSON.stringify(objNoWork));
console.log(objNoWork);
console.log(obj);
从拷贝后结果我们可以看出来,
undefined,function,Symbol
直接被忽略了,Date
也转换成字符串,NaN
直接变成null
了。
所以
JSON.parse(JSON.stringify(obj))
,使用的时候特别注意这些特殊的数据情况。
ecma262#sec-json.stringify
mdn-JSON/stringify
思路二:按照类型处理—递归拷贝
如果要实现一个完美的深拷贝,可以根据不同数据类型做递归处理。
所以实现的思路其实主要就是:类型判断+具体的拷贝实现+递归处理
这个基础思路看起来没啥大问题,不过如果遇到一种特殊情况的话,就会有问题。
- 局限性:循环引用
什么是循环引用
简单的说,就是在子对象当中又引用了父对象,这种情况在JS当中是完全允许的。
//循环引用
let obj ={
a:1,
b: {
ref:obj
}
}
循环引用的问题:子对象保存的是对象的引用,实际上的数值还是指向同一个对象,所以在递归过程当中,一遇到循环引用,就会进入新的递归过程,无限套娃,这个子对象一直没有解除递归的条件,最后会死循环,导致拷贝爆栈。
所以在实现深拷贝的过程当中,我们需要对循环引用的情况进行另外处理。
结合循环引用情况,梳理下这个深拷贝的实现思路可以概括为:
- 判断数值的数据类型
- 根据特定数据类型进行具体的拷贝
- 可遍历类型(Object/Array/Map/Set等):遍历每个值递归处理
- 不可遍历类型: 根据类型进行赋值
- 根据类型,通过
constructor
构造初始值然后拷贝内容
- 引用类型,记录拷贝情况,出现循环引用且已经拷贝过的对象,不另外拷贝
深拷贝具体实现
具体实现主要参考的是lodash当中cloneDeep的实现。
类型判断
- typeof
- Object.prototype.toString.call(value)
【js类型判断】包装类以及isArray,instanceof,typeof用法及原理
数组
/**
* Initializes an array clone.
*
* @private
* @param {Array} array The array to clone.
* @returns {Array} Returns the initialized clone.
*/
function initCloneArray(array) {
const { length } = array
const result = new array.constructor(length)
// Add properties assigned by `RegExp#exec`.
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}
Map/Set
//复制Map
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
// set
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
深拷贝Symbol
Symbol
是ES6当中出现的全新的基本数据类型,它会返回一个唯一的symbol值,而且不能使用new Symbo()
来创建新的对象。
let a = Symbol('b');
let b = Symbol('b');
console.log(a === b);//false
let c = a;
console.log( c === a);//true
MDN-Symbol
Symbol代表的是唯一的值,可是它又不能new,那它的拷贝是几个意思?
我一开始纠结了一下Symbol深拷贝是什么意思呢?
直接赋值那不就是原来的那个value吗,他们是不就是同一个value吗?
但是Symbol('b')
和Symbol('b')
不是两个一样的数值呀?
后来想明白了,所谓拷贝,拷贝后的属性和数值跟原来的要一样,而且Symbol本身是基本数据类型,不是引用类型。
/** Used to convert symbols to primitives and strings. */
const symbolValueOf = Symbol.prototype.valueOf
/**
* Creates a clone of the `symbol` object.
*
* @private
* @param {Object} symbol The symbol object to clone.
* @returns {Object} Returns the cloned symbol object.
*/
function cloneSymbol(symbol) {
return Object(symbolValueOf.call(symbol))
}
export default cloneSymbol
深拷贝function
- 思路:
- prototype来区分下箭头函数和普通函数,箭头函数是没有prototype的。
- 使用eval和函数字符串来重新生成一个箭头函数
- 使用正则来处理普通函数:使用正则取出函数体和函数参数,然后使用new Function ([arg1[, arg2[, ...argN]],] functionBody)
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
console.log('普通函数');
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
console.log('匹配到函数体:', body[0]);
if (param) {
const paramArr = param[0].split(',');
console.log('匹配到参数:', paramArr);
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
如何写出一个惊艳面试官的深拷贝?
循环引用深拷贝
循环引用处理思路:记录引用记录是否已经被拷贝过了,如果拷贝过了,那么就直接把引用地址赋值就可以,没有拷贝过,再进行深拷贝。
-
lodash处理循环引用——用栈来记录引用记录
通过key-value的形式来保存拷贝值和它是否被拷贝过的记录
如果被拷贝过了,就直接返回它当时被拷贝后的记录,结束当前递归调用。
// 循环引用
//Check for circular references and return its corresponding clone.
// result是拷贝过后的值,value是传入的待拷贝的值
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
stack.set(value, result)
如果我们去看Stack
源代码,我们会发现在源代码里面设置了size限制为200
-
使用weekMap来记录引用记录
设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。
WeakMap:对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。
MDN-WeakMap
WeakMap的特性就是,保存在其中的对象不会影响垃圾回收,如果WeakMap保存的节点,在其他地方都没有被引用了,那么即使它还在WeakMap中也会被垃圾回收回收掉了。
那么这个特性为什么可以应用到引用记录这来呢?
在深拷贝的过程当中,里面所有的引用对象,都是被引用的,为了解决循环引用的问题,在深拷贝的过程中,我们希望有个数据结构能够记录每个引用对象有没有被使用过,但是深拷贝结束之后这个数据能自动被垃圾回收,避免内存泄漏。
JS垃圾回收机制
深拷贝和浅拷贝
JavaScript对象深拷贝/浅拷贝遇到的坑和解决方法
【进阶4-3期】面试题之如何实现一个深拷贝
JS深浅拷贝探究
性能优化考量
我们可以看到lodash当中遍历使用的是while循环去实现的。
function arrayEach(array, iteratee) {
let index = -1
const length = array.length
while (++index < length) {
if (iteratee(array[index], index, array) === false) {
break
}
}
return array
}
因为对比for...in
和for
循环来说,while
循环的效率会更高一些。
参考这篇文章中的性能对比:如何写出一个惊艳面试官的深拷贝?
lodash深拷贝源码整体逻辑梳理
- lodash当中类型判断用的也是
Object#toString
和typeof结合判断数据类型
/** `Object#toString` result references. */
const argsTag = '[object Arguments]'
const arrayTag = '[object Array]'
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const mapTag = '[object Map]'
const numberTag = '[object Number]'
const objectTag = '[object Object]'
const regexpTag = '[object RegExp]'
const setTag = '[object Set]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const weakMapTag = '[object WeakMap]'
const arrayBufferTag = '[object ArrayBuffer]'
const dataViewTag = '[object DataView]'
const float32Tag = '[object Float32Array]'
const float64Tag = '[object Float64Array]'
const int8Tag = '[object Int8Array]'
const int16Tag = '[object Int16Array]'
const int32Tag = '[object Int32Array]'
const uint8Tag = '[object Uint8Array]'
const uint8ClampedTag = '[object Uint8ClampedArray]'
const uint16Tag = '[object Uint16Array]'
const uint32Tag = '[object Uint32Array]'
- 不同类型的数据,根据类型生成新的拷贝初始值
/**
* Initializes an object clone based on its `toStringTag`.
*
* **Note:** This function only supports cloning values with tags of
* `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`.
*
* @private
* @param {Object} object The object to clone.
* @param {string} tag The `toStringTag` of the object to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the initialized clone.
*/
function initCloneByTag(object, tag, isDeep) {
const Ctor = object.constructor
switch (tag) {
case arrayBufferTag:
return cloneArrayBuffer(object)
case boolTag:
case dateTag:
return new Ctor(+object)
case dataViewTag:
return cloneDataView(object, isDeep)
case float32Tag: case float64Tag:
case int8Tag: case int16Tag: case int32Tag:
case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
return cloneTypedArray(object, isDeep)
case mapTag:
return new Ctor
case numberTag:
case stringTag:
return new Ctor(object)
case regexpTag:
return cloneRegExp(object)
case setTag:
return new Ctor
case symbolTag:
return cloneSymbol(object)
}
}
- 特殊的数据类型不支持拷贝
/** Used to identify `toStringTag` values supported by `clone`. */
const cloneableTags = {}
cloneableTags[argsTag] = cloneableTags[arrayTag] =
cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =
cloneableTags[boolTag] = cloneableTags[dateTag] =
cloneableTags[float32Tag] = cloneableTags[float64Tag] =
cloneableTags[int8Tag] = cloneableTags[int16Tag] =
cloneableTags[int32Tag] = cloneableTags[mapTag] =
cloneableTags[numberTag] = cloneableTags[objectTag] =
cloneableTags[regexpTag] = cloneableTags[setTag] =
cloneableTags[stringTag] = cloneableTags[symbolTag] =
cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true
cloneableTags[errorTag] = cloneableTags[weakMapTag] = false
- 拷贝主逻辑
/** Used to compose bitmasks for cloning. */
const CLONE_DEEP_FLAG = 1
const CLONE_FLAT_FLAG = 2
const CLONE_SYMBOLS_FLAG = 4
/**
* The base implementation of `clone` and `cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone.
* @param {number} bitmask The bitmask flags.
* 1 - Deep clone
* 2 - Flatten inherited properties
* 4 - Clone symbols
* @param {Function} [customizer] The function to customize cloning.
* @param {string} [key] The key of `value`.
* @param {Object} [object] The parent object of `value`.
* @param {Object} [stack] Tracks traversed objects and their clone counterparts.
* @returns {*} Returns the cloned value.
*/
function baseClone(value, bitmask, customizer, key, object, stack) {
let result
const isDeep = bitmask & CLONE_DEEP_FLAG
const isFlat = bitmask & CLONE_FLAT_FLAG
const isFull = bitmask & CLONE_SYMBOLS_FLAG
//自定义clone函数
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value)
}
if (result !== undefined) {
return result
}
// 非对象类型,直接返回数值
if (!isObject(value)) {
return value
}
//处理数组
const isArr = Array.isArray(value)
const tag = getTag(value)
if (isArr) {
result = initCloneArray(value)
if (!isDeep) {
return copyArray(value, result)
}
} else {
const isFunc = typeof value === 'function'
if (isBuffer(value)) {
return cloneBuffer(value, isDeep)
}
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
result = (isFlat || isFunc) ? {} : initCloneObject(value)
if (!isDeep) {
return isFlat
? copySymbolsIn(value, copyObject(value, keysIn(value), result))
: copySymbols(value, Object.assign(result, value))
}
} else {
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
result = initCloneByTag(value, tag, isDeep)
}
}
// 循环引用Check for circular references and return its corresponding clone.
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
stack.set(value, result)
//复制Map
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
// set
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
//TypedArray
if (isTypedArray(value)) {
return result
}
const keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys)
const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
if (props) {
key = subValue
subValue = value[key]
}
// Recursively populate clone (susceptible to call stack limits).
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
export default baseClone
【进阶4-4期】Lodash是如何实现深拷贝的
深入剖析 JavaScript 的深复制