栈内存、堆内存和预处理

浏览器是如何运行JavaScript代码的?

看例1:

1
2
3
4
5
6
var num = 12;
function fn() {
var num = 100;
}
fn();
num = 200;

用图解释这一行代码是如何执行的。

图例一

这是一个简单的图示,现在我们来增加几个概念:栈内存、堆内存和预处理。

栈内存

栈内存用来存放基本数据类型(Number、String、Boolean、Null和Undefined),在执行完之后销毁。

栈内存与另一个概念息息相关——作用域,即代码的执行环境。上图中左边的栈内存就是全局作用域,而右边的则是局部作用域。全局作用域在浏览器窗口关闭之后才销毁。局部作用域在执行完之后就会销毁。

JavaScript规定,父作用域不能使用子作用域中变量和方程,而反过来是可以的。这个反过来的方向链条则被称为作用域链。

这里需要注意的是,判断子作用域的父作用域是哪一个,要看这个子作用域是在哪里定义的,而不是在哪里执行。

堆内存

堆内存用来存放引用数据类型(object、array、function、date),在没有被引用之后销毁。

当我们声明和定义了一个引用数据类型之后,这个对象保存在堆内存中,而这个对象的地址则保存在栈内存中以用于引用。

在全局作用域声明和定义的引用数据类型,销毁的方法是手动赋值null。

看一组例子来说明栈内存与堆内存的区别:

例2

1
2
3
4
var a = 20;
var b = a;
b = 30;
// a等于多少?

例3

1
2
3
4
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
// m.a的值是分别是什么?

例2的图解

例3的图解

由上两个图解可见,当基本数据类型传递的时候,其实是复制了一个新的数据给另一个变量;而当引用类型传递的时候,复制的仅仅是引用数据类型的地址,两个变量通过地址指向的是同一个堆内存中的数据。

所以在例3中,当我们改变n.a的时候,m.a也同样改变了。

预处理

预处理是浏览器在执行代码前要做的任务,它包括变量的声明和函数声明与定义。

预处理是变量提升的原因。

当我们写了var num = 12这样的一行代码的时候,在执行时其实是分为两步:声明var num和定义num = 12。对于变量,预处理只做声明而不做定义。

而相对于函数function fn(){var num 12},同样有声明和定义之分,与变量不同的是,预处理时声明和定义全部执行。具体步骤是:声明function fn(),定义fn() = "{var num = 12}"

理解了栈内存、堆内存和预处理之后,重新画出例1的图示:

这里的堆内存xxxfff000被全局作用域的函数fn引用,而全局作用域只有在浏览器窗口关闭的时候才会销毁,所以,只要浏览器窗口没有关闭,则堆内存xxxfff000一直被引用而不会销毁。

参考资料:

JavaScript高级程序设计(第三版);