对象拷贝
1、数据类型
在JavaScript中,数据类型可以分为基本数据类型和引用数据类型。
- 基本数据类型包括:Undefined、Null、Number、String、Boolean以及ES6新加的Symbol。
- 引用数据类型包括Array和Object等。
因为基本数据类型操作比较频繁,并且所占空间较小,所以是存放在栈中,提升变量的访问速度和效率。而引用数据类型所占的空间比较大,不适合存放在栈中,故将数据实体存放在堆中,在栈中只存放该数据的地址,访问引用数据类型的时候先通过栈中的地址来找到堆中的数据实体。
引用类型的数据之间赋值给另外一个变量,无论改变新变量的数据还是原数据,另外一个变量里面存放的数据也会改变。例如:
let arr = [1,2,3];let arr1 = arr;arr1[0] = 50;console.log(arr); // [50,2,3]console.log(arr1); // [50,2,3]
这是因为引用类型在赋值的时候是将自身的存在栈中的地址复制给了新变量,而不是自身实际的值。因为两个新老变量指向的是堆中同一个地址,也就对应着同一个数据实体,所以改变任意一个变量的值,都会造成另外一个变量值的改变。
在实际开发中,有时候需要对数据进行完全拷贝,生成一个新的变量来处理逻辑,这样就不能直接采用赋值的方式来拷贝了。在JavaScript中,拷贝可以分为浅拷贝和深拷贝两种方式。下面将详细对这两种方式进行讲解。
2、浅拷贝
从字面理解,我们可以似懂非懂的理解这个含义,那什么是浅拷贝呢?浅拷贝就是只对数据进行较浅处的拷贝,什么意思呢?就是当数据类型为基本数据类型的时候,是完全复制出一个新的数值出来,再次去改变原对象的时候,新对象不受影响;但如果数据对象中一些数据是引用类型时,那么当改变原对象或者新对象数据中为对象的值的时候,新老对象都会改变。因为拷贝的还是指针。
可能定义比较抽象,下面以obj对象为例,采用JavaScript中的浅拷贝方式进行拷贝,得到个新对象obj1,如果改变对象中的a或者b,则obj和obj1互不影响,因为a和b的数据类型是基本数据类型;但如果直接改变obj或者obj1中的c时,则两个对象中的c都会变成一样,因为c本身是一个引用数据类型,采用浅拷贝方式拷贝的还是c数据对象的地址。
// 定义一个层次较深的对象,后面的数据都以这个为例let obj = {a: 1,b: \'hello\',c:{d:3,e:{f:4}}}
JavaScript中提供了几种方式实现浅拷贝:
- Object.assign()
let obj1 = Object.assign({},obj);obj1.a = 20;obj1.c.d = \'world\'console.log(obj.a) // 原对象obj.a还是1不变console.log(obj1.a) // 新对象obj1.a变为了20console.log(obj.c) // 原对象obj.c中的d也变为\'world\'console.log(obj1.c) // 新对象obj1.c中的d变成了\'world\'
从上面这个例子,就可以理解浅拷贝的含义了,当数据中含有引用类型的数据时,用的还是同一个指针地址。除此之外,在JavaScript中,还可以用其他的方法来实现浅拷贝。
- ES6新增的
...
运算符也可以实现浅拷贝的功能。
let obj2 = {...obj} //完成对obj对象的拷贝obj2.a = 20;obj2.c.d = \'world\'console.log(obj.a) // 原对象obj.a还是1不变console.log(obj2.a) // 新对象obj2.a变为了20console.log(obj.c) // 原对象obj.c中的d也变为\'world\'console.log(obj2.c) // 新对象obj2.c中的d变成了\'world\'
- 除去上面的两种对象方式外,数组Array的
concat()
和
slice()
以及
splice()
都可以实现对数据的浅拷贝。
3、深拷贝
通过上面对浅拷贝的理解,相信对深拷贝的概念就更加的清楚了,深拷贝就是在数据中有引用类型数据的时候,无论改变原数据还是新数据二者之间都不会造成数据共享。
- 在JavaScript中可以通过
JSON.parse(JSON.stringify())
的方法来实现对数据的深拷贝。
let obj3 = JSON.parse(JSON.stringify(obj));obj3.a = 20;obj3.c.d = \'world\'console.log(obj.a) // 原对象obj.a还是1不变console.log(obj3.a) // 新对象obj3.a变为了20console.log(obj.c) // 原对象obj.c中的d依然还是3console.log(obj3.c) // 新对象obj3.c中的d变成了\'world\'
从这个例子看出,深拷贝就是完成对数值的复制,而不是把引用类型的数据也复制过来。
JavaScript通过的这个深拷贝方法,虽然简单好用,但也存在很多的不足,例如:
- 会自动忽略undefined
- 会忽略symbol
- 不能序列化函数,也就是说如果数据中有函数对象,就不能序列化,新对象无法读取到
- 不能解决循环引用的对象
4、手写浅拷贝与深拷贝
虽然JavaScript提供了几种方法来实现浅拷贝,但手写浅拷贝和理解其工作原理是找工作面试中的一道热门考点。而JavaScript提供的深拷贝的方法对有一些局限性,至此,我们自己来实现一个深拷贝和浅拷贝的代码。
- 浅拷贝
var shallowCopy = function(obj){// 判断传进来的数据是否是对象,如果不是,直接返回if(typeof obj !== \'object\') return;// 判断传进来的数据是数组类型还是Object对象,然后定义一个新的变量newObj// 此处的instanceof是判断obj是否输入Arra的实例,也可以用Array.isArray(obj)来判断是否是数组let newObj = obj instanceof Array ? [] : {}// 对象循环for(let key in obj){// 判断一下obj数据里面是否有key这个键if(obj.hasOwnProperty(key)){newObj[key] = obj[key];//直接赋值给新的对象}}return newObj}
- 深拷贝,深拷贝在浅拷贝代码的基础上,判断一下子数据是否为对象,然后递归调用一下自己。
var deepCopy = function(obj){// 判断传进来的数据是否是对象,如果不是,直接返回if(typeof obj !== \'object\') return;// 判断传进来的数据是数组类型还是Object对象,然后定义一个新的变量newObj// 此处的instanceof是判断obj是否输入Arra的实例,也可以用Array.isArray(obj)来判断是否是数组let newObj = obj instanceof Array ? [] : {}// 对象循环for(let key in obj){// 判断一下obj数据里面是否有key这个键if(obj.hasOwnProperty(key)){// 判断子数据类型是否也是对象,如果是对象,则递归一次,如果不是就直接赋值newObj[key] = typeof obj[key] === \'object\' ? deepCopy(obj[key]) : obj[key];}}return newObj}
然后通过调用
shallowCopy(obj)
和
deepCopy(obj)
就可以实现浅拷贝和深拷贝了。