JavaScript的内存问题

一直以来,对于Js的内存空间这部分的知识概念有些模糊,最近在回顾一些知识点的时候,特地的对js的内存这部分知识加深了一下理解,比如基本类型数据和引用类型数据在js内存中是怎么回事?什么是按值传递和按引用传递?以及对作用域和闭包的理解等等。

JavaScript的内存是怎样的?

JavaScript中有两种不同数据类型的值,一种是原始值,另外一种是引用类型值,原始值就是常说的基本数据类型值,包括String、Number、Boolean、Undefined和Null这五大基本数据类型,引用值指的是那些可能由多个指构成的对象,包括Object,Function,Array等类型的值。

JavaScript中的内存也分为栈内存和堆内存。一般来说,栈内存中存放的是存储对象的地址,而堆内存中存放的是存储对象的具体内容。对于原始类型的值而言,其地址和具体内容都存在与栈内存中;而基于引用类型的值,其地址存在栈内存,其具体内容存在堆内存中。堆内存与栈内存是有区别的,栈内存运行效率比堆内存高,空间相对堆内存来说较小,反之则是堆内存的特点。所以将构造简单的原始类型值放在栈内存中,将构造复杂的引用类型值放在堆中而不影响栈的效率。

我们看下下面Js的内存示意图:

内存示意图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    var a = 20;
var b = 'abc';
var c = true;
var d = { m: 20 }
```

可以看出变量a,b,c属于原始数据类型的变量,它们的值都存放在栈内存中,而d是一个对象即属于引用值,栈内存中存放的是它的内存地址,指向堆内存里面具体的一个值。

接着我们来看下面的一段代码:

```js
var a = {n:1};
var b = a;
a.x = a = {n:2};
console.log(a.x);// --> undefined
console.log(b.x);// --> [object Object]

一开始看这道题,然后看答案,一脸懵逼啊有木有???为啥结果输出的是undefined和[object Object]???

了解了Js的变量在内存的存储形式之后,我们一起来解释一下:
1、a是一个引用类型的变量,一开始它在栈内存中的地址是指向堆内存的具体内容{n:1},接着赋值给b,所以b和a一样,此时都指向对象{n:1};

1
2
var a = {n:1} ;
var b = {n:1} ;

2、接下来a.x = a = {n:2},我们都知道js的赋值运算是从右往左的,但“.”是优先级最高的运算符,所以这段代码先执行了a.x,所以此时对象{n:1}新增加了一个x的属性,并且值是undefined,所以运行到这里a和b都指向了对象{n:1,x:undefined};

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
  var a = {n:1,x:undefined} ;
var b = {n:1,x:undefined} ;
```

3、接着,依循“从右往左”的赋值运算顺序先执行 a={n:2} ,这时候,a指向的对象发生了改变,变成了新对象{n:2};而a.x = a则是对象{n:1,x:undefined}中的属性x指向了对象{n:2},所以此时指向的对象变成了{n:1,x:{n:2}}。

```js
var a = {n:2} ;
var b = {n:1,x:{n:2}} ;
```

综上所述,我们可以看到最后的运行结果,显然a.x是undefined,b.x是对象{n:2},用对象的字符串形式[object Object]表示。

## Js的内存空间管理

JavaScript的内存分配和回收是自动完成的,满足一定条件,就会被垃圾回收器自动回收,下面我们简单的了解下js的内存管理机制。

### JavaScript的内存生命周期:

1. 分配你所需要的内存
2. 使用分配到的内存(读、写)
3. 不需要时将其释放、归还

```js
var num = 10; // 在内存中给数值变量分配空间
alert(num); // 使用内存
num = null; // 使用完毕之后,释放内存空间

var obj = {v:1}; // 内存中存在{v:1}对象,及obj这个引用地址
obj = {value:2}; // 垃圾回收机制自动清理{v:1},并为新的有用到的{value:2}分配空间
```

### 垃圾回收算法

js垃圾回收有两种常见的算法:引用计数和标记清除。

#### 引用计数

引用计数就是跟踪对象被引用的次数,当一个对象的引用计数为0即没有其他对象引用它时,说明该对象已经无需访问了,因此就会回收其所占的内存,这样,当垃圾回收器下次运行就会释放引用数为0的对象所占用的内存。

> 但引用计数存在一个弊端就是循环引用问题(IE6和IE7就是采用此算法)。循环引用就是指对象A中包含一个指向对象B的引用,而对象B中也包含一个指向对象的引用。

```js
function problem() {
var A = {};
var B = {};
A.a = B;
B.a = A;
}
```

上面例子可以看出对象A和B存在循环音引用的问题,即两个的引用次数均为2,它们在运行之后依然存在,并且引用次数永远不为0,如果这个函数被多次调用,就有可能引起内存泄漏问题。为了解决循环引用的问题,还有一种方法就是可以实现垃圾回收,那就是标记清除法。

#### 标记清除

标记清除法是现代浏览器常用的一种垃圾收集方式,当变量进入环境(即在一个函数中声明一个变量)时,就将此变量标记为“进入环境”,进入环境的变量是不能被释放,因为只有执行流进入相应的环境,就可能会引用它们。而当变量离开环境时,就标记为“离开环境”。

垃圾收集器在运行时会给储存在内存中的所有变量加上标记,然后会去掉环境中的变量以及被环境中的变量引用的变量的标记,当执行完毕那些没有存在引用无法访问的变量就被加上标记,最后垃圾收集器完成清除工作,释放掉那些打上标记的变量所占的内存。

> 标记清除之所以不存在循环引用的问题,是因为当函数执行完毕之后,对象A和B就已经离开了所在的作用域,此时两个变量被标记为“离开环境”,等待被垃圾收集器回收,最后释放其内存。

### 管理内存

使用具备垃圾收集机制的语言编写程序,开发人员一般都不必担心内存管理的问题。但JavaScript在进行内存管理以及垃圾收集时面临的问题还是有些不同。出于安全方面的考虑,系统分配给浏览器的可用内存数量通常要比分配给桌面应用程序的少,防止JavaScript的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。
因此为了确保占用最少的内存可以让页面获取更好的性能。优化内存占用的最佳方式就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用,即解除引用。这一做法适用于大多全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用。(具体请阅读《JavaScript高级程序设计》第四章)

可以分析以下代码:

```js
function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson("Junga");
globalPerson = null;//手动解除全局变量的引用
```

在这个🌰中,变量globalPerson取得了createPerson()函数的返回的值。在createPerson()的内部创建了一个局部变量localPerson并添加了一个name属性。由于localPerson在函数执行完毕之后就离开执行环境,因此会自动解除引用,而对于全局变量来说则需要我们手动设置null,解除引用。

> 不过,解除一个值的引用并不意味着自动回收该值所占用的内存,解除引用真正的作用是让值脱离执行环境,以便垃圾收集器下次运行时将其收回。

## 内存优化

> 对于全局变量,JavaScript不能确定它在后面不能够被用到,所以它会从声明之后就一直存在于内存中,直至手动释放或者关闭页面/浏览器,这就导致了某些不必要的内存消耗。我们可以进行以下的优化:

### 立即执行函数的运用

```js
;(function(window, $, undefined) {
// 主业务代码
})(window, jQuery);

立即执行函数的作用就是建立一个独立的作用域,其一是为了防止全局污染,同时也可以防止过多的定义全局变量造成的内存回收问题。如果你的某些变量真的需要一直存在可以通过上面的方法挂载在window下。同样,你也可以传入jQuery进行使用。

手动解除变量的引用

1
2
var obj = {a:1,b:2,c:3};
obj = null;

使用回调

除了使用闭包进行内部变量访问,回调函数也有这个功能。

1
2
3
4
5
6
7
function getData(callback) {
var data = 'Junga';
callback(data);
}
getData(function(data) {
console.log(data);
});

回调函数是一种后续传递风格(Continuation Passing Style, CPS)的技术,这种风格的程序编写将函数的业务重点从返回值转移到回调函数中去。而且其相比闭包的好处也不少:

  1. 如果传入的参数是基础类型(如字符串、数值),回调函数中传入的形参就会是复制值,业务代码使用完毕以后,更容易被回收;
  2. 通过回调,我们除了可以完成同步的请求外,还可以用在异步编程中,这也就是现在非常流行的一种编写风格;
  3. 回调函数自身通常也是临时的匿名函数,一旦请求函数执行完毕,回调函数自身的引用就会被解除,自身也得到回收。