栈(stack):主要存放的是基本类型的变量和对象的应用,其优势是存储速度比堆快,缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

堆(heap):用于引用类型(复杂数据类型:如数组对象、object对象)分配空间,运行时动态分配内存,存储速度较慢。

JavaScript中,内存分为三种类型:代码空间、栈空间、堆空间,其中代码空间用于存放可执行代码。

一、堆栈内存空间

1、栈内存空间

用栈作为数据结构在内存中所申请的空间。

2、栈的特点:

1、后进先出,最后添加进栈的元素最先出

2、访问栈底元素,必须拿掉它上面的元素

js7种基本数据类型变量保存在栈内存中,因为基本数据类型占用空间小、大小固定,通过值来访问,属于被频繁使用的数据。

3、闭包

闭包中的基本数据类型变量是保存在堆内存里的,当函数执行完弹出调用栈后,返回内部函数的一个应用,这时候函数的变量就会转移到堆上,因此内部函数依然能访问到上一层函数的变量。

二、堆内存空间

用堆作为数据结构在内存中所申请的空间。
通常情况下,我们所说的堆数据结构指的就是二叉堆。

1、二叉堆的特点:

1、它是一颗完全二叉树

2、二叉堆不是最小堆就是最大堆

2、引用数据类型

引用数据类型存储在堆内存中,引用数据类型占据空间大、大小不固定,如果存储在栈中,将影响程序的运行性能。
引用数据类型会在栈中存储一个指针,这个指针指向堆内存空间中该实体的起始地址。
当解释器寻找引用值时,会先检索其在栈中的地址,取得地址后,从堆中获得实体。

1
2
3
4
5
6
7
8
9
10
// 基本数据类型-栈内存
let name = "大白";
// 基本数据类型-栈内存
let age = 20;
// 基本数据类型-栈内存
let info = null;
// 对象指针存放在栈内存中,指针指向的对象放在堆内存中
let msgObj = {msg: "测试", id: 5};
// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中
let ages = [19, 22, 57]

上面代码中:

1
2
3
4
5
6
1、创建了两个变量msgObj、ages,它们的值都是引用类型(object、array)
2、堆内存空间采用二叉堆作为数据结构,msgObj与ages的具体值会存在堆内存空间中
3、存储完成后,堆内存空间会返回这两个值的引用地址(指针)
4、拿到引用地址后,这个引用地址会和它的变量名对应起来,存放在栈内存空间中
5、在查找变量msgObj与ages的具体值时,会先从栈内存空间中获取它的引用地址
6、获取到引用地址后,通过引用地址在堆内存空间的二叉堆中查找到对应的值。

堆内存空间中的object,表示的是存储在空间中的其他对象的引用值

3、栈内存空间与堆内存的区别

堆内存空间:相当于一个采用二叉堆作为数据结构的容器。
堆内存:指的是一个引用类型的具体值
堆内存存在于堆内存空间中

三、变量复制

1、基本数据类型的复制

下面代码中,name、alias都是基本类型,值存储在栈内存,分别有各自独立的栈空间,因此修改了alias的值,name是不受影响的。

1
2
3
4
5
6
let name ='jude'
let alias = name
alias = 'summer'

console.log(name) // jude
console.log(alias) // summer

相当于复制前是这样的:

name | jude

复制后:

alias | jude
name | jude

修改后:

alias | summer
name | jude

2、引用类型的复制

下面代码中,info、book都是引用类型,它们引用存在栈内存,值存在堆内存,它们的值指向同一块堆内存,栈内存中会复制一份相同的引用。

1
2
3
4
let book = {title:'book',id:1}
let info = book
info.title = 'javascript'
console.log(book.title) // javascript

四、深拷贝与浅拷贝

1、浅拷贝:

引用数据类型在复制时,改了其中一个数据的值,另一个数据的值也会跟着改变,这种拷贝方式我们称为浅拷贝。

1.1 Object.asign()

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' }

1.2 扩展运算符

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 } }

1.3 Array.prototype.concat()/Array.ptototype.slice()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]


let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

2、深拷贝:

引用类型复制到新的变量后,二者是独立的,不会因为一个的改变而影响到另一个
实际上就是重新在堆内存中开辟一块新的空间,把原对象的数据拷贝到这个新地址空间里来

2.1 深拷贝方法1:

将对象转一遍JSON,缺点是只能转化一般常见的数据,function、undefined、正则等类型无法通过这种方法变回来。

1
2
3
4
5
const data = { name: "jude" };
const obj = JSON.parse(JSON.stringify(data));
obj.age = 20;
console.log("data = ", data);// data = { name:"jude"}
console.log("obj = ", obj);// obj = {name:'jude',age:20}

2.2 深拷贝方法2:

手动去写循环遍历

1
2
3
4
5
const data = [{ name: "jude" }];
let obj = data.map(item => item);
obj.push({ name: "summer" });
console.log("data = ", data);// data = [{name:'jude'}]
console.log("obj = ", obj);// obj = [{name:'jude',name:'summer'}]