【吃透Js】深入学习浅拷贝和深拷贝

news2025/1/13 15:36:58

    • 一、JavaScript数据类型
      • 原始类型
      • 对象类型
    • 二、原始类型和对象类型的区别
      • 1.原始类型
      • 2.引用类型
      • 3.复制
      • 4.比较
      • 5.值传递
    • 三、浅拷贝
      • 概念
      • 实现方法
    • 四、深拷贝
      • 概念
    • 五、浅拷贝、深拷贝和赋值的区别
      • 浅拷贝和赋值
    • 六、小结

想要真正搞明白深浅拷贝,你必须要熟练掌握赋值、对象在内存中的存储、数据类型等基础知识。

为了更好地掌握深浅拷贝,我们先来看一下数据类型和在内存中的存储形式。

一、JavaScript数据类型

原始类型

  • Null:只包含一个值:null
  • Undefined:只包含一个值:undefined
  • Boolean:包含两个值:truefalse
  • Number:整数或浮点数,还有一些特殊值(-Infinity+InfinityNaN
  • String:一串表示文本值的字符序列
  • Symbol:一种实例是唯一且不可改变的数据类型

对象类型

  • Object:自己分一类丝毫不过分,除了常用的Object,Array、Function等都属于特殊的对象

二、原始类型和对象类型的区别

1.原始类型

在JavaScript中,每一个变量都需要一个内存空间来存储。
内存空间被分为两种:堆内存和栈内存。

JavaScript中的原始类型的值被直接存储在栈中,在变量定义时,栈就为其分配好了内存空间。

栈内存特点:

  • 存储的值大小固定
  • 空间较小
  • 可以直接操作其保存的变量,运行效率高
  • 由系统自动分配存储空间

原始类型的特点就是不可变性,即值本身是不可被改变的。

var str = 'JingYu';
str.slice(1);
str.substr(1);
str.trim(1);
str.toLowerCase(1);
str[0] = 1;
console.log(str);  // JingYu

有一个特殊情况:

str += '6'
console.log(str);  // JingYu6

这个情况是不满足不可变性了吗?不是的,在上面的代码中,我们执行了str += ‘6’的操作,实际上是在栈中又开辟了一块内存空间用于存储’JingYu6’,然后将变量str指向这块空间,所以这并不违背不可变性的特点。
在这里插入图片描述

2.引用类型

JavaScript中的引用类型(对象类型)的值实际被直接存储在堆内存中,在栈内存中只存储了一个固定长度的地址,这个地址指向堆内存中的值。

堆内存特点:

  • 存储的值大小不定,可动态调整
  • 空间较大,运行效率低
  • 无法直接操作其内部存储,使用引用地址读取
  • 通过代码进行分配空间

引用类型就不具有不可变性的特点了,我们可以轻易改变它,尤其是数组有很多函数可以改变:

  • pop() 删除数组最后一个元素,如果数组为空,则不改变数组,返回undefined,改变原数组,返回被删除的元素
  • push()向数组末尾添加一个或多个元素,改变原数组,返回新数组的长度
  • shift()把数组的第一个元素删除,若空数组,不进行任何操作,返回undefined,改变原数组,返回第一个元素的值
  • unshift()向数组的开头添加一个或多个元素,改变原数组,返回新数组的长度
  • reverse()颠倒数组中元素的顺序,改变原数组,返回该数组
  • sort()对数组元素进行排序,改变原数组,返回该数组
  • splice()从数组中添加/删除项目,改变原数组,返回被删除的元素

3.复制

当我们把一个变量复制到另一个变量的时候,原始类型和引用类型的表现也是不同的。
原始类型:

var name = 'JingYu';
var name2 = name;
name2 = 'JINGYU';
console.log(name); // JingYu;

通过输出结果可以看出,我们修改name2的结果的时候,对name没有任何的影响。那是因为我们将变量name复制给name2的时候是在栈内存空间中创建了一块新的内存空间,这块内存空间存储的是变量name2的值,值与变量name是相同的但是内存空间地址是完全不同的,所以修改name2之后name的值不变。
引用类型:

var obj = {name:'JingYu'};
var obj2 = obj;
obj2.name = 'JINGYU';
console.log(obj.name); // JINGYU

你会惊奇的发现,显示的结果和原始类型是不同的。这又是为什么呢?
那是因为当我们复制引用类型的变量时,实际上复制的是栈中存储的地址,所以复制出来的obj2实际上和obj指向的堆中同一个对象。因此,我们改变其中任何一个变量的值,另一个变量都会受到影响,这就是为什么会有深拷贝和浅拷贝的原因。

4.比较

先看一下代码,猜猜运行结果…

var name = 'JingYu';
var name2 = 'JingYu';
console.log(name === name2); // true
var obj = {name:'JingYu'};
var obj2 = {name:'JingYu'};
console.log(obj === obj2); // false

对于原始类型,比较时会直接比较它们的值,如果值相等,即返回true。
对于引用类型,比较时会比较它们的引用地址,虽然两个变量在堆中存储的对象具有的属性值都是相等的,但是它们被存储在了不同的存储空间,因此比较值为false。

5.值传递

首先给出结论:ECMAScript中所有的函数的参数都是按值传递的。
ECMAScript中是没有引用传递的。

let name = 'JingYu';
function changeValue(name){
  name = 'JINGYU';
}
changeValue(name);
console.log(name);

很明显,上面的执行结果是’JingYu’,即函数参数仅仅是被传入变量复制给了的一个局部变量,改变这个局部变量不会对外部变量产生影响。

let obj = {name:'JingYu'};
function changeValue(obj){
  obj.name = 'JINGYU';
}
changeValue(obj);
console.log(obj.name); //JINGYU

当函数参数是引用类型时,我们同样将参数复制了一个副本到局部变量,只不过复制的这个副本是指向堆内存中的地址而已,我们在函数内部对对象的属性进行操作,实际上和外部变量指向堆内存中的值相同,但是这并不代表着引用传递。

let obj = {};
function changeValue(obj){
  obj.name = 'JingYu';
  obj = {name:'JINGYU'};
}
changeValue(obj);
console.log(obj.name); // JingYu

obj = {name:'JINGYU'};这个只是函数内部的局部对象。

三、浅拷贝

基础知识前面我们已经介绍过了。现在进入我们的正题。

概念

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

实现方法

简单的浅拷贝

function shallowClone(obj) {
    const newObj = {};
    for(let prop in obj) {
        if(obj.hasOwnProperty(prop)){
            newObj[prop] = obj[prop];
        }
    }
    return newObj;
}

在JavaScript中,存在浅拷贝现象的还有三种:
Object.assign

var obj = {
    age: 18,
    nature: ['smart', 'good'],
    names: {
        name1: 'fx',
        name2: 'xka'
    },
    love: function () {
        console.log('fx is a great girl')
    }
}
var newObj = Object.assign({}, fxObj);

Array.prototype.slice(), Array.prototype.concat()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

使用拓展运算符实现的复制

const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

四、深拷贝

概念

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式:
_.cloneDeep()

const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

jQuery.extend()

const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

JSON.stringify()

const obj2=JSON.parse(JSON.stringify(obj1));

但是这种方式存在弊端,会忽略undefined、symbol和函数

const obj = {
    name: 'A',
    name1: undefined,
    name3: function() {},
    name4:  Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}

手写循环递归

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

五、浅拷贝、深拷贝和赋值的区别

下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别

在这里插入图片描述
从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

// 浅拷贝
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存

console.log('obj1',obj1) // obj1 { name: 'init',  arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

// 深拷贝
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存

console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

浅拷贝和赋值

赋值

 var obj1={
        name:'张三',
        age:18,
        class:['一班']
    }
    var obj2=obj1//进行了赋值的操作
    obj2.name='李四'
    obj2.class[0]='二班'
    console.log(obj1)
    console.log(obj2)

在这里插入图片描述
从例子的可以看出赋值后的对象obj2改变,原对象obj1的值也改变.这是因为赋值后的对象obj2赋值的是原对象obj1的栈内存地址,他们指向的是同一个堆内存数据,所以对赋值后的对象obj2对数据进行操作会改变公共的堆内存中的数据,所以原对象的值也改变了。
浅拷贝

 var obj1={
        name:'张三',
        age:18,
        class:['一班']
    }
  function qianCopy(obj){
    var obj2={}
    for(var attr in obj){//循环对象的所有属性值
         if(obj.hasOwnProperty(attr)){
             obj2[attr]=obj1[attr]
         }
    }
    return obj2
  }
  var obj3=qianCopy(obj1)
  obj3.name='李四'
  obj3.age = 20
  obj3.class[0]='二·班'
  console.log(obj1)
  console.log(obj3)

在这里插入图片描述
从结果可以看出obj3改变了基本类型的值name,并没有使原对象obj1的name改变,obj3改变了引用类型的值,导致原对象的值也改变了

六、小结

赋值是完全复制,将一个对象赋值给另一个对象时,只是将一个对象在栈中的内存地址复制给另外一个对象,如果改变其中一个对象的属性值时另外一个对象的属性值也会跟着改变
浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,复制的是对象的内存地址,两个对象指向同一个地址
深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/379414.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

港科夜闻|香港科大与中国医药创新促进会共建创新研究平台,推动大湾区医药创新发展...

关注并星标每周阅读港科夜闻建立新视野 开启新思维1、香港科大与中国医药创新促进会共建创新研究平台,推动大湾区医药创新发展。香港科技大学与中国医药创新促进会2月27日签署合作备忘录,成立「大湾区生物医药国际创新中心」及「大湾区生物医药发展政策研…

【论文导读】Towards Unsupervised Domain Generalization

之前看到过的一篇论文,挺有意思的,虽然查到了有讲解的博客,但是不太符合我的思考逻辑 于是自己梳理一下。 CVPR 2022丨清华大学提出:无监督域泛化 (UDG)_我爱计算机视觉的博客-CSDN博客 方法引入: 针对现有的处理域…

苹果笔可以不买原装吗?开学必备性价比电容笔

在当今的时代,电容笔日益普及,而且相关的功能也逐渐完善。因此,在使用过程中,怎样挑选一款性价比比较高的电容笔成为大家关心的焦点。随着电容笔的普及,更好更便宜的电容笔成为了一种趋势。那么,哪个品牌的…

[ 云计算入门与实战 - AWS ] 在控制台创建 Amazon EC2 实例

本章节主要介绍在 AWS 控制台窗口 如何创建一台 Amazon EC2 实例。 文章目录写在前面开始创建前进入控制台并启动 EC2 实例服务创建 EC2 实例详细步骤步骤 1:为所要创建的 EC2 实例个体命名步骤 2:选择 AMI步骤 3:选择实例类型步骤 4&#xf…

【线程池的使用规范、线程池的7个参数、4种拒绝策略、线程池的5种状态、线程池的执行流程】

一.线程池的使用规范 阿里巴巴开发手册规定,线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让开发人员更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的…

EndNote20 自动生成毕业论文参考文献

软件版本: EndNote 20 字体:Chinese Standard GB/T 7714 去谷歌学术下载Endnote格式的enw文件 在EndNote中 File>New… 新建Libaray Tools>Output Styles>Open Style Manager… 勾选Chinese std GBT7714(numeric) Tools>Output Styles>…

CSS背景属性之颜色渐变

颜色渐变 颜色渐变其实在网页设计中并不是特别常见, 但也不可避免的会出现导航栏是渐变色这种情况或者别的不是单一颜色的情况, 例如:这样的设计解决方案并不是只可以使用颜色渐变,我们可以使用两个div拼接,将文字放…

如果坚定了想要进入网络安全领域的决心,应该怎样学习呢?

1、建立科学的学习路径 如果你原本从事程序开发,可以选择Web安全/渗透测试方向入门,一是市场需求量高,二则是发展相对成熟,入门比较容易。一定要根据自身的知识结构建立科学的学习路径,只有学到一定程度、或者有了一定…

Function Mesh:流处理任务的 Serverless 化实践

文章摘要本文整理自 ApacheCon Asia 上,StreamNative 工程师付睿的分享《Use Apache Pulsar Functions in a Cloud-Native way》。本文将介绍在云原生环境中使用 Pulsar Functions 的实践,以及基于 Pulsar Functions 和 Kubernetes 的项目 Function Mesh…

Redis实现高可用

怎么实现Redis的高可用?我们在项目中使用Redis,肯定不会是单点部署Redis服务的。因为,单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上&#xff0c…

tensorflow2.4--1.框架介绍

前言 虽然1.x版本tensorflow有很多项目都基于此构建,然而随着2.x版本的推出,很多架构已经发生了改变,代码发生了改变,同时很多模组已经废弃不用或者更新,tensorflow1.x已经不能再兼容最新的项目,与时俱进是必要的,因此…

【图像处理】数字图像处理基础(分辨率,像素,显示...)

Table of Contents1.数字图像处理基础1.1 图像表示1.1.1 图像成像模型1.1.2 数字图像的表示a.图像采样b.图像灰度的量化c.算比特数1.2 分辨率1.2.1 空间分辨率1.2.2 灰度分辨率1.3 像素间的关系1.3.1 像素邻域a.4邻域b.4对角邻域c.8邻域1.3.2 像素邻接1.3.3 像素连通1.3.4 像素…

【C语言航路】第十五站:程序环境和预处理

目录 一、程序的翻译环境和执行环境 二、编译和链接 1.翻译环境 2.编译本身也分为几个阶段 3.运行环境 三、预处理 1.预定义符号 2.#define 1.#define定义标识符 2.#define定义宏 3.#define 替换规则 4.#和## 5.带副作用的宏参数 6.宏和函数的对比 7.命名约定 …

Android Qcom Display学习(十二)

该系列文章总目录链接与各部分简介: Android Qcom Display学习(零) 本章主要是基于高通平台上dump出GPU渲染 or GPU合成 or HWC合成的GraphicBuffer的数据。 起初是在B站上看到这么一个视频,能dump出每个Layer的数据显示系统原理以及图形系统调试&#…

Delphi 中 FireDAC 数据库连接(设置选项)

描述了为什么选项集使FireDAC成为一个灵活的数据库框架以及如何使用这些选项。FireDAC提供了大量的选项,这些选项被组织成一个分层的选项系统,大多数选项可以保留其默认值。一、议题TopicDescription数据类型映射FireDAC提供了一个灵活的可调整的数据类型映射系统&a…

command-line变成-bash-4.2

故障描述: 故障诊断: 分析用户创建的过程: Useradd jfedu1命令默认创建用户jfedu1,会根据如下步骤进行操作: 读取/etc/default/useradd,根据配置文件执行创建操作; 在/etc/passwd文件中添加…

Python 之 Pandas merge() 函数、set_index() 函数、drop_duplicates() 函数和 tolist() 函数

文章目录一、merge() 函数1. inner2. left 和 right3. outer二、set_index() 函数三、drop_duplicates() 函数四、tolist() 函数五、视频数据分析案例1. 问题要求2. 解决过程在最开始,我们先导入常规的 numpy 和 pandas 库。 import numpy as np import pandas as …

【数据挖掘】2、数据预处理

文章目录一、数据预处理的意义1.1 缺失数据1.1.1 原因1.1.2 方案1.1.3 离群点分析1.2 重复数据1.2.1 原因1.2.2 去重的方案1.3 数据转换1.4 数据描述二、数据预处理方法2.1 特征选择 Feature Selection2.2 特征提取 Feature Extraction2.2.1 PCA 主成分分析2.2.2 LDA 线性判别分…

四维地球2.0上线,中国四维遥感云平台布局初见端倪

‍数据智能产业创新服务媒体——聚焦数智 改变商业近日,土耳其大地震一直备受全球各国人民的关注,为了在黄金72小时内帮助解救受困人员,包括中国在内的不少国家纷纷向土耳其政府和人民伸出援手,除了派出专业的救援队伍之外&#…

数据结构前提知识

数据结构数据结构 个体的存储个体关系的存储算法对存储数据的操作程序数据结构算法衡量算法的标准时间复杂度:注意不是程序执行的时间,因为一个程序执行的时间取决于软硬件环境,不同的机器,执行的速度不一样,配置好的…