JavaScript内存和拷贝

本文最后更新于 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();

栈堆空间示意图(来自掘金@三分钟学前端An)

优缺点

基本数据类型变量大小固定,且操作简单容易,所以把它们放入栈中存储。引用数据类型变量大小不固定,所以把它们分配给堆中,让它们申请空间的时候自己确定大小,这样把它们分开存储能够使得程序运行起来占用的内存最小。

栈内存由于它的特点,所以它的系统效率较高。
堆内存需要分配空间和地址,还要把地址存到栈中,所以效率低于栈。

为何区分

区分栈内存与堆内存与垃圾回收机制有关,为了使程序运行时占用的内存最小。

执行方法时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里。方法执行结束后,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的。

我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法结束而销毁,即使方法执行结束,这个对象还可能被另一个引用变量所引用,这个对象依然不会被销毁,只有当一个对象没有被任何引用变量引用时,系统的垃圾回收机制才会在核实的时候回收它。

闭包

闭包中的变量并不保存在栈内存中,而是保存在堆内存中。这也就解释了为什么创建闭包函数的上下文已经被销毁,而闭包函数还能引用那个上下文的变量。

闭包中的变量没有保存在栈内存中,而是保存在堆内存中:

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
let num = 1; // 创建局部变量 num 和局部函数 bar
function bar() {
// bar() 是函数内部方法,是一个闭包
num++;
console.log(num); // 使用了外部函数声明的变量,内部函数可以访问外部函数的变量
}
return bar; // bar 被外部函数作为返回值返回了,返回的是一个闭包
}
let test = foo();
test(); // 2
test(); // 3
console.dir(test); // 结果看下图,闭包中的变量没有保存在栈内存中,而是保存在堆内存中:

来自掘金@三分钟学前端An

即使闭包没有被返回,闭包中的变量还是保存到了堆堆内存中:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
let num = 1; // 创建局部变量 num 和局部函数 bar
function bar() {
// bar() 是函数内部方法,是一个闭包
num++;
console.log(num); // 使用了外部函数声明的变量,内部函数可以访问外部函数的变量
}
bar(); // 2
bar(); // 3
console.dir(bar);
}
foo(); // 结果看下图,即使不返回函数(闭包没有被返回),闭包中的变量还是保存到了堆堆内存中:

来自掘金@三分钟学前端An

现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。所以 JS 引擎判断当前是一个闭包时,就会在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JS 无法访问)。

赋值和拷贝

基本数据类型

对于 基本数据类型 的赋值、深拷贝、浅拷贝,操作一样:在栈内存创建开辟地址。

引用数据类型

对于 引用数据类型 的赋值、深拷贝、浅拷贝:

当将一个对象赋值给一个新的对象的时候,赋的其实是该对象在栈中的地址(指针),而不是堆中的数据。也就是说,两个对象指向的是同一个堆内存,因而无论哪个对象发生改变,其实都是改变的堆内存的内容。因此,两个对象是联动的。

浅拷贝会在堆内存开辟地址,创建一个对象,然后遍历原对象。当原对象的属性值是基本数据类型,则拷贝基本数据类型的值;当原对象的属性值是引用数据类型,则拷贝的是引用数据类型在栈中的地址(指针)。

深拷贝也在堆内存开辟地址,创建一个对象,然后遍历原对象。当原对象的属性值是基本数据类型,则拷贝基本数据类型的值;当原对象的属性值是引用数据类型,则在堆内存开辟地址拷贝引用数据类型。

浅拷贝(来自掘金@ConardLi)
深拷贝(来自掘金@ConardLi)

赋值和浅拷贝的区别在于对象第一层数据对原对象的影响。
如果是赋值,改变都会直接影响原对象。
如果是浅拷贝,属性值是原始值,改变不会影响原对象;属性值是引用值,改变会影响原对象。
如果是深拷贝,属性值是原始值,改变不会影响原对象;属性值是引用值,改变也不会影响原对象。

赋值示例

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); // str1
console.log(arr2); // [0, 0, 0]
console.log(obj2); // { name: '浪里行舟', arr: [ 1, [2, 3], 4 ] }
// 修改值
str2 = "aaa";
arr2[0] = "bbb";
obj2.name = "阿浪";
obj2.arr[1] = [5, 6, 7];
// 结果
console.log(str1, str2);
// str1 aaa
console.log(arr1, arr2);
// ['bbb', 0, 0] ['bbb', 0, 0]
console.log(obj1, obj2);
// { name: '阿浪', arr: [ 1, [5, 6, 7], 4 ] }
// { name: '阿浪', arr: [ 1, [5, 6, 7], 4 ] }

浅拷贝示例

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); // { 0: 's', 1: 't', 2: 'r', 3: '1' }
console.log(arr2); // { 0: 0, 1: 0, 2: 0 }
console.log(obj2); // { name: '浪里行舟', arr: [ 1, [2, 3], 4 ] }
// 修改值
str2[0] = "aaa";
arr2[0] = "bbb";
obj2.name = "阿浪";
obj2.arr[1] = [5, 6, 7];
// 结果
console.log(str1, str2);
// str1 { 0: 'aaa', 1: 't', 2: 'r', 3: '1' }
console.log(arr1, arr2);
// [0, 0, 0] {0: 'bbb', 1: 0, 2: 0}
console.log(obj1, obj2);
// { name: '浪里行舟', arr: [ 1, [5, 6, 7], 4 ] }
// { name: '阿浪', arr: [ 1, [5, 6, 7], 4 ] }

深拷贝示例

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); // str1
console.log(arr2); // [0, 0, 0]
console.log(obj2); // { name: '浪里行舟', arr: [ 1, [2, 3], 4 ] }
// 修改值
str2 = "aaa";
arr2[0] = "bbb";
obj2.name = "阿浪";
obj2.arr[1] = [5, 6, 7];
// 结果
console.log(str1, str2);
// str1 aaa
console.log(arr1, arr2);
// [0, 0, 0] ['bbb', 0, 0]
console.log(obj1, obj2);
// { name: '浪里行舟', arr: [ 1, [2, 3], 4 ] }
// { name: '阿浪', arr: [ 1, [5, 6, 7], 4 ] }

引用数据类型总结

操作方式 和原数据指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

浅拷贝的实现

展开运算符

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); // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

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); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

Array.prototype.concat()

1
2
3
4
let arr1 = [1, 3, { username: "kobe" }];
let arr2 = arr1.concat();
arr2[2].username = "wade";
console.log(arr1); //[ 1, 3, { username: 'wade' } ]

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, 3, { username: 'wade' } ]

手写实现

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); // the objects are not the same (not same identity)
console.assert(clone.name === "MDN"); // they do have the same values
console.assert(clone.itself === clone); // and the circular reference is preserved

浏览器的 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)。此外的问题还有:

  • NaNInfinity变为null
  • SetMapRegExpDate等数据类型无法拷贝
  • functionsymbolundefined等属性会丢失
  • 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;
}
}

JavaScript内存和拷贝
https://xuekeven.github.io/2021/08/23/JavaScript内存和拷贝/
作者
Keven
发布于
2021年8月23日
更新于
2025年7月30日
许可协议