在 JavaScript 中,因为对象和数组是引用类型,所以当我们直接将它们赋值给另一个变量时,实际上是将它们的引用地址复制了一份。这样,当我们对其中一个变量进行修改时,另一个变量也会受到影响。因此,为了避免这种情况,我们通常需要使用拷贝方法来复制一个对象或数组的值并创建一个新的副本。本文将来介绍浅拷贝和深拷贝的概念以及它们的应用场景。
浅拷贝
浅拷贝是指将原对象或数组的值复制到一个新的对象或数组中,但是新的对象或数组的属性或元素依然是原对象或数组的引用,这意味着当我们修改其中一个对象或数组时,另一个对象或数组也会受到影响。因此,浅拷贝通常只针对引用类型。下面是常见的浅拷贝方法:
1. Object.create(obj)
Object.create() 方法可以用于创建一个新对象,并将原对象作为新对象的原型。这样,新对象就可以访问原对象的所有属性和方法。
示例代码:
ini
const obj1 = { name: '张三', age: 18 };
const obj2 = Object.create(obj1);
console.log(obj2.name); // 张三
obj1.name = '李四'
console.log(obj2.name); //李四
我们可以看到,obj2具有obj1的属性,且当obj1的属性修改时,obj2的这个属性也变了。
2. Object.assign({}, obj)
Object.assign() 方法是用于将一个或多个源对象的属性复制到目标对象中。它的语法如下:
css
Object.assign(target, ...sources)
其中,target 是目标对象,sources 是一个或多个源对象。
该方法会遍历源对象的所有可枚举属性,并将其复制到目标对象中。如果目标对象中有相同的属性,则后面的属性会覆盖前面的属性。
需要注意的是,该方法只会复制对象自身的属性,不会复制继承自原型链上的属性 。而且,如果源对象中有值为 null 或 undefined 的属性,则该属性不会被复制。
浅拷贝示例代码:
css
let a = {
name:'A',
like:{n: 'coding'}
}
let b = Object.assign({},a)
a.name = 'B'
a.like.n = 'running'
console.log(b.name); // 输出A
console.log(b.like.n); // 输出running
当a中的原始类型属性被修改时,b中的这个属性是不会跟着修改的,但是引用类型被修改则不然,这是因为原始类型与引用类型的存放位置是不同的,在代码执行的时候,原始类型的值存放在调用栈中,而引用类型的值则会被存放在堆中,如果对这点有疑惑的小伙伴请去看一下关于数据类型分类,构造函数和包装类的重要底层原理这篇文章的数据类型分类部分哦!
新对象b中从a中复制过来的所有原始类型的属性都是新的属性,在b的执行上下文中具有自己的空间地址,而复制过来的引用类型like的值并不是它真正的值,而是它在堆中的引用地址,所以在上述代码中b.like和a.like仍然是同一个对象,它们在调用栈中的值都指向同一个堆中的地址,所以当a.like中的属性被修改时,b.like的属性也变了。
3. [].concat(arr)
[].concat() 方法可以用于将一个或多个数组合并成一个新数组。
示例代码:
scss
let arr = [1,2,3]
let newArr = [].concat(arr)
arr.push(4)
arr[0] = 10
console.log(newArr);//输出[1,2,3]
这个时候修改原数组arr,新数组newArr是不变的,那是不是说明这是深拷贝呢?不,如果数组中的值为对象呢?
ini
let arr = [1,2,3,{n:10}]
let newArr = [].concat(arr)
arr[3].n = 100
console.log(newArr); //输出[1,2,3,{n:100}]
当对象中的值修改时,新数组中的对象中这个值也变了,原理同上一个方法相似。
4. 数组解构 [...arr]
数组解构是一种将数组或类数组对象中的值提取出来,赋值给变量的方法。它可以让我们更方便地访问数组的元素。
[...arr]使用示例代码:
ini
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5, 6];
console.log(arr2); // [1, 2, 3, 4, 5, 6]
为什么它也是浅拷贝方法呢?
ini
let arr = [1,2,3,{n:10}]
let newArr = [...arr]
arr[3].n = 100
console.log(newArr);
同上面讲的方法一样,它也是原数组有些值修改后会跟着变。
5. arr.slice()
slice() 方法是 JavaScript 数组对象的一个方法,用于从数组中提取指定区间的元素创建一个新的数组,并不会对原数组产生影响。它接受两个参数:开始索引和结束索引,当参数为空时,则会复制整个原数组 arr。这意味着返回的新数组与原数组具有相同的元素、长度和顺序。
示例代码:
ini
const arr1 = [1, 2, 3];
const arr2 = arr1.slice();
console.log(arr2); // [1, 2, 3]
若原数组中值存在对象,则结果也同上面的方法,原数组中的对象中的属性值改变新数组也跟着变。
深拷贝
深拷贝是指将原对象或数组的值复制到一个新的对象或数组中,并且新的对象或数组的属性或元素完全独立于原对象或数组,即它们不共享引用地址。因此,当我们修改其中一个对象或数组时,另一个对象或数组不会受到任何影响。
常见的深拷贝方法是使用 JSON.parse(JSON.stringify(obj)),它的语法如下:
ini
let newObj = JSON.parse(JSON.stringify(obj));
JSON.stringify() 方法是 JavaScript 的一个内置方法,用于将一个 JavaScript 对象或值转换为 JSON 字符串,而JSON.parse()用于将 JSON 字符串解析为对应的 JavaScript 对象或值。
这个方法可以将一个对象序列化为 JSON 字符串,然后再将 JSON 字符串解析为一个新的对象,因为它曾转化为字符串,所以不会出现像浅拷贝中那种引用类型指向同一地址的情况,从而实现深拷贝。
ini
let obj = {
name: 'A',
age:18,
a:{
n:1
}
}
let obj2 = JSON.parse(JSON.stringify(obj))
obj.a.n = 2
console.log(obj2.a.n); //输出1
但是,这种方法也存在一些缺陷:
- 不能处理 undefined、function 和 Symbol 等特殊数据类型,因为它们在 JSON 中没有对应的表示方式。
- 无法处理循环引用,即当一个对象的属性指向自身或形成循环引用关系时,深拷贝会陷入无限递归。
手写实现深拷贝
scss
function deepCopy(obj){
//不是引用类型就不拷贝
if(!(obj instanceof Object)) return
//如果形参obj是数组,就创建数组,如果是对象就创建对象
let objCopy = obj instanceof Array ? [] : {}
for(let key in obj){
if(obj.hasOwnProperty(key)){
objCopy[key] = obj[key]
}
}
return objCopy
}
本文到这就讲完啦!欢迎下次再来一起学习ヾ(◍°∇°◍)ノ゙!!