本文最后更新于 2025年7月30日 下午
参考:https://juejin.cn/post/6844903873992196110 。https://juejin.cn/post/6955962274917908488 。https://juejin.cn/post/6844904197595332622 。https://juejin.cn/post/6844903929705136141 。
栈堆内存 JavaScript 是动态语言,因为在声明变量之前并不需要确认其数据类型,所以 JavaScript 变量没有数据类型,值才有数据类型,变量可以随时持有任何类型的数据 。
JavaScript 内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈)。 其中,栈存放变量,堆存放复杂对象,池存放常量(也叫常量池)。
栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端称为栈顶。栈被称为是一种后入先出的数据结构。由于栈具有后入先出的特点,所以任何不在栈顶的元素都无法访问。为了得到栈底的元素,必须先拿掉上面的元素。
堆是一种经过排序的树形数据结构,每个结点都有一个值。通常,我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,我们只需要关心书的名字。
栈内存 JavaScript 的栈空间就是我们所说的调用栈(执行上下文栈),是用来存储执行上下文的。 其包含变量空间与词法环境,var、function 等保存在变量环境;let、const 声明的变量等保存在词法环境。
基本数据类型保存在栈内存中,因为基本数据类型占用空间小、大小固定。
所以,栈空间通常都不会设置太大,基本类型在内存中占有固定大小的空间,所以它们的值保存在栈空间,我们通过按值访问。它们也不需要手动管理,由操作系统来管理,函数调时创建,调用结束则消失。
堆内存 引用数据类型存储在堆内存中,因为引用数据类型占据空间大、大小不固定。如果存储在栈,将会影响程序运行的性能。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。
当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。因此,当我们需要访问堆内存中的引用数据类型时,实际上我们首先是从变量中获取了该对象的地址指针,然后再从堆内存中取得我们需要的数据。所以引用数据类型是。
所以,堆空间通常很大,能存放很多大的数据,我们通过按引用访问,不过缺点是分配内存和回收内存都会占用一定的时间。一般堆空间由程序员手动管理,如果程序结束还存在,则可能由操作系统回收。
代码示例:
1 2 3 4 5 6 7 var a = 1 ;function foo ( ) { var b = 2 ; var c = { name : "an" }; } foo();
优缺点 基本数据类型变量大小固定,且操作简单容易,所以把它们放入栈中存储。引用数据类型变量大小不固定,所以把它们分配给堆中,让它们申请空间的时候自己确定大小,这样把它们分开存储能够使得程序运行起来占用的内存最小。
栈内存由于它的特点,所以它的系统效率较高。 堆内存需要分配空间和地址,还要把地址存到栈中,所以效率低于栈。
为何区分 区分栈内存与堆内存与垃圾回收机制有关,为了使程序运行时占用的内存最小。
执行方法时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里。方法执行结束后,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的。
我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法结束而销毁,即使方法执行结束,这个对象还可能被另一个引用变量所引用,这个对象依然不会被销毁,只有当一个对象没有被任何引用变量引用时,系统的垃圾回收机制才会在核实的时候回收它。
闭包 闭包中的变量并不保存在栈内存中,而是保存在堆内存中。这也就解释了为什么创建闭包函数的上下文已经被销毁,而闭包函数还能引用那个上下文的变量。
闭包中的变量没有保存在栈内存中,而是保存在堆内存中:
1 2 3 4 5 6 7 8 9 10 11 12 13 function foo ( ) { let num = 1 ; function bar ( ) { num++; console .log(num); } return bar; }let test = foo(); test(); test(); console .dir(test);
即使闭包没有被返回,闭包中的变量还是保存到了堆堆内存中:
1 2 3 4 5 6 7 8 9 10 11 12 function foo ( ) { let num = 1 ; function bar ( ) { num++; console .log(num); } bar(); bar(); console .dir(bar); } foo();
现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。所以 JS 引擎判断当前是一个闭包时,就会在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JS 无法访问)。
赋值和拷贝 基本数据类型 对于 基本数据类型 的赋值、深拷贝、浅拷贝,操作一样:在栈内存创建开辟地址。
引用数据类型 对于 引用数据类型 的赋值、深拷贝、浅拷贝:
当将一个对象赋值给一个新的对象的时候,赋的其实是该对象在栈中的地址(指针),而不是堆中的数据。也就是说,两个对象指向的是同一个堆内存,因而无论哪个对象发生改变,其实都是改变的堆内存的内容。因此,两个对象是联动的。
浅拷贝会在堆内存开辟地址,创建一个对象,然后遍历原对象。当原对象的属性值是基本数据类型,则拷贝基本数据类型的值;当原对象的属性值是引用数据类型,则拷贝的是引用数据类型在栈中的地址(指针)。
深拷贝也在堆内存开辟地址,创建一个对象,然后遍历原对象。当原对象的属性值是基本数据类型,则拷贝基本数据类型的值;当原对象的属性值是引用数据类型,则在堆内存开辟地址拷贝引用数据类型。
赋值和浅拷贝的区别在于对象第一层数据对原对象的影响。 如果是赋值,改变都会直接影响原对象。 如果是浅拷贝,属性值是原始值,改变不会影响原对象;属性值是引用值,改变会影响原对象。 如果是深拷贝,属性值是原始值,改变不会影响原对象;属性值是引用值,改变也不会影响原对象。
赋值示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let str1 = "str1" ;let arr1 = [0 , 0 , 0 ];let obj1 = { name : "浪里行舟" , arr : [1 , [2 , 3 ], 4 ] };let str2 = str1;let arr2 = arr1;let obj2 = obj1;console .log(str2); console .log(arr2); console .log(obj2); str2 = "aaa" ; arr2[0 ] = "bbb" ; obj2.name = "阿浪" ; obj2.arr[1 ] = [5 , 6 , 7 ];console .log(str1, str2);console .log(arr1, arr2);console .log(obj1, obj2);
浅拷贝示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 let str1 = "str1" ;let arr1 = [0 , 0 , 0 ];let obj1 = { name : "浪里行舟" , arr : [1 , [2 , 3 ], 4 ] };function shallowClone (source ) { var target = {}; for (var i in source) { if (source.hasOwnProperty(i)) { target[i] = source[i]; } } return target; }let str2 = shallowClone(str1);let arr2 = shallowClone(arr1);let obj2 = shallowClone(obj1);console .log(str2); console .log(arr2); console .log(obj2); str2[0 ] = "aaa" ; arr2[0 ] = "bbb" ; obj2.name = "阿浪" ; obj2.arr[1 ] = [5 , 6 , 7 ];console .log(str1, str2);console .log(arr1, arr2);console .log(obj1, obj2);
深拷贝示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 let str1 = "str1" ;let arr1 = [0 , 0 , 0 ];let obj1 = { name : "浪里行舟" , arr : [1 , [2 , 3 ], 4 ] };function deepClone (obj ) { if (obj === null ) return obj; if (obj instanceof Date ) return new Date (obj); if (obj instanceof RegExp ) return new RegExp (obj); if (typeof obj !== "object" ) return obj; let cloneObj = new obj.constructor(); for (let key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key]); } } return cloneObj; }let str2 = deepClone(str1);let arr2 = deepClone(arr1);let obj2 = deepClone(obj1);console .log(str2); console .log(arr2); console .log(obj2); str2 = "aaa" ; arr2[0 ] = "bbb" ; obj2.name = "阿浪" ; obj2.arr[1 ] = [5 , 6 , 7 ];console .log(str1, str2);console .log(arr1, arr2);console .log(obj1, obj2);
引用数据类型总结
操作方式
和原数据指向同一对象
第一层数据为基本数据类型
原数据中包含子对象
赋值
是
改变会使原数据一同改变
改变会使原数据一同改变
浅拷贝
否
改变不会使原数据一同改变
改变会使原数据一同改变
深拷贝
否
改变不会使原数据一同改变
改变不会使原数据一同改变
浅拷贝的实现 展开运算符 1 2 3 4 5 let obj1 = { name : "Kobe" , address : { x : 100 , y : 100 } };let obj2 = { ...obj1 }; obj1.address.x = 200 ; obj1.name = "wade" ;console .log("obj2" , obj2);
Object.assign() 1 2 3 4 5 let obj1 = { person : { name : "kobe" , age : 41 }, sports : "basketball" };let obj2 = Object .assign({}, obj1); obj2.person.name = "wade" ; obj2.sports = "football" ;console .log(obj1);
Array.prototype.concat() 1 2 3 4 let arr1 = [1 , 3 , { username : "kobe" }];let arr2 = arr1.concat(); arr2[2 ].username = "wade" ;console .log(arr1);
Array.prototype.slice() 1 2 3 4 let arr1 = [1 , 3 , { username : "kobe" }];let arr3 = arr1.slice(); arr3[2 ].username = "wade" ;console .log(arr1);
手写实现 1 2 3 4 5 6 7 function clone (target ) { let cloneTarget = {}; for (const key in target) { cloneTarget[key] = target[key]; } return cloneTarget; }
深拷贝的实现 HTML API 1 2 3 4 5 const original = { name : "MDN" };const clone = structuredClone(original);console .assert(clone !== original); console .assert(clone.name === "MDN" ); console .assert(clone.itself === clone);
浏览器的 structuredClone() 缺点:
原型:无法拷贝对象的原型链
函数:无法拷贝函数
不可克隆:并没有支持所有类型的拷贝,比如 Error
JSON 1 2 3 4 let arr1 = [1 , 3 , { username : " kobe" }];let arr4 = JSON .parse(JSON .stringify(arr1)); arr4[2 ].username = "duncan" ;console .log(arr1, arr4);
这种方法虽然可以实现数组和对象深拷贝,但不能处理函数和正则,因为这两者基于 JSON.stringify() 和 JSON.parse() 处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为 null)。此外的问题还有:
NaN
、Infinity
变为null
Set
、Map
、RegExp
、Date
等数据类型无法拷贝
function
、symbol
、undefined
等属性会丢失
enumerable 为 false 的不可遍历属性丢失
手写实现 基础版本 1 2 3 4 5 6 7 8 9 10 11 function clone (target ) { if (typeof target === "object" ) { let cloneTarget = {}; for (const key in target) { cloneTarget[key] = clone(target[key]); } return cloneTarget; } else { return target; } }
考虑数组 1 2 3 4 5 6 7 8 9 10 11 function clone (target ) { if (typeof target === "object" ) { let cloneTarget = Array .isArray(target) ? [] : {}; for (const key in target) { cloneTarget[key] = clone(target[key]); } return cloneTarget; } else { return target; } }
循环引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function clone (target, map = new Map () ) { if (typeof target === "object" ) { let cloneTarget = Array .isArray(target) ? [] : {}; if (map.get(target)) { return map.get(target); } map.set(target, cloneTarget); for (const key in target) { cloneTarget[key] = clone(target[key], map); } return cloneTarget; } else { return target; } }
性能优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function clone (target, map = new WeakMap () ) { if (typeof target === "object" ) { const isArray = Array .isArray(target); let cloneTarget = isArray ? [] : {}; if (map.get(target)) { return map.get(target); } map.set(target, cloneTarget); const keys = isArray ? undefined : Object .keys(target); forEach(keys || target, (value, key ) => { if (keys) { key = value; } cloneTarget[key] = clone2(target[key], map); }); return cloneTarget; } else { return target; } }