JavaScript中栈和堆,值传递和引用传递
基本数据类型和引用数据类型
在讲什么是栈和堆之前我们首先要知道为什么会有这种东西,我们都知道JavaScript
中的数据类型有两种,基本数据类型和引用数据类型。这两种数据有什么区别呢,最大的区别就是基本数据类型是存在栈中,引用类型存在堆中。下面有关于栈和堆的详细讲解
栈和堆
在js引擎中对变量的存储主要有两种位置,堆内存和栈内存。
栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,**以及对象变量的指针,这时候栈内存给人的感觉就像一个线性排列的空间,每个小单元大小基本相等。而且大小不会发生变化。堆内存实际是一个树状结构,并且大小是未知的。
而堆内存主要负责像对象Object这种变量类型的存储,如下图:
基本数据类型的值是直接存储在栈内存中的,而引用数据类型的值是存在堆中,栈中存的实际上是一个指向堆内存的指针,我们是通过这个指针来获取我们需要的值的。知道了这个之后我们就可以引出很多有意思的东西。
1. const定义一个变量是不是不可变的
我们知道const
的作用就是定义一个常量,但是如果我们定义的是一个对象的话,对象的值其实是可以改变的。
这是为什么呢???其实我们定义const
定义的确实是常量。但是当我们定义一个const
对象的时候,我们说的常量其实是指针,就是const
对象对应的堆内存指向是不变的,但是堆内存中的数据本身的大小或者属性是可变的。而对于const
定义的基础变量而言,这个值就相当于const
对象的指针,是不可变。
既然知道了const
在内存中的存储,那么const
、let定义的变量不能二次定义的流程也就比较容易猜出来了,每次使用const
或者let
去初始化一个变量的时候,会首先遍历当前的内存栈,看看有没有重名变量,有的话就返回错误。
再次测试,证实了我们的猜想,当我们尝试把一个常量指针指向另一块地址的时候报错了,这也符合我们const
的初衷
2. 数据赋值
考虑下面这两种情况:
情况一:
let a = 20;
let b = a;
b = 30;
console.log(a); // 此时a的值是多少,是30?还是20?
情况二:
let m = { a: 10, b: 20 };
let n = m;
n.a = 15;
console.log(m.a) //此时m.a的值是多少,是10?还是15?
答案:情况一输出 20, 情况二输出 15
分析:
在情况一中,a、b 都是基本类型,它们的值是存储在栈内存中的,当赋值的时候,b会重新存一份和a相同的值在栈中,a、b 分别有各自独立的栈空间, 所以修改了 b 的值以后,a 的值并不会发生变化。如图:
在情况二中,m、n都是引用类型,栈内存中存放地址指向堆内存中的对象, 引用类型的复制会为新的变量自动分配一个新的值保存在变量中, 但只是引用类型的一个地址指针而已,实际指向的是同一个对象, 所以修改 n.a 的值后,相应的 m.a 也就发生了改变。如图:
3. 值传递和引用传递
我们在使用函数的时候经常需要给函数传参,这个时候传递基本数据类型和引用数据类型会有不同,原因也是也为上面第二点数据赋值引起的。例如:
// 值传递
function addTen(num) {
num += 10;
return num;
}
var count = 20;
var result = addTen(count); //按值传递 num = count
alert(count); // 20, 没变化
alert(result); // 30
/*很好理解,因为是按值传递的,传递完后俩个变量各不相干!*/
// 引用传递
function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person); // obj = person
alert(person.name); // "Nicholas"
/* 以上代码中创建一个对象,并将其保存在变量person中。然后,这个变量被传递到setName(obj)函数中之后就被复制给了obj。在这个函数内部,obj和person引用的是同一个对象。于是,在函数内部为obj添加name属性后,函数外部的person也将有所反应;因为这时的person和obj指向同一个堆内存地址。 */
其实在《JavaScript高级程序设计》有这么一句话,ECMAScript中所有函数的参数都是按值传递的,传引用也是值传递,只不过传递的值是一个指向对象的指针,引入传引用这个说法只是为了帮助我们的理解。
4. 栈内存和堆内存的优缺点
在JS中,基本数据类型变量大小固定,并且操作简单容易,所以把它们放入栈中存储。 引用类型变量大小不固定,所以把它们分配给堆中,让他们申请空间的时候自己确定大小,这样把它们分开存储能够使得程序运行起来占用的内存最小。
栈内存的特点:存取速度快,系统效率较高,但不灵活,同时由于结构简单,在变量使用完成后就可以将其释放,内存回收容易实现。
堆内存需要分配空间和地址,还要把地址存到栈中,所以效率低于栈,采用的数据结构是树,结构复杂,垃圾回收需要自己控制,但是灵活性高。
栈内存和堆内存的垃圾回收
栈内存中变量一般在它的当前执行环境结束就会被销毁被垃圾回收制回收, 而堆内存中的变量则不会,因为不确定其他的地方是不是还有一些对它的引用。 堆内存中的变量只有在所有对它的引用都结束的时候才会被回收。所以为了防止内存泄漏导致的内存不足,我们在不需要使用某个对象的时候就要将他的引用去掉
let a = {name: 'hh', age: 13};
a = null; // 去掉对于这个的引用释放内存