一、内存
1.内存是什么?
内存是计算机中重要的部件之一,它是外存与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。
2.内存的生命周期
无论是哪种编程语言,内存的生命周期都主要分为三个阶段:
- 分配内存:由操作系统来分配内存,供程序使用
- 使用内存:程序获得操作系统所分配的内存之后,在内存中发生读和写操作
- 释放内存:程序使用完内存之后,会将这部分内存释放出来供其他程序使用。在JavaScript中,有垃圾回收机制,会自动释放内存。
二、JavaScript中的内存分配
在前端开发日常工作中,几乎每天都在做着变量的声明和赋值,这些变量最终都会被存放到内存中,所以我们还是有必要了解一下在JavaScript中的内存分配方式。
1. JavaScript中的两种数据类型
JavaScript有两种数据类型:基本数据类型和引用数据类型
- 基本数据类型:number、string、boolean、undefined、null、symbol
- 引用数据类型:Array、Function、Object、Date、Math、RegExp 等
JavaScript中的三种数据结构
JavaScript中有三种数据结构:堆(heap),栈(stack)与队列(queue)。
程序运行的时候,需要内存空间存放数据。一般来说,系统会划分出两种不同的内存空间:一种叫做堆(heap),另一种叫做栈(stack)。JavaScript中并没有严格意义上区分栈内存与堆内存。
- 堆(heap)是没有结构的,多数时候被看作一种树状结构,数据可以任意存取,它是用于存放复杂数据类型(引用类型)的,例如数组对象、object对象等。
- 栈(stack)是有结构的,每个区块按照一定次序存取,其特点为先进后出,后进先出,栈中主要存放的是基本类型的变量的值以及指向堆中的数组或者对象的地址,存在栈中的数据大小与生存期必须是确定的。除此之外,还可以明确知道每个区块的大小,因此,stack的寻址速度要快于heap
- 队列是一种先进先出(FIFO)的数据结构。
JavaScript两种数据类型的变量声明和赋值
-
基本数据类型的变量声明与赋值
首先定义一个变量 num,为变量赋值为1。
let num = 1;
复制代码
当JavaScript引擎在执行到这行代码时,会执行如下操作:
- 为变量num创建一个唯一标识符(identifier),该标识符用于与栈内存中的地址A1形成映射关系。
- 在栈内存中为其分配一个地址A1。
- 将值1存储到分配的地址。
如图所示:
通常我们说num变量的值等于1,但其实严格意义上来讲,num变量的值等于栈内存中存放对应值的内存地址(如图中的A1)。接下来我们创建一个新的变量new_num并将num赋值给它:
let new_num = num;
复制代码
经过以上赋值之后,通常说new_num的值为1,同样从严格意义上来讲的话是指new_num和num指向同一个内存地址A1,如下图所示:
如果我们给num加1,会怎么样呢?
num = num + 1;
复制代码
由于 JS 中的基本数据类型是不可变的,当 num 值变为2时,JS 将在内存中分配一个新地址,将2作为其值存储,num将指向新地址。如下图所示
- 引用数据类型的变量声明和赋值
先定义一个数组 arr;
let arr = [1,2,3];
复制代码
当JavaScript引擎在执行到这行代码时,会执行如下操作:
- 为变量arr创建一个唯一标识符(identifier),该标识符用于与栈内存中的地址A3形成映射关系。
- 在栈内存中为其分配一个地址A3。
- 栈内存中存储在堆中分配的内存地址的值x023。
- 在堆中存储分配的值空数组[1,2,3]。
接下来我们创建一个新的数组,将arr赋值给它
let new_arr = arr;
复制代码
赋值原理与基本数据内存一致,为变量创建一个唯一的标识符,并与arr指向栈内存中的同一个地址A3。如图所示
接下来我们改变arr中第一项的值,会发生什么呢?
let arr[0]=5;
复制代码
我们修改了arr第一项的值,只在内存堆中,将x023这个地址下的标识符为0的指向值改变了。而标识符arr与new_arr指向的内存栈中的地址还是A3。对应堆中的引用地址仍是x023。因此,我们修改arr的值的同时,也修改了new_arr的值,当前arr与new_arr的值都变成了[5,2,3]。如图所示:
引用类型深浅拷贝原理
了解了内存存储数据的机制,那我们就能很容易地理解,js中的深浅拷贝是怎么回事了。
深拷贝与浅拷贝的区别是什么呢?
- 浅拷贝: 浅拷贝只复制了原对象中最外层的属性,也就是拷贝了其基本类型的数据,而对于对象中的引用类型数据而言,它仅复制了其引用,指向的地址还是原来的地址。
- 深拷贝: 深拷贝不会拷贝引用类型的引用,而是层层拷贝,直到指向值为基础数据类型。
这么看起来不太清晰,我们用图来理解下。
首先,我们先定义一个对象obj。
let obj = { a:{name:"anna",age:10},b:{name:"baby",age:40,work:{worker_age:10,company:"acb"}},id:0};
复制代码
首先,我们obj是指向栈中的A。对应堆中引用地址为0x001。然后再0x001的堆中,存储了a,b两个引用类型的数据,以及一个基础数据类型的数据id。他们的指向分别为下图所示:
由上流程图我们可以了解到,一个复杂的数据类型,在内存中是怎样存储的。那接下来我们来了解一下,深浅拷贝时,内存中是怎么存储这些数据的。
现在我们来浅拷贝一个数据
let copy_boj = {...obj};
复制代码
我们在堆中开辟一个新空间,将0x001的数据复制进来。当前数据指向如下图所示:
浅拷贝时,我们只对obj对象做了一次拷贝。现在obj中的数据与copy_obj中的数据,指向完全一致。
现在我们改变obj的id会怎么样呢?
obj.id = 1;
复制代码
此时,由于obj的id是一个基础数据类型,因此,obj的id对应的值变成了1。而copy_obj的id,对应的值仍是0。若有不理解的地方,再回头看看基本数据类型的变量声明与赋值。
那我们将a中的age修改为11,会怎么样呢?
obj.a.age = 11;
复制代码
此时,我们修改的是 0x111 中 age的值。由于,obj中的a与copy_obj中的a都指向 0x111。因此,当 0x111中的age被修改时。obj.a.age与copy_obj.a.age都被修改为了11。
2. let与const
- let: 用于定义变量。
- cosnt: 用于定义常量。
一般来说,我们应该尽可能多地使用const,只有当我们知道某个变量将发生改变时才使用let。 怎么来理解改变呢?
下面我们先来看看下面几种情况。
// 定义一个基本数据类型的常量,与一个引用类型的常量
const name = "bob", num_arr = [1,2,3];
复制代码
我们先尝试修改一下name的值,会怎么样呢?
name = "baby"; // Uncaught TypeError: invalid assignment to const 'name'
复制代码
提示错误,常量无法被修改。因为我们在给name赋一个新的值时,标识符name所指向的内存空间中的地址,会发生改变。
如果改变 new_arr 中某项数据的值,又会怎么样呢?
num_arr[0] = 4; // num_arr = [4,2,3]。
复制代码
可以看到,num_arr的数据被成功修改了。因为引用类型的数据,我们存储的是一个引用地址,虽然我们改变了num_arr这个数组中第一项的值。但是num_arr这个标识符指向的内存中的地址,没有发生改变。
如果我们直接给new_arr重新赋值,又会怎么样呢?
num_arr = [3,4,5]; // invalid assignment to const 'num_arr'
复制代码
现在我们给num_arr重新赋值了,现在num_arr指向的引用地址就不再是之前那个地址了。因此,无法修改成功了。
由此可看出,改变其实就是更改内存地址。let 允许更改内存地址。而const 则不允许更改内存地址。现在是不是更能明白const什么情况下,修改值会成功,什么时候会报错了呢?
3. null和undefined的本质区别
从内存来看 null 和 undefined 本质的区别是什么呢?
给一个全局变量赋值为null,相当于将这个变量的指针对象以及值清空,如果是给对象的属性赋值为null,或者局部变量赋值为null,相当于给这个属性分配了一块空的内存,然后值为null, JS会回收全局变量为null的对象。
给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果是给对象的属性赋值为undefined,说明这个值为空值
三、内存空间管理
JavaScript有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?就是找出那些不再继续使用的值,然后释放其占用的内存。
js中常用的垃圾回收机制有哪些呢?
-
标记清除法
- js中最常用的垃圾回收方式就是标记清除。当变量进入执行环境是,就标记这个变量为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。垃圾回收机制获取根并标记他们,访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用,如此层层标记,直到不可达。然后垃圾回收机制,会将不可达的进行删除,进入执行环境的不能进行删除
-
引用计数法
- 当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,垃圾回收机制清除该对象