JavaScript 数据结构与算法之美 - 栈内存与堆内存 、浅拷贝与深拷贝-程序员宅基地

技术标签: ViewUI  前端  json  javascript  

JavaScript 数据结构与算法之美

前言

想写好前端,先练好内功。

栈内存与堆内存 、浅拷贝与深拷贝,可以说是前端程序员的内功,要知其然,知其所以然。

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

栈

定义

  1. 后进者先出,先进者后出,简称 后进先出(LIFO),这就是典型的结构。
  2. 新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底
  3. 在栈里,新元素都靠近栈顶,旧元素都接近栈底。
  4. 从栈的操作特性来看,是一种 操作受限的线性表,只允许在一端插入和删除数据。
  5. 不包含任何元素的栈称为空栈

栈也被用在编程语言的编译器和内存中保存变量、方法调用等,比如函数的调用栈。

定义

  • 堆数据结构是一种树状结构。 它的存取数据的方式,与书架与书非常相似。我们不关心书的放置顺序是怎样的,只需知道书的名字就可以取出我们想要的书了。 好比在 JSON 格式的数据中,我们存储的 key-value 是可以无序的,只要知道 key,就能取出这个 key 对应的 value。

堆与栈比较

  • 堆是动态分配内存,内存大小不一,也不会自动释放。
  • 栈是自动分配相对固定大小的内存空间,并由系统自动释放。
  • 栈,线性结构,后进先出,便于管理。
  • 堆,一个混沌,杂乱无章,方便存储和开辟内存空间。

栈内存与堆内存

JavaScript 中的变量分为基本类型和引用类型。

  • 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问,并由系统自动分配和自动释放。 这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。 JavaScript 中的 Boolean、Null、Undefined、Number、String、Symbol 都是基本类型。

  • 引用类型(如对象、数组、函数等)是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。 JavaScript 中的 Object、Array、Function、RegExp、Date 是引用类型。

结合实例说明

let a1 = 0; // 栈内存
let a2 = "this is string" // 栈内存
let a3 = null; // 栈内存
let b = { x: 10 }; // 变量 b 存在于栈中,{ x: 10 } 作为对象存在于堆中
let c = [1, 2, 3]; // 变量 c 存在于栈中,[1, 2, 3] 作为对象存在于堆中

栈/堆内存空间

当我们要访问堆内存中的引用数据类型时

    1. 从栈中获取该对象的地址引用
    1. 再从堆内存中取得我们需要的数据

基本类型发生复制

let a = 20;
let b = a;
b = 30;
console.log(a); // 20

基本类型发生复制过程

在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值,最后这些变量都是 相互独立,互不影响的

引用类型发生复制

let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5
  • 引用类型的复制,同样为新的变量 b 分配一个新的值,保存在栈内存中,不同的是,这个值仅仅是引用类型的一个地址指针。
  • 他们两个指向同一个值,也就是地址指针相同,在堆内存中访问到的具体对象实际上是同一个。
  • 因此改变 b.x 时,a.x 也发生了变化,这就是引用类型的特性。

结合下图理解

引用类型(浅拷贝)的复制过程

总结

栈内存 堆内存
存储基础数据类型 存储引用数据类型
按值访问 按引用访问
存储的值大小固定 存储的值大小不定,可动态调整
由系统自动分配内存空间 由代码进行指定分配
空间小,运行效率高 空间大,运行效率相对较低
先进后出,后进先出 无序存储,可根据引用直接获取

浅拷贝与深拷贝

上面讲的引用类型的复制就是浅拷贝,复制得到的访问地址都指向同一个内存空间。所以修改了其中一个的值,另外一个也跟着改变了。

深拷贝:复制得到的访问地址指向不同的内存空间,互不相干。所以修改其中一个值,另外一个不会改变。

平时使用数组复制时,我们大多数会使用 =,这只是浅拷贝,存在很多问题。比如:

let arr = [1,2,3,4,5];
let arr2 = arr;
console.log(arr) //[1, 2, 3, 4, 5]
console.log(arr2) //[1, 2, 3, 4, 5]
arr[0] = 6;
console.log(arr) //[6, 2, 3, 4, 5]
console.log(arr2) //[6, 2, 3, 4, 5]
arr2[4] = 7;
console.log(arr) //[6, 2, 3, 4, 7]
console.log(arr2) //[6, 2, 3, 4, 7]

很明显,浅拷贝下,拷贝和被拷贝的数组会相互受到影响。

所以,必须要有一种不受影响的方法,那就是深拷贝。

深拷贝的的复制过程

let a = { x: 10, y: 20 }
let b = JSON.parse(JSON.stringify(a));
b.x = 5;
console.log(a.x); // 10
console.log(b.x); // 5

复制前

复制后

b.x 修改为 5 后

数组

一、for 循环

//for 循环 copy
function copy(arr) {
    let cArr = []
    for(let i = 0; i < arr.length; i++){
      cArr.push(arr[i])
    }
    return cArr;
}
let arr3 = [1,2,3,4];
let arr4 = copy(arr3) //[1,2,3,4]
console.log(arr4) //[1,2,3,4]
arr3[0] = 5;
console.log(arr3) //[5,2,3,4]
console.log(arr4) //[1,2,3,4]

二、slice 方法

//slice实现深拷贝
let arr5 = [1,2,3,4];
let arr6 = arr5.slice(0);
arr5[0] = 5;
console.log(arr5); //[5,2,3,4]
console.log(arr6); //[1,2,3,4]

三、concat 方法

//concat实现深拷贝
let arr7 = [1,2,3,4];
let arr8 = arr7.concat();
arr7[0] = 5;
console.log(arr7); //[5,2,3,4]
console.log(arr8); //[1,2,3,4]

四、es6 扩展运算

//es6 扩展运算实现深拷贝
let arr9 = [1,2,3,4];
let [...arr10] = arr9;
arr9[0] = 5;
console.log(arr9) //[5,2,3,4]
console.log(arr10) //[1,2,3,4]

五、JSON.parse 与 JSON.stringify

let arr9 = [1,2,3,4];
let arr10 = JSON.parse(JSON.stringify(arr9))
arr9[0] = 5;
console.log(arr9) //[5,2,3,4]
console.log(arr10) //[1,2,3,4]

注意:该方法在数据量比较大时,会有性能问题。

对象

一、对象的循环

//  循环 copy 对象
let obj = {
    id:'0',
    name:'king',
    sex:'man'
}
let obj2 = copy2(obj)
function copy2(obj) {
    let cObj = {};
    for(var key in obj){
      cObj[key] = obj[key]
    }
    return cObj
}
obj2.name = "king2"
console.log(obj) // {id: "0", name: "king", sex: "man"}
console.log(obj2) // {id: "0", name: "king2", sex: "man"}

二、JSON.parse 与 JSON.stringify

var obj1 = {
    x: 1, 
    y: {
        m: 1
    },
    a:undefined,
    b:function(a,b){
      return a+b
    },
    c:Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ƒ, c: Symbol(foo)}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ƒ, c: Symbol(foo)}
console.log(obj2) //{x: 1, y: {m: 2}}

可实现多维对象的深拷贝。

注意:进行JSON.stringify() 序列化的过程中,undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

三、es6 扩展运算

let obj = {
    id:'0',
    name:'king',
    sex:'man'
}
let {...obj4} = obj
obj4.name = "king4"
console.log(obj) //{id: "0", name: "king", sex: "man"}
console.log(obj4) //{id: "0", name: "king4", sex: "man"}

四、Object.assign()

Object.assign() 只能实现一维对象的深拷贝。

var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) // {x: 1, y: 2}
console.log(obj2) // {x: 1, y: 2}

obj2.x = 2; // 修改 obj2.x
console.log(obj1) // {x: 1, y: 2}
console.log(obj2) // {x: 2, y: 2}

var obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) // {x: 1, y: {m: 1}}
console.log(obj2) // {x: 1, y: {m: 1}}

obj2.y.m = 2; // 修改 obj2.y.m
console.log(obj1) // {x: 1, y: {m: 2}}
console.log(obj2) // {x: 2, y: {m: 2}}
通用深拷贝方法

简单版

let clone = function (v) {
    let o = v.constructor === Array ? [] : {};
    for(var i in v){
      o[i] = typeof v[i] === "object" ? clone(v[i]) : v[i];
    }
    return o;
}
// 测试
let obj = {
    id:'0',
    name:'king',
    sex:'man'
}
let obj2 = clone(obj)
obj2.name = "king2"
console.log(obj) // {id: "0", name: "king", sex: "man"}
console.log(obj2) // {id: "0", name: "king2", sex: "man"}

let arr3 = [1,2,3,4];
let arr4 = clone(arr3) // [1,2,3,4]
arr3[0] = 5;
console.log(arr3) // [5,2,3,4]
console.log(arr4) // [1,2,3,4]

但上面的深拷贝方法遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈,所以要避免。

let obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;
let obj2 = clone(obj1);
console.log(obj2) 

结果如下:

爆栈

总结:深刻理解 javascript 的深浅拷贝,可以灵活的运用数组与对象,并且可以避免很多 bug。

7. 最后

文中所有的代码及测试事例都已经放到我的 GitHub 上了。

如果你觉得有用或者喜欢,就点收藏,顺便点个赞吧,你的支持是我最大的鼓励 !

参考文章:

JavaScript栈内存和堆内存 JavaScript实现浅拷贝与深拷贝的方法分析 浅拷贝与深拷贝(JavaScript)

转载于:https://my.oschina.net/u/3978399/blog/3100138

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/chentang6196/article/details/100791968

智能推荐

node.js+Vue计算机毕设项目勤工助学管理系统(程序+LW+部署)_基于vue node勤工助学管理系统-程序员宅基地

文章浏览阅读150次。该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流项目运行环境配置:项目技术:Express框架 + Node.js+ Vue 等等组成,B/S模式 +Vscode管理+前后端分离等等。环境需要1.运行环境:最好是Nodejs最新版,我们在这个版本上开发的。其他版本理论上也可以。2.开发环境:Vscode或HbuilderX都可以。推荐HbuilderX;3.mysql环境:建议是用5.7版本均可。_基于vue node勤工助学管理系统

【深度学习项目】基于FER-2013数据集的人脸表情识别代码实现-程序员宅基地

文章浏览阅读369次,点赞6次,收藏6次。本人的深度学习课程大作业。基于FER-2013数据集的人脸表情识别代码实现。3万多字的文字说明,从零开始训练自己的模型。包含FER-2013数据集下载资源、完整可运行代码文件、训练好的模型文件。

python 括号 垃圾_Python 为什么抛弃累赘的花括号,使用缩进来划分代码块?-程序员宅基地

文章浏览阅读199次。大家好,这是“Python为什么”系列节目的文字稿(文末有观看地址)。本期话题:Python 为什么使用缩进来划分代码块,而不像其它语言使用花括号 {} 或者 “end” 之类的语法?Python 的缩进是一个老生常谈的话题,经常有人会提及它,比如 Python 之父在上个月就恰好转发过一篇文章:image因为这篇文章,Guido 还受邀在 Python Bytes 播客上录制了一期节目:imag..._python为什么抛弃大括号

国王给骑士金币c 语言编程,noip2015年全国青少年信息学奥林匹克竞赛决赛(普及组)01 金币...-程序员宅基地

文章浏览阅读1.2k次。【noip2015年全国青少年信息学奥林匹克竞赛决赛(普及组)】01:金币【题目描述】国王将金币作为工资,发放给忠诚的骑士。第一天,骑士收到一枚金币;之后两天(第二天和第三天),每天收到两枚金币;之后三天(第四、五、六天),每天收到三枚金币;之后四天(第七、八、九、十天),每天收到四枚金币……;这种工资发放模式会一直这样延续下去:当连续N天每天收到N枚金币后,骑士会在之后的连续N+1天里,每天收到...

Python之ruamel.yaml模块详解(二)-程序员宅基地

文章浏览阅读733次。【代码】Python之ruamel.yaml模块详解(二)_ruamel.yaml

用Arduino玩转掌控板(ESP32):B站粉丝计数器-程序员宅基地

文章浏览阅读608次,点赞2次,收藏11次。众所周知,掌控板在创客教育中用的非常广泛,它是一块基于 ESP32 的学习开发板。大家对掌控板编程,用的比较多的都是图形化编程的方式,比如 mPython、Mind+ 等。但是,既然掌控板..._esp32b站粉丝数

随便推点

matlab输出二进制,Matlab:如何将实数表示为二进制-程序员宅基地

文章浏览阅读1.4k次。你当然可以在实数空间中计算它,但是你有可能遇到精度问题(取决于起点).如果您对研究轨道感兴趣,您可能更愿意使用合理的分数表示法.有更有效的方法可以做到这一点,但下面的代码说明了一种计算从该映射派生的系列的方法.您将在Link 2的第2页上看到period-n定义.您应该能够从此代码中看到如何在实数空间中轻松地工作作为替代(在这种情况下,matlab函数鼠将恢复从你的真实数字的理性近似).[编辑]现..._matlab将输出样本变为二进制的方法

vivado时序约束_vivado logic level-程序员宅基地

文章浏览阅读2.7k次,点赞5次,收藏43次。vivado时序约束_vivado logic level

点云从入门到精通技术详解100篇-基于三维点云的路况语义分割(续)-程序员宅基地

文章浏览阅读123次。提升了点云数据的处理速度,但其对于语义信息的利用方面尚有改进的空间。机采样的方法来大大降低点云场景的采样密度,同时应用局部特征聚合器来保留突出。距离前期所有采样点距离最远的新采样点,从而逐步形成对整个点集的采样覆盖。最远点采样过程中,需要不断地计算新采样点和已采样点集之间的距离,因此计算复。杂度比较高,速度相对比较慢,当采样场景中的点云数量较大时,时间成本成指数型。和最远点采样相比,其采样方法复杂度低,效率更高,因此采样。通常用于点云的采样方式有很多种,其中主要包括最远点采样、逆密度重要性采。

随机梯度下降(Stochastic Gradient Descent,SGD) 迭代优化算法原理、算法实现及应用_梯度下降算法 sgd对噪声敏感-程序员宅基地

文章浏览阅读1.4k次。概括地来说,随机梯度下降(Stochastic Gradient Descent,SGD)是一种迭代优化算法,用于最小化代价函数J(θ)。该算法在每次迭代时随机选择一个训练样本,并利用该样本对模型参数θ进行更新,然后重复这个过程多次。虽然每次迭代都能获得局部最优解,但是由于采用了随机梯度下降法,使得模型训练的效率很高,而且能够很好地克服局部最优解带来的挑战。但同时,这种方法也是有其缺点的。一般来说,当训练集较小时,随机梯度下降法易受到噪声的影响,可能会陷入局部最优解的漫长寻找中;_梯度下降算法 sgd对噪声敏感

计算机应用基础教师授课视频,微课在计算机应用基础教学的应用-程序员宅基地

文章浏览阅读217次。《微课在计算机应用基础教学的应用》由会员分享,可在线阅读,更多相关《微课在计算机应用基础教学的应用(8页珍藏版)》请在人人文库网上搜索。1、微课在计算机应用基础教学的应用摘要:随着我国社会经济的不断发展和进步,我国教育系统和体系也在日趋完善。传统的教学方法和模式已经不适于当代社会的需求。现代化和技术化和教学方式已经被广泛应用于教学中,这一显著特点在中职计算机教学中也有着直观的体现。微课是近年来新兴..._计算机基础与应用类课程微课(或教学辅助课件)

微信Android模块化架构重构实践_微信 架构优化 安卓-程序员宅基地

文章浏览阅读282次。微信Android架构历史微信Android诞生之初,用的是常见的分层结构设计。这种架构简单、清晰并一直沿袭至今。这是微信架构的v1.x时代。到了微信架构的v2.x时代,随着业务的快速发展,消息通知不及时和Android 2.3版本之前webview内存泄露问题开始突显。由于代码、内存、apk大小都在增长,对系统资源的占用越来越多,导致微信进程容易被系统回收。因此微信开始转向多进..._微信 架构优化 安卓

推荐文章

热门文章

相关标签