- 作者:老汪软件技巧
- 发表时间:2024-10-14 15:03
- 浏览量:
引言
在处理对象和数组这类复合数据类型时,一个常见的挑战就是如何正确地复制这些数据结构,以避免不必要的副作用。这就是“深浅拷贝”概念登场的地方。本文旨在通过深入浅出的方式,带你全面了解JavaScript中深浅拷贝的概念、方法以及实现原理。
let a = {
name: '今天一定晴q',
age: 18,
like: {
n: 'sleep'
}
}
现在我有一个对象 a ,如果我创造出了一个新对象并让它长得跟 a 一样,那么这就是拷贝。但是注意,let b = a 这不叫拷贝,因为在堆中还是只有一个对象。
拷贝又分为深拷贝和浅拷贝。
浅拷贝
基于原对象拷贝得到一个新的对象,原对象中数据的修改会影响新对象。有以下六种方法:
Object.create(x)
let b = Object.create(a)
a.age = 20
console.log(b.age) // 20 b被a的修改影响了
Object.create(a)创建了一个新对象b,并且能够让新对象隐式继承到原对象 a 的属性,这些属性存在 b 的原型上
Object.assign({}, x)
Object.assign(a, b)可以把 b 里面的属性拼接到 a 上,这样 a 对象就拥有了 a 和 b 所有的属性:
let b = {
sex: 'female'
}
let c = Object.assign(a, b)
console.log(c) // { name: '今天一定晴q', age: 18, sex: 'female' }
console.log(a) // { name: '今天一定晴q', age: 18, sex: 'female' } c 和 a 就是一个东西
所以可以利用Object.assign({}, a)拷贝出一个和 a 相同的新对象
需要注意的是,如果 a 和 b 中存在引用类型(如对象或数组),那么 Object.assign 只会复制引用,因此原对象中数据的修改会影响新对象
let c = Object.assign({}, a)
a.age = 20
a.like.n = 'food'
console.log(c) // { name: '今天一定晴q', age: 18, like: { n: 'food' } }
[].concat(arr)
将 arr 中的元素和 [] 中的元素合并,返回一个新数组 newArr
let arr = [1, 2, 3, {a: 10}]
let newArr = [].concat(arr)
arr[1] = 20
arr[3].a = 100
console.log(newArr); // [ 1, 2, 3, { a: 100 } ] 引用类型的元素被改变了,拷贝还是不够彻底
数组解构
将原数组的元素解构到一个新数组中
let arr = [1, 2, 3, {a: 10}]
let newArr = [...arr]
arr[3].a = 100
console.log(newArr); // [ 1, 2, 3, { a: 100 } ]
arr.slice(0)
arr.slice(a, b)可以接收两个参数,分别是开始的下标和结束的下标(左闭右开),返回一个数组,包含我们需要取下的那一节数据,原数组不受影响
let arr = [1, 2, 3, {a: 10}]
let s = arr.slice(1, 2)
console.log(s); // [ 2 ]
如果不写第二个参数就可以一截到底,直接取下原数组中所有元素返回一个新数组。但改变原数组中的引用类型元素,新数组还是会受到影响
let arr = [1, 2, 3, {a: 10}]
let newArr = arr.slice(0)
console.log(newArr); // [ 1, 2, 3, { a: 10 } ]
arr[3].a = 100
console.log(newArr); // [ 1, 2, 3, { a: 100 } ]
arr.toReversed().reverse()
toReversed()会翻转数组并返回出一个新数组,而reversed()会将原数组的元素翻转
let arr = [1, 2, 3, {a: 10}]
let newArr = arr.toReversed().reverse()
console.log(newArr); // [ 1, 2, 3, { a: 10 } ]
实现原理创建新对象借助for in 遍历原对象如果key是obj显式具有的属性,就将该属性及其值增添至新对象中返回这个新对象
function shallowCopy(obj) {
let newObj = {}
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
当遍历对象的所有键名时,会遍历到对象隐式具有的属性(即对象原型上的属性),但是开发过程中我们一般只需要拷贝对象显式具有的属性,所以使用if(obj.hasOwnProperty(key)) {...}来判断要拷贝的属性是否为对象显式具有的
深拷贝
基于原对象拷贝得到一个新的对象,原对象中内容的修改不会影响新对象。有以下两种方法:
JSON.parse(JSON.stringify(obj))
把对象变成JSON字符串,再把JSON字符串变成对象
let obj = {
name: '今天一定晴q',
age: 18,
like: {
n: 'money'
},
a: true,
b: null,
c: undefined,
d: Symbol('f'),
f: function() {}
}
let newObj = JSON.parse(JSON.stringify(obj))
obj.like.n = 'coding'
console.log(newObj); // { name: '今天一定晴q', age: 18, like: { n: 'money' }, a: true, b: null }
正如代码所示,这种方法有几个缺点:
structuredClone(obj)
js新的内置的深拷贝方法,可能目前还有很多浏览器没跟上
const user = {
name: {
firstname: '今天一定晴',
lastname: 'q'
},
age: 18
}
const newUser = structuredClone(user)
user.name.firstname = 'hello'
console.log(newUser) // { name: { firstname: '今天一定晴', lastname: 'q' }, age: 18 }
实现原理创建新对象借助for in 遍历原对象如果key是obj显式具有的属性,就将该属性及其值增添至新对象中如果遍历到的属性值是原始类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象返回这个新对象
function deepClone(obj) {
let newObj = {}
for(let key in obj) {
if(obj.hasOwnProperty(key)) { // 只拷贝显式具有的属性
if(typeof obj[key] === 'object' && obj[key] !== null) { // 如果是对象,递归拷贝
newObj[key] = deepClone(obj[key])
} else {
newObj[key] = obj[key]
}
}
}
return newObj
}
这种深拷贝写法属于是乞丐版,但是面试中会写这种就够了,让面试官明白你懂里面的核心思想
结语
以上带大家全面了解了拷贝的所有方法,并手搓了实现原理。两种拷贝没有优劣之分,我们只需认清二者的区别,拎清使用场景。