教你手撸深拷贝与浅拷贝

深拷贝和浅拷贝

初识

  js中数据分为基本数据类型如nullstringundefinednumberbooleansymbol,以及引用数据类型如对象数组

  而在js中存储方式也是分栈和堆,基础数据类型是栈存储,引用数据类型一般是堆存储。

  深拷贝和浅拷贝是对于复杂类型而言的。

  下面先通过一个简单的例子进行说明其区别。

// 直接赋值
const initData = [ 1 , { obj: '123' } ];
const finalData = initData;
finalData[0] = 2;
finalData[1].obj = '456';
console.log(initData); // [ 2, { obj: '456' } ]
console.log(finalData); // [ 2, { obj: '456' } ]

// 基本数据类型
let str1 = '123';
str2 = str1;
str2 = '456';
console.log(str1); // '123'
console.log(str2); // '456'

// // 引用数据类型
let arr1 = [1, 2, 3];
arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4]
console.log(arr2); // [1, 2, 3, 4]

  我们在例子中可以看到,在直接赋值的情况下,修改finalData也会改变原来的initData,而基本数据类型的拷贝,修改str2不会影响原来的str1,而引用数据类型的拷贝,修改arr2会影响原来的arr1。

  那么这样就诞生了一个问题,在平时咱们怎么能进行限次数的拷贝,从而能保证修改拷贝后的数据一直不会影响原来的数据呢?这就是深拷贝的基本概念了。针对以上问题,咱们可以把想法思维化,先写个小demo,先把各个需要的功能简单罗列一下。

/**
* @name 赋值
*/
const dataOne = {
title: 'study',
number: ['html', 'css', 'javascript'],
};
const dataTwo = dataOne;
dataTwo.title = 'play';
dataTwo.number = ['null'];
console.log(dataOne);
// dataOne: { title: 'play', number: ['null'] }
console.log(dataTwo);
// dataTwo: { title: 'play', number: ['null'] }


/**
* @name 浅拷贝
*/

const dataThree = {
title: 'study',
number: ['html', 'css', 'javascript'],
};
const dataFour = shallowClone(dataThree); // shallowClone 待实现
dataFour.title = 'play';
dataFour.number = ['null'];
console.log(dataThree);
// dataThree: { title: 'study', number: ['null'] }
console.log(dataFour);
// dataFour: { title: 'play', number: ['null'] }


/**
* @name 深拷贝
*/

const dataFive = {
title: 'study',
number: ['html', 'css', 'javascript'],
};

const dataSix = deepClone(dataFive); // deepClone 待实现
dataSix.title = 'play';
dataSix.number = ['null'];
console.log(dataFive);
// dataFive: { title: 'study', number: ['html', 'css', 'javascript'] }
console.log(dataSix);
// dataSix: { title: 'play', number: ['null'] }

经过上面的实例,就这样把拷贝分成了三种情况,大致意思就是这样:

  • 赋值:引用地址的拷贝。修改赋值后的数据,不管是基本数据类型还是引用数据类型,都会影响到原数据。

  • 浅拷贝:一层拷贝。在浅拷贝中,修改基本数据类型不会影响原有数据的基本数据类型,修改引用数据类型会影响原有的数据类型。

  • 深拷贝:无限层级拷贝。在深拷贝中,修改基本数据类型和引用数据类型都不会影响原有的数据类型。

  深拷贝与桥拷贝可能在日常开发中都碰不到,但是确是面试常问的问题,虽然碰不到,但是怎么手写一个浅拷贝或深拷贝,确是能用到很多知识,还是值得学习的。

代码实现浅拷贝

1. 循环遍历对象可枚举值

// 手写浅拷贝
const arr1 = [1, 2, ['html', 'css'], 4];

const shallowClone = (arr) => {
const dst = [];
for (let prop in arr) {
console.log(prop)
if (arr.hasOwnProperty(prop)) {
dst[prop] = arr[prop];
}
}
return dst;
}

const arr2 = shallowClone(arr1);
arr2[2].push('Javascript');
arr2[3] = 5;

console.log(arr1);
// [ 1,2,['html','css','Javascript'], 4 ]
console.log(arr2);
// [1,2,['html','css','Javascript'], 5 ]

手写思路:

  • for... in : 遍历Object对象arr1,将骑可枚举值列举出来。
  • hasOwnProperty () : 检查该枚举值是否属于该对象arr1, 如果是继承过来的就去掉,如果是自身的则进行拷贝。

2. Object.assign()


const obj = {
name : "张三",
age : "16",
skill : {
read : "html",
write : "css"

},
like : ['吃饭','睡觉','敲代码']
}

const newobj = Object.assign({},obj)

newobj.name = "李四"
newobj.skill.read = "js"
newobj.like = ["玩耍"]

console.log(obj)
// { name: '张三',
// age: '16',
// skill: { read: 'js', write: 'css' },
// like: [ '吃饭', '睡觉', '敲代码' ] }
console.log(newobj)
// { name: '李四',
// age: '16',
// skill: { read: 'js', write: 'css' },
// like: [ '玩耍' ] }

Object.assign() 方法是ES6新增方法,可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

通过代码可以看到的是,Object.assign() 对于第一层的数据来说,是深拷贝,对于第二层及以上的数据来说,是浅拷贝。

3. Array. prototype. concat ()

concat ()是数组的一个内置方法,用户合并两个或者多个数组。这个方法不会改变现有数组,而是返回一个新数组。


const arr = [1,2,{ name : "张三", age : "16"}]

const newarr = arr.concat()

newarr[0] = 3
newarr[2].name = "李四"

console.log(arr) // [ 1, 2, { name: '李四', age: '16' } ]
console.log(newarr) // [ 3, 2, { name: '李四', age: '16' } ]

4. Array. prototype. slice ()

slice()也是数组的一个内置方法,该方法会返回一个新的对象。slice()不会改变原数组。


const arr = [1,2,{ name : "张三", age : "16"}]


const newarr = arr.slice()

newarr[0] = 3
newarr[2].name = "李四"

console.log(arr) // [ 1, 2, { name: '李四', age: '16' } ]
console.log(newarr) // [ 3, 2, { name: '李四', age: '16' } ]

5.展开运算符… obj

展开运算符是ES6中新提出来的一种运算符。在拷贝数组、对象以及拼接数组等方面都可以使用。这边我们也可以尝试下使用const obj2 = {..obj1} 的形式进行浅拷贝。

// 对数组

const arr = [1,2,{ name : "张三", age : "16"}]
const newarr = [...arr]

console.log(arr) // [ 1, 2, { name: '张三', age: '16' } ]
console.log(newarr) // [ 1, 2, { name: '张三', age: '16' } ]

newarr[0] = 3
newarr[2].name = "李四"

console.log(arr) // [ 1, 2, { name: '李四', age: '16' } ]

console.log(newarr) // [ 3, 2, { name: '李四', age: '16' } ]

// 对于对象进行浅拷贝
const obj = {
name : "张三",
age : "16",
like : ['吃饭','睡觉','敲代码']
}
const newobj = {...obj}

newobj.like.push('打豆豆')
newobj.name = "李四"

console.log(obj)
// { name: '张三', age: '16', like: [ '吃饭', '睡觉', '敲代码', '打豆豆' ] }
console.log(newobj)
// { name: '李四', age: '16', like: [ '吃饭', '睡觉', '敲代码', '打豆豆' ] }

代码实现深拷贝

  经过对浅拷贝的简单了解,咱们就到了如何实现深拷贝的问题上,常见的循环遍历,只是遍历了一层数据,明显解决不了这个问题,不过咱们可以看出,深拷贝的问题不就在于怎么解决无限层级拷贝问题吗,这种数据类型似乎在哪见过,对没错,可以用递归解决!

  咱们先对浅拷贝代码进行改造,用了递归之后就实现了一个深拷贝。

const deepClone = (source) => {
const target = {};
for (const i in source) {
if (source.hasOwnProperty(i)
&& target[i] === 'object') {
target[i] = deepClone(source[i]); // 注意这里
} else {
target[i] = source[i];
}
}
return target;
};

但是这份代码还有一些细节需要修改,如:

  • 没有对参数进行校验,如果传入进来的不是对象或者数组,我们直接返回即可。

  • 通过 typeof 判断是否对象的逻辑不够严谨。 typeof null

对此可进行改进,实现一份完整的深拷贝代码。

1. 手写深拷贝

// 定义检测数据类型的功能函数
const checkedType = (target) => {
// console.log(Object.prototype.toString.call(target))
console.log(Object.prototype.toString.call(target));
return Object.prototype.toString.call(target).slice(8, -1);
}

// 实现深度克隆对象或者数组
const deepClone = (target) => {
// 判断拷贝的数据类型
// 初始化变量 result 成为最终数据
let result, targetType = checkedType(target);
if (targetType === 'Object') {
result = {};
} else if (targetType === 'Array') {
result = [];
} else {
return target;
}

// 遍历目标数据
for (let i in target) {
// 获取遍历数据结构的每一项值
let value = target[i];
// 判断目标结构里的每一项值是否存在对象或者数组
if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
// 如果对象或者数组中还嵌套了对象或者数组,那么继续遍历
result[i] = deepClone(value);
} else {
result[i] = value;
}
}

// 返回最终值
return result;
}

const obj1 = [
1,
'Hello!',
{ name: '张' },
[
{
name: '李',
}
],
]
const obj2 = deepClone(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = '小张';
obj2[3][0].name = '小李';

console.log(obj1);
// [ 1, 'Hello!', { name: '张' }, [ { name: '李' } ] ]
console.log(obj2);
// [ 2, 'Hi!', { name: '小张' }, [ { name: '小李' } ] ]

经过深拷贝后,我们可以看到,再修改引用数据类型里的值时,原对象毫不收到影响,说明这份代码还是挺成功的。下面是代码的实现思路:

  • Object.prototype.toString.call():稳健地判断 JavaScript 数据类型方式,可以符合预期的判断基本数据类型 StringUndefined 等,也可以判断 ArrayObject 这些引用数据类型。

  • 然后,我们通过方法 targetType() 中的 Object.prototype.toString.call(),判断传入的数据类型属于那种,从而改变 result 的值为 {}、[] 或者直接返回传入的值(return target)

  • 最后,我们再通过 for...in 判断 target 的所有元素,如果属于 {} 或者 [],那么就递归再进行 clone() 操作;

  • 如果是基本数据类型,则直接传递到数组中……从而在最后返回一个深拷贝的数据。

但是如果经过严格的测试,这份代码也是行不通的,如果拷贝对象是个循环空对象,那么咱们将把自己绕进去,写了个死循环。而对广度和深度进行测试后,数据大的话,也是行不通的,十分容易爆栈。

这就是手写深拷贝待解决的两个大问题:

  • 死循环
  • 爆栈

解决方法,博主也是了解的比较少,能简单理解到:

  • 死循环的解决是用哈希表进行循环检测,我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可。
  • 爆栈的问题可用栈解决

这里就不过多解释了,有兴趣的可去大佬文章深挖。

面试题之如何实现一个深拷贝
深拷贝的终极探索

2. JSON. parse (JSON. stringify())

  • JSON. stringify():将对象转成JSON 字符串。
  • JSON. parse():将字符串解析成对象。

const arr1 = [
1,
{
username: 'zhangsan',
},
];

let arr2 = JSON.parse(JSON.stringify(arr1));
arr2[0] = 2;
arr2[1].username = 'li';
console.log(arr1);
// [ 1, { username: 'zhangsan' } ]
console.log(arr2);
// [ 2, { username: 'li' } ]

  通过JSON. parse (JSON. stringify())JavaScript 对象转序列化(转换成JSON字符串),再将其还原成JavaScript对象,一去一 来我们就产生了一个新的对象,而且对象会开辟新的栈,从而实现深拷贝。

  此方法虽然简单,但是却有很多局限性。

  • 1、不能存放函数或者 Undefined,否则会丢失函数或者 Undefined
  • 2、不要存放时间对象,否则会变成字符串形式;
  • 3、不能存放 RegExpError 对象,否则会变成空对象;
  • 4、不能存放 NaNInfinity-Infinity,否则会变成 null

3.函数库Lodash

Lodash作为一个JavaScript 函数库/工具库,它里面有非常好用的封装好的功能,大家可以去试试,这里我们查看下它的cloneDeep() 方法,该方法会递归拷贝value

首先需要npm先下载lodash

var lodash = require('lodash');

const obj1 = [
1,
'Hello!',
{ name: 'html' },
[
{
name: 'css',
}
],
]
const obj2 = lodash.cloneDeep(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'lodash';
obj2[3][0].name = 'js';

console.log(obj1);
// [ 1, 'Hello!', { name: 'html' }, [ { name: 'css' } ] ]
console.log(obj2);
// [ 2, 'Hi!', { name: 'lodash' }, [ { name: 'js' } ] ]

4.框架jQuery中extend() 方法

const obj1 = [
1,
'Hello!',
{ name: 'html' },
[
{
name: 'css',
}
],
]
const obj2 = {};
/**
* @name jQuery深拷贝
* @description $.extend(deep, target, object1, object2...)
* @param {Boolean} deep 可选 true 或者 false,默认是 false,所以一般如果需要填写,最好是 true。
* @param {Object} target 需要存放的位置
* @param {Object} object 可以有 n 个原数据
*/
$.extend(true, obj2, obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'js';
obj2[3][0].name = 'jq';

console.log(obj1);
console.log(obj2);

总结

  之前浏览博客的时候,总是看到别人关于深拷贝与浅拷贝的文章,也知道面试这是常问的知识点,但是一直没怎么看。而在前不久,在小组轮到我讲课了,思来想去讲写什么,最后决定讲这个,经过一星期的学习,真的学习到了不少,碰到任何不懂的问题,自己立马去搜了搜,也连联想了许多相关的知识点,也算是把这一难点学会了,虽然平时可能用不用到,但用不用的到并不是关键,要的是学会知识😁,一个深拷贝却蕴含了那么多的知识点,谁能想的到,平时可能也不会怎么注意吧。

Author: ahuiyoのblog
Link: http://ahuiyo.cn/clone/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
支付宝打赏
微信打赏