js的克隆是一个老生常谈的内容了,今天没啥好写的,就写这个了
要搞清楚js的克隆,就需要先搞清楚js中的数据类型,js中数据类型分为两大类
类型 | 说明 |
---|---|
原始类型 | - |
string | 字符串类型,用于表示文本数据。 |
number | 数字类型,包括整数和浮点数,用于表示数值数据。 |
boolean | 布尔类型,true 或 false,用于表示逻辑值。 |
null | 空值,表示无或不存在任何值。 |
undefined | 未定义值,表示变量未被赋值或初始化。 |
bigint | 大整数类型,可以表示任意大的整数(使用n作为后缀,如 100n)。 |
symbol | 符号类型,用于创建唯一的标识符(使用Symbol()创建)。 |
引用类型 | - |
object | 对象类型,由一组键值对组成,用于表示复杂的数据结构(使用大括号 {} 创建)。 |
原始类型的拷贝确实相对简单,因为这些类型通常存储在栈上,这意味着它们在内存中占据的空间是固定的,并且可以直接通过指针进行复制。
tips:如果你还搞不清楚什么是引用类型,什么是原始类型,可以看我的这篇文章
javascript数据类型与引用类型的区别以及原始值详解
然而,对于引用类型来说,拷贝则要复杂得多。引用类型的对象(例如数组或对象)存储在堆上,这意味着它们在内存中占据的空间是不固定的,并且由垃圾回收器管理。因此,对于引用类型的拷贝,我们不能简单地复制指针,而是需要复制整个对象。
为了实现引用类型的拷贝,我们需要使用深拷贝(deep copy)或浅拷贝(shallow copy)的技术。深拷贝会复制对象的所有嵌套对象和数组,而浅拷贝则只会复制对象的顶层结构。
需要注意的是,深拷贝可能会导致性能问题,因为它需要复制大量的数据。因此,对于大型的引用类型对象,我们可能需要使用一些优化技巧来避免深拷贝,例如使用对象代理(object proxy)或冻结(freeze)对象来阻止修改。
对于简单对象的拷贝_数组克隆
数组对象作为引用类型,栈内存储的是这个数组对象的堆内引用地址,因为对象类型通常比较庞大,这是数据开销和内存开销优化的手段,也就是说,对于数组只通过简单=赋值符号赋值的话,是行不通的
示例如下
var x = [1,2,3];var y = x;console.log(y); //[1,2,3]y.push(4);console.log(y); //[1,2,3,4]console.log(x); //[1,2,3,4]
对于这种情况,我们可以通过一个循环,简单粗暴的解决这个问题
var x = [1, 2, 3];var y = [];x.forEach(v => y.push(v))console.log(y); //[1,2,3]y.push(4);console.log(y); //[1,2,3,4]console.log(x); //[1,2,3]
对于简单对象的拷贝_简单对象的克隆
var x = {a:1,b:2};var y = {};for(var i in x){y[i] = x[i];}console.log(y); //Object {a: 1, b: 2}y.c = 3;console.log(y); //Object {a: 1, b: 2, c: 3}console.log(x); //Object {a: 1, b: 2}
当然,我知道还有一种更加简单高效的方法,那就是使用json的序列化.但是这里先不讲它,后面再讲
对于简单对象的拷贝_函数的克隆
由于函数对象克隆之后的对象会单独复制一次并存储实际数据,因此并不会影响克隆之前的对象。所以采用简单的复制“=”即可完成克隆。
var x = function(){console.log(1);};var y = x;y = function(){console.log(2);};x(); //1y(); //2
JavaScript浅克隆和深度克隆
浅克隆(Shallow Clone)和深度克隆(Deep Clone)是 JavaScript 中用来复制对象或数组的两种主要方法。它们的主要区别在于复制过程中对嵌套对象或数组的处理方式。
- 浅克隆:
浅克隆只复制对象或数组的顶层元素,对于嵌套的对象或数组,它只复制了引用,而没有复制内部的元素。这意味着,如果你改变了复制的对象或数组,原对象或数组也会被改变,因为它们指向的是同一份数据。 - 深度克隆:
深度克隆会复制对象或数组的所有层级的元素。这意味着,对于嵌套的对象或数组,它会创建完全独立的副本。这样,改变复制的对象或数组不会影响到原对象或数组。在 JavaScript 中,可以使用 JSON.parse(JSON.stringify(obj)) 方法来进行深度克隆。但是这个方法只能用于对象或数组不包含函数、RegExp、Date、Infinity、NaN、undefined、Infinity、NaN的情况
浅克隆示例如下
// 浅克隆函数function shallowClone(obj) {let clone = Array.isArray(obj) ? [] : {}for (let key in obj) {if (obj.hasOwnProperty(key)) {clone[key] = obj[key]}}return clone;}// 被克隆对象const oldObj = {a: 1,b: ['1', '2', '3'],c: { d: { e: 2 } }};const newObj = shallowClone(oldObj);console.log(newObj.c.d, oldObj.c.d); // { e: 2 } { e: 2 }console.log(oldObj.c.h === newObj.c.h); // truenewObj.c.d = 100 //改变newObjconsole.log(oldObj.c.d) //oldObj随之改变
我们可以很明显地看到,虽然oldObj.c.d被克隆了,但是它还与oldObj.c.d相等,这表明他们依然指向同一段堆内存,我们上面讨论过了引用类型的特点,这就造成了如果对newObj.c.d进行修改,也会影响oldObj.c.d。这往往不是我们想要的
所以我们需要构建一个深度克隆函数
深度克隆
上面我们讲过,使用json的序列化和反序列化可以对简单对象进行深度拷贝,而当对象中出现诸如function 、RegExp、Date、Infinity、NaN、undefined 或 Symbol 等等之类的类型时,json的序列化便处理不了,而且有循环引用的时候更是会直接报错
但反过来想,如果我们针对这些特殊情况做处理,那不就能实现深度克隆了吗
所以我们先要获取不同对象的类型做出判断,这样我们就可以对特殊对象进行类型判断了,从而采用针对性的克隆策略.
const isType = (obj, type) => {if (typeof obj !== 'object') return false// 判断数据类型的经典方法:const typeString = Object.prototype.toString.call(obj)let flagswitch (type) {case 'Array':flag = typeString === '[object Array]'breakcase 'Date':flag = typeString === '[object Date]'breakcase 'RegExp':flag = typeString === '[object RegExp]'breakdefault:flag = false}return flag};
测试一下
const arr = Array.of(3, 4, 5, 2)console.log(isType(arr, 'Array'))
类型识别正常
对于正则对象,我们在处理之前要先补充一点新知识.
我们需要通过正则的扩展了解到flags属性等等,因此我们需要实现一个提取flags的函数
const getRegExp = re => {var flags = ''if (re.global) flags += 'g'if (re.ignoreCase) flags += 'i'if (re.multiline) flags += 'm'return flags}
昨晚前置工作,就是把这些方法组合起来了,而且为了防止有循环引用,我们这里使用递归来进行遍历属性
const clone = parent => {const parents = [];const children = [];const _clone = parent => {if (parent === null) return null;if (typeof parent !== 'object') return parent;let child, proto;if (isType(parent, 'Array')) {child = [];} else if (isType(parent, 'RegExp')) {child = new RegExp(parent.source, getRegExp(parent));if (parent.lastIndex) child.lastIndex = parent.lastIndex;} else if (isType(parent, 'Date')) {child = new Date(parent.getTime());} else {proto = Object.getPrototypeOf(parent);child = Object.create(proto);}const index = parents.indexOf(parent);if (index != -1) {return children[index];}parents.push(parent);children.push(child);for (let i in parent) {child[i] = _clone(parent[i]);}return child;};return _clone(parent);};
声明一个复杂一点的对象来做测试
class person {constructor(pname) {this.name = pname}}const Messi = new person('Messi');function say() {console.log('hi');}const oldObj = {a: say,c: new RegExp('ab+c', 'i'),d: Messi,};oldObj.b = oldObj;const newObj = deepClone(oldObj);console.log(newObj)console.log(newObj==oldObj)
如下,
对于一些对象属性只是原始类型或数组的对象但又有循环嵌套的对象处理方法
如标题所示,其实很多时候,要拷贝的对象没有那么复杂,所以我们可以使用简单一点的方法来实现深拷贝
对于对象属性只是原始类型或数组的对象但又有循环嵌套的对象处理方法如下
方法一
function deepClone(obj) {const objectMap = new Map()const _deepClone = value => {const type = typeof valueif (type !== 'object' || type === null) return valueif (objectMap.has(value)) return objectMap.get(value)const result = Array.isArray(value) ? [] : {}objectMap.set(value, result)for (const key in value) {result[key] = _deepClone(value[key])}return result}return _deepClone(obj)}
声明一个简单的对象来测试一下
方式二
function deepClone(obj) {return new Promise(res => {const { port1, port2 } = new MessageChannel()port1.postMessage(obj)port2.onmessage = msg => {res(msg.data)}})}
继续拿刚刚那个对象做测试
let oldObj = {a: 11,b: '123',c: [1, 2, 3, '4']}oldObj.d = oldObjdeepClone(oldObj).then(v => {console.log(v)console.log(newObj == oldObj)})
输出如下