JS浅拷贝及面试时手写源码

简介: JS浅拷贝及面试时手写源码

前言

在JavaScript中,浅拷贝和深拷贝是两种常用的对象复制方式,它们的区别主要体现在对嵌套对象的处理上。今天我们就来聊聊浅拷贝,了解浅拷贝的实现原理,并自己手搓一个浅拷贝,同时浅拷贝在前端面试中也是一个容易被问到的考点,我们一起来看看吧

我们知道对象是引用数据类型,我们先来看一道题:

let obj = {
    name: '菌菌'
}
let lw = obj
obj.name = '来颗奇趣蛋'
console.log(lw.name);

如果不太了解引用数据类型的小伙伴们,这里可能就会觉得输出菌菌了。让我们来看看输出结果:

image.png

我们可以看到,这里输出了来颗奇趣蛋。这是为什么呢,我们修改obj.name,为什么lw.name也被修改了呢?

  • 引用类型的值是存在堆当中,但是会将引用地址存在栈中

这里我们画张图来理解一下:(图画的不是很好,请见谅)

image.png

实际上,引用数据的值存放在堆区,也就是图中黄色框框所在的位置,堆区中存在着一段连续的地址,而这些地址又对应引用数据的值,如图中的1005,而栈区中变量存储的是堆区中的地址,就如图中obj = 1005,所以通过这些地址值,我们就可以访问堆区中的值,这里也可以叫指针

为什么改变obj.name会影响到lw.name呢?从图中观察到,obj和lw存储着相同的指向堆区的地址。当let obj = {...}时,obj会将变量的值的地址存储进去,这里为obj = 1005,而真正的变量值存储在堆区。当赋值语句lw = obj时,lw被赋值为obj相同的地址,lw = 1005.

而当执行到obj.name = '来颗奇趣蛋'时,先在栈区寻找变量obj,发现obj存储的是一段地址,然后会顺着obj存储的地址来到堆区,将堆区中name的值改为'来颗奇趣蛋'.所以当输出lw.name时,发现lw的值为1005,为地址,顺着地址找到了堆区中name的值,但此时name的值已经被修改为'来颗奇趣蛋',所以输出'来颗奇趣蛋',引用数据类型共享变量。

浅拷贝

JavaScript 中的浅拷贝是创建一个新对象,将原始对象的属性值复制到新对象中。当我们改变新(原)对象中属性的值时,原(新)对象中的属性值也会改变,这就是浅拷贝的特性。在我们上面那道例题中,let lw = obj,直接将原对象赋值给新对象,这同样也是浅拷贝的一种方式,所以当我们改变对象中的值时,另一个对象也会改变。

当然直接将对象赋给另一个对象,这是最简单的方式,大家也都知道,我们下面来聊聊别的浅拷贝方法:

Object.create(x)

Object.create 方法用于创建一个新对象,新对象的原型是指定的对象或 null。虽然 Object.create 通常用于对象的继承,但也可以实现一种浅拷贝的效果,因为新创建的对象与原始对象之间存在原型链关系。

js

代码解读

复制代码

const sourceObject = {
  a: 1,
  b: {
    c: 2
  }
};
// 使用 Object.create 实现浅拷贝
const shallowCopy = Object.create(sourceObject);
console.log(shallowCopy.a); // 1
console.log(shallowCopy.b.c); // 2
// 修改原始对象的嵌套属性
sourceObject.b.c = 99;
console.log(shallowCopy.b.c); // 99,因为浅拷贝中嵌套对象的引用未复制

在这个示例中,Object.create(sourceObject) 创建了一个新对象 shallowCopy,并将 sourceObject 设为 shallowCopy 的原型。这样,shallowCopy 通过原型链访问了 sourceObject 的属性。

需要注意的是,Object.create只复制对象的第一层属性,而不会递归复制嵌套对象。也就是说,如果我们修改sourceObject.a = 66, shallowCopy中的属性a的值是不会改变的,但如果原始对象包含引用类型,也就是此题中的b对象,当我们改变b对象中属性的值,还是会影响新对象,所以还是浅拷贝。

Object.assign({}, x)

Object.assign({}, x) 方法用于创建一个浅拷贝,将一个或多个源对象的可枚举属性复制到目标对象。这是一种常见的浅拷贝方法,适用于对象的第一层属性,但不会递归地复制嵌套对象。

const sourceObject = {
  a: 1,
  b: {
    c: 2
  }
};
// 使用 Object.assign 实现浅拷贝
const shallowCopy = Object.assign({}, sourceObject);
console.log(shallowCopy.a); // 1
console.log(shallowCopy.b.c); // 2
// 修改原始对象的嵌套属性
sourceObject.b.c = 99;
console.log(shallowCopy.b.c); // 99,因为浅拷贝中嵌套对象的引用未复制

在这个示例中,Object.assign({}, sourceObject) 创建了一个新对象 shallowCopy,并将 sourceObject 的属性复制到了 shallowCopy。这样,shallowCopy 中的属性与 sourceObject 中的属性具有相同的值。

此方法同样与上方法一样,只复制第一层属性,当我们改变b对象中属性的值,还是会影响新对象,所以还是浅拷贝。

concat

在 JavaScript 中,concat 方法用于连接两个或多个数组,并返回一个新数组。对于数组中的基本类型元素,concat 方法实现了浅拷贝。

示例:

let arr = [1, 2, 3, {a: 10}] 
let newArr = [].concat(arr)
arr[3].a = 100
console.log(newArr); // [1, 2, 3, {a: 100}]

concat 方法对于数组中的对象是浅拷贝的。在这个例子中,arr 中的第四个元素是一个对象 {a: 10},通过 concat 方法创建的新数组 newArr 仍然引用相同的对象。

所以,当你修改 arr 中的对象时,这个变化也会反映在 newArr 中,因为它们实际上共享同一个对象引用。

这是因为 concat 方法只复制数组的一层,而不是递归复制数组中的每个对象。

slice

slice 方法是复制数组的一部分,它从给定的开始索引复制到结束索引,并返回一个新数组。

let arr = [1, 2, 3, {a: 10}] 
let newArr = arr.slice()
arr[3].a = 100
console.log(newArr); // [1, 2, 3, {a: 100}]

arr 使用 slice 方法创建的新数组 newArr,但仍然共享相同的对象引用。因此,当你修改 arr 中的对象时,这个变化也会反映在 newArr 中。只复制对象的第一层属性,而不会递归复制嵌套对象。当我们改变对象中的属性值时,还是会影响到新的对象。

浅拷贝实现源码

function shalldowCopy(obj) {
    
    if (typeof obj !== 'object' || obj == null) return 
    let objCopy = obj instanceof Array ? [] : {}
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            objCopy[key] = obj[key]
        }
    }
    return objCopy
}
  1. 类型检查:if (typeof obj !== 'object' || obj == null) return obj;
  • 如果传入的 obj 不是对象或者为 null,则直接返回原对象,因为不需要拷贝。
  1. 创建新对象:let objCopy = obj instanceof Array ? [] : {};
  • 根据原对象的类型创建一个新的对象或数组。这里使用 instanceof 检查 obj 是否是数组。
  1. 遍历属性:for (let key in obj) { ... }
  • 使用 for...in 循环遍历原对象的属性。
  1. 判断属性所有权:if (obj.hasOwnProperty(key)) { ... }
  • 使用 hasOwnProperty 方法判断属性是否为对象自身的属性,而不是原型链上的属性。
  1. 复制属性:objCopy[key] = obj[key];
  • 将原对象的属性复制到新对象中。
  1. 返回新对象:return objCopy;
  • 返回新的对象,完成浅拷贝。

总结

浅拷贝

  • 常见的浅拷贝方法:
  1. Object.create(x)
  2. Object.assign({}, x)
  3. concat
  4. slice

我们需要注意的是,这些方法只复制对象的第一层属性,而不会递归复制嵌套对象。也就是说,如果我们修改嵌套对象的值,也就是说原始对象包含引用类型的值,还是会影响新对象,所以还是浅拷贝。

目录
打赏
0
0
0
0
43
分享
相关文章
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
168 2
|
5月前
|
JS实现简单的打地鼠小游戏源码
这是一款基于JS实现简单的打地鼠小游戏源码。画面中的九宫格中随机出现一个地鼠,玩家移动并点击鼠标控制画面中的锤子打地鼠。打中地鼠会出现卡通爆破效果。同时左上角统计打地鼠获得的分数
233 1
当面试官再问我JS闭包时,我能答出来的都在这里了。
闭包(Closure)是前端面试中的高频考点,广泛应用于函数式编程中。它不仅指函数内部定义的函数,还涉及内存管理、作用域链和垃圾回收机制。闭包可以让函数访问其外部作用域的变量,但也可能引发内存泄漏等问题。通过合理使用闭包,可以实现模块化、高阶函数和回调函数等应用场景。然而,滥用闭包可能导致代码复杂度增加、调试困难以及潜在的性能问题。为了避免这些问题,开发时应谨慎处理闭包,避免不必要的嵌套,并及时清理不再使用的变量和监听器。
146 16
当面试官再问我JS闭包时,我能答出来的都在这里了。
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
3月前
html+js+css实现的建筑方块立体数字时钟源码
html+js+css实现的建筑方块立体数字时钟源码
131 33
|
4月前
一个好看的小时钟html+js+css源码
一个好看的小时钟html+js+css源码
145 24
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
html5 + Three.js 3D风雪封印在棱镜中的梅花鹿动效源码
html5 + Three.js 3D风雪封印在棱镜中的梅花鹿动效源码。画面中心是悬浮于空的梅花鹿,其四周由白色线段组成了一个6边形将中心的梅花鹿包裹其中。四周漂浮的白雪随着多边形的转动而同步旋转。建议使用支持HTML5与css3效果较好的火狐(Firefox)或谷歌(Chrome)等浏览器预览本源码。
137 2
[JS]面试官:你的简历上写着熟悉jsonp,那你说说它的底层逻辑是怎样的?
本文介绍了JSONP的工作原理及其在解决跨域请求中的应用。首先解释了同源策略的概念,然后通过多个示例详细阐述了JSONP如何通过动态解释服务端返回的JavaScript脚本来实现跨域数据交互。文章还探讨了使用jQuery的`$.ajax`方法封装JSONP请求的方式,并提供了具体的代码示例。最后,通过一个更复杂的示例展示了如何处理JSON格式的响应数据。
97 2
[JS]面试官:你的简历上写着熟悉jsonp,那你说说它的底层逻辑是怎样的?
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
120 2

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等