JavaScript作用域、闭包与内存管理

基本类型值

Undefined、Null、Boolean、Number和String,按值来保存。复制基本类型值时,会完全复制出一个独立的变量。

引用类型值(对象)

保存在内存中的对象,按引用访问。复制引用类型值时,副本其实是一个指针,指向堆内存中的对象,两者其实指向同一对象。

在函数传递参数时,参数都是按值传递的。当参数为对象时,虽然是值传递,但还是会按引用来访问同一个对象。内部可修改引用,但对外部将不会产生影响。

每个函数都有自己的执行环境(之前this文章里已经提过了,也就是执行上下文),当执行流进入一个函数时,函数环境就会被推入一个环境栈。函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。当代码在一个环境中执行时,会创建变量对象的一个作用域链,其可以保证对执行环境有权访问的所有变量和函数的有序访问。变量对象搜索的过程就是从前向后遍历作用域链的过程。

作用域链: 当前函数环境(argument…) -> 上级外部环境 -> 上上级外部环境


由于这样的机制,我们发现我们无法访问到函数内部的变量,因为作用域链总是向上的。因而产生了闭包,先看一下闭包的概念:

闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外.

方法一(返回函数):

1
2
3
4
5
6
7
8
9
function a() {
var b = 1;
return function() {
return b;
}
}
console.log(a()());

方法二(利用回调函数):

1
2
3
4
5
6
7
8
function c(callback){
var d = 2;
callback(d);
}
c(function(k){
console.log(k);
})

方法三(返回对象):

1
2
3
4
5
6
7
8
9
10
11
function e(){
var f = 10;
return {
get: function() {
return f;
}
}
}
console.log(e().get());

方法四(执行外部函数):

1
2
3
4
5
6
7
8
9
10
function a(){
var b = 3;
c(b);
}
function c(res){
console.log(res);
}
a();

方法五(在函数声明中不加var):

1
2
3
4
5
6
7
8
9
function a() {
var b = 6;
get = function(){
return b;
}
}
a();
console.log(get());

主要目的在于将函数内部的函数传递到其所在词法作用域之外,它都会持有对原始定义作用域的引用,通过它就可以访问函数内部的变量。一般函数在执行后其作用域就会被销毁,引擎使用垃圾回收器释放不再使用的内存空间。但由于闭包的存在,仍可以从外部访问到函数作用域内部,保持着这个引用,所以闭包也可能导致内存的泄露。

经典例子,这样会输出5个6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for(var i = 1; i <= 5; i++) {
// timer函数都共享对i的引用
setTimeout( function timer() {
// timer function 属于独立的一个函数作用域
// i变量需要沿着作用域链向上级环境寻找i值
console.log(i);
}, i * 1000);
}
for(var i = 1; i <= 5; i++) {
function test(){
// 1,2,3,4,5
console.log(i);
}
test();
}

这里我认为原因与setTimeOut也有一定的关系,setTimeout执行是浏览器开定时器线程,过setTimeout设置的时间后,向浏览器的事件队列插入执行的函数,js引擎线程执行完当前所有代码之后,开始执行事件队列里的代码,此时引用的i已经为6了。

正确的写法应该是利用一个IIFE(立即执行函数表达式):

1
2
3
4
5
6
7
for(var i = 1; i <= 5; i++) {
(function(i){
setTimeout( function timer() {
console.log(i);
}, i * 1000);
})(i);
}

另外,在ECMAScript中只采用静态作用域,闭包是一系列函数,并且静态保存所有父级的作用域。通过这些保存的作用域来搜寻到函数中的自由变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var x = 10;
function foo() {
console.log(x);
}
(function (funArg) {
var x = 20;
// 这里我们需要关注funArg,也就是foo是在哪里定义的,而不是在哪里调用的,因为js只有静态作用域
// 但要注意this,this却类似于动态作用域
// 10, 而不是20
funArg();
})(foo);

总之,闭包是代码块和创建该代码块的上下文中数据的结合,可以说所有的函数都是闭包,在它们定义时就已经保存了上层上下文的作用域链。


JavaScript自动分配内存,也由垃圾回收器自动回收,找出不再继续使用的变量,然后释放其占用内存,固定时间,周期执行操作。

标记清除

先会给存储在内存中的所有变量加上标记,去掉环境中的变量以及被环境中的变量引用的变量的标记。此后若再次被标记的变量将被清除,它们已经被认为是无法访问到的了,也就不需要了。

引用计数

值被引用一次,计数就加一,被赋予其它值,计数就减一,当计数为0时,说明无法被访问,那么就回收内存。但会发生循环引用,内存就无法回收,从而造成内存泄露。