前言
想写好前端先练好内功。栈内存与堆内存 、浅拷贝与深拷贝可以说是前端程序员的内功要知其然知其所以然。
笔者写的 Javascript 数据结构与算法之美 系列用的语言是 Javascript 旨在入门数据结构与算法和方便以后复习。
栈
定义
栈也被用在编程语言的编译器和内存中保存变量、方法调用等比如函数的调用栈。
堆
定义
- 堆数据结构是一种树状结构。 它的存取数据的方式与书架与书非常相似。我们不关心书的放置顺序是怎样的只需知道书的名字就可以取出我们想要的书了。 好比在 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] 作为对象存在于堆中
当我们要访问堆内存中的引用数据类型时
基本类型发生复制
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); // 10console.log(b.x); // 5
数组
一、for 循环
//for 循环 copyfunction copy(arr) {let cArr []for(let i 0; i }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 ab},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.mconsole.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} objobj4.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.xconsole.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.mconsole.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。
文章输出计划
Javascript 数据结构与算法之美 的系列文章坚持 3 - 7 天左右更新一篇暂定计划如下表。
标题链接时间和空间复杂度https://github.com/biaochenxuying/blog/issues/29
线性表数组、链表、栈、队列https://github.com/biaochenxuying/blog/issues/34
实现一个前端路由如何实现浏览器的前进与后退 https://github.com/biaochenxuying/blog/issues/30
栈内存与堆内存 、浅拷贝与深拷贝https://github.com/biaochenxuying/blog/issues/35
非线性表树、堆
递归
冒泡排序
插入排序
选择排序
归并排序
快速排序
计数排序
基数排序
桶排序
希尔排序
堆排序
十大经典排序汇总
如果有错误或者不严谨的地方请务必给予指正十分感谢。7. 最后
文章中的代码已经全部放在了我的 github 上如果喜欢或者有所启发欢迎 star对作者也是一种鼓励。
关注我的公众号第一时间接收最新的精彩博文。
参考文章
Javascript栈内存和堆内存
Javascript实现浅拷贝与深拷贝的方法分析
浅拷贝与深拷贝Javascript