前言
因为工作中的深拷贝和浅拷贝用的次数还是算比较频繁的,所以打算深入学习一下深拷贝和浅拷贝 正好可以复习一下JS的基础知识,如果要写一个好的深拷贝函数要涉及到的细节还是蛮多的
准备工作(补充一下需要的知识点先)
原始数据类型和引用类型
原始数据类型:null、boolean、undefined、number、string、symbol
引用类型:对象、数组、函数
栈内存和堆内存
栈内存:栈在数据结构中是先进后出的数据结构,JS中它是一块用于存储原始数据类型和引用类型指针地址的内存块。
堆内存:堆内存和栈内存不同,虽然都是内存空间,但是堆内存中的变量是没有顺序的,所以我们需要通过对应的指针来取得堆内存中的值,JS中堆内存主要负责存储引用类型的变量。
apply、call、bind改变this指向
this:指的是当前代码运行时所属的上下文环境,所以this的指向默认是全局的window,当我们想要改变this的指向时,可在调用函数时通过apply、call、bind去临时改变this的指向(apply和call只会再调用函数时生效一次,bind会永久改变回调函数的this指向,不改变原函数的this指向)。
apply:传入的参数是数组形式,改变this指向后会立即调用函数
call:传入的参数是一个一个的传,改变this指向后会立即调用函数
bind:传入的参数是一个一个的传,此刻不会立即调用函数,它会返回一个回调函数,此时回调函数的this会永久改变,但是原函数的this还是指向全局window
为什么会发生浅拷贝
因为当我们用一个变量去接变量类型为引用类型的的变量时,比如说对象,此时我们copy得到的是对象的指针地址,而不是实际的值,此时指针指向的值是堆内存中的值,当我们改变其中一个对象的值时,改变的是堆内存中的值,并没有改变栈内存中的指针地址,所以另一个对象的指针还是指向这个堆内存,所以获取到的值是最新的值。
举个很形象的例子,指针地址就是微信的登录账号,账号里面的聊天功能、通讯录就是指针对应的值,你拿到你女朋友的登录账号,就是copy了这个对象,此刻女朋友的登录账号就是指针地址,然后你登录上去看到的(不考虑本地存储和云存储聊天记录的情况)和你女朋友看到的是一样的,你删了其中一个好友,当你女朋友用登录账号重新登录时,她那也没有了你删除的这个好友了。
实现浅拷贝
function isObject(target){
return typeof target === 'object'
}
function LightCopy(target){
// 判断是否是引用类型
if (!isObject(target)){
// 异常抛出错误,不一定要用alert
alert('不是引用类型')
return
}
// typeof会将null、object、array都定义为true,所以需要先排除一下 null
if (target === null){
return null
}
let cloneTarget = Object.prototype.toString.call(target) === "[object Object]"? {} : []
for (const key in target){
cloneTarget[key] = target[key]
}
return cloneTarget
}
为什么通过Object.prototype.toString.call(target)来判断类型
通常我们可以也用array.isArray()方法来判断,这是ES5新增的一个用来判断是否是数组的方法,局限性是只能判断数组,但是array.isArray()的核心还是通过Object.prototype.toString.call(target)==='[object Array]'来判断的。
JS重写了Number、Function、Array的toString方法,所以我们用Number.toString()拿到的是一个字符串
但是实际上每个类型的继承原型上的toString()方法返回的都是"[object xxxx]",xxxx是对应的类型,所以我们可以通过Object.prototype.toString.call(target)来判断要拷贝的类型,call就是用来改变this指向的,使得调用此刻调用toString()方法是继承原型上toString()方法
手写array.Array()方法
array.isArray = function(target){
return Object.prototype.toString.call(target) === '[object Array]';
}
为什么需要深拷贝
比如说表格数据,当我们表格的数据可以通过表单来回显并且编辑,表单回显时将行数据给表单,此刻表单拿到的是一个对象,直接赋值的话就是浅拷贝,但是我们又不想没有点击表单的保存的时候将表格的数据更改了,所以此刻我们就需要使用深拷贝,当然使用深拷贝的场景肯定很多,大家可以按需使用。
实现深拷贝
JSON.parse(JSON.stringify(data))
因为懒之前我用的都是这个,反正是用的六的飞起,不过这个有缺陷。
- 对象里面有new Date(),深拷贝后会变成字符串形式
- 对象里面有function、undefined是序列化会将其丢失
- 对象里面有RegExp、Error对象,序列化将其会变成空对象
- 对象里面里有NaN、Infinity和-Infinity,序列化会将其变成null
- 对象里面有构造函数生成的对象,深拷贝后会丢失其构造函数原型上的constructor
lodash中的cloneDeep
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。里面有封装好的深拷贝函数,可以直接引用
手写一个简单的cloneDeep
function deepClone(target) {
// 判断是否是引用类型,基本类型直接返回
if (!isObject(target)){
return target
}
// typeof会将null、object、array都定义为true,所以需要先排除一下 null
if (target === null){
return target
}
let cloneTarget = Object.prototype.toString.call(target)==="[object Object]"?{}:[]
for (const key in target){
cloneTarget[key] = deepClone(target[key])
}
return cloneTarget
}
如果还有其他的特殊的数据类型可以继续在深拷贝函数中加上其他类型的判断并处理。
小结
再往下就是性能优化、用for...in、while之类的去遍历目标对象,看看哪个性能好之类的,还有一些复杂的场景,后期再进行补充。