一、浅拷贝的原理与实现
浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值;如果拷贝的是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。
1. Object.assign()
object.assign
是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并。我们可以使用它来实现浅拷贝。
该方法的参数 target 指的是目标对象,sources指的是源对象。使用形式如下:
Object.assign(target, ...sources) 复制代码
使用示例:
let target = {a: 1}; let object2 = {b: {d : 2}}; let object3 = {c: 3}; Object.assign(target, object2, object3); console.log(target); // {a: 1, b: {d : 2}, c: 3} 复制代码
这里通过 Object.assign 将 object2 和 object3 拷贝到了 target 对象中,下面来尝试将 object2 对象中的 b 属性中的d属性由 2 修改为 666:
object2.b.d = 666; console.log(target); // {a: 1, b: {d: 666}, c: 3} 复制代码
可以看到,target的b属性值的d属性值变成了666,因为这个b的属性值是一个对象,它保存了该对象的内存地址,当原对象发生变化时,引用他的值也会发生变化。
注意:
- 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性;
- 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回;
- 因为
null
和undefined
不能转化为对象,所以第一个参数不能为null
或undefined
,否则会报错; - 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。
实际上,Object.assign 会循环遍历原对象的可枚举属性,通过复制的方式将其赋值给目标对象的相应属性。
2. 扩展运算符
使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。使用形式如下:
let cloneObj = { ...obj }; 复制代码
使用示例:
let obj1 = {a:1,b:{c:1}} let obj2 = {...obj1}; obj1.a = 2; console.log(obj1); //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj1.b.c = 2; console.log(obj1); //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}} 复制代码
扩展运算符 和 object.assign 实现的浅拷贝的功能差不多,如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
3. 数组浅拷贝
(1)Array.prototype.slice()
slice()
方法是JavaScript数组方法,该方法可以从已有数组中返回选定的元素,不会改变原始数组。使用方式如下:
array.slice(start, end) 复制代码
该方法有两个参数,两个参数都可选:
- start: 规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
- end:规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
如果两个参数都不写,就可以实现一个数组的浅拷贝:
let arr = [1,2,3,4]; console.log(arr.slice()); // [1,2,3,4] console.log(arr.slice() === arr); //false 复制代码
slice 方法不会修改原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。原数组的元素会按照下述规则拷贝:
- 如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
- 对于字符串、数字及布尔值来说,slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
如果向两个数组任一中添加了新元素,则另一个不会受到影响。
(2)Array.prototype.concat()
concat()
方法用于合并两个或多个数组,此方法不会更改原始数组,而是返回一个新数组。使用方式如下:
arrayObject.concat(arrayX,arrayX,......,arrayX) 复制代码
该方法的参数arrayX是一个数组或值,将被合并到arrayObject数组中。如果省略了所有 arrayX 参数,则 concat 会返回调用此方法的现存数组的一个浅拷贝:
let arr = [1,2,3,4]; console.log(arr.concat()); // [1,2,3,4] console.log(arr.concat() === arr); //false 复制代码
concat方法创建一个新的数组,它由被调用的对象中的元素组成,每个参数的顺序依次是该参数的元素(参数是数组)或参数本身(参数不是数组)。它不会递归到嵌套数组参数中。
concat方法不会改变this或任何作为参数提供的数组,而是返回一个浅拷贝,它包含与原始数组相结合的相同元素的副本。 原始数组的元素将复制到新数组中,如下所示:
- 对象引用(而不是实际对象):concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。
- 数据类型如字符串,数字和布尔值:concat将字符串和数字的值复制到新数组中。