下面创建一个经典 的 阶乘的递归函数:
arguments.callee() 是一个指向正在执行的函数的指针。
function factorial(num) { if (num <= 1 ) { return num; } else { return num * arguments.callee(num - 1 ); } } alert(factorial( 3 )); // 6
但是这样在严格模式下是不能用的,在严格模式下,如下调用:
var factorial = ( function f(num) { // 命名函数表达式 if (num <= 1 ) { return num; } else { return num * f(num - 1 ); } }); alert(factorial( 3 ));
闭包
闭包 是指 有权访问另一个函数作用域中的变量的函数。下面看个例子:
function compareAge(age) { return function (object1, object2) { var v1 = object1.age; var v2 = object2.age; if (v1 < v2) { return - 1 ; } else if (v1 > v2) { return 1 } else { return 0 ; } } }
变量v1,v2的赋值,调用了外部函数的参数。即使这个内部函数被反回了,而且是被其他地方调用了,
它仍然可以访问变量age, 之所以能够访问,是因为内部函数的作用域中包含外部函数的作用域。
当某个函数 第一次被调用,会创建一个 执行环境 及相应的作用域链,并把作用域链赋值给一个特殊的内部属性
[[Scope]] .然后,使用this,arguments和其他命名参数的值 来初始化函数的 活动对象 但在作用域链中,外部函数的活动对象
始终处于第二位,外部函数的外部函数处于第三位,一次类推。。。
在函数执行时,为读取和写入变量的值,就需要在 作用域链 中查找变量,看下面例子:
上述代码创建一个比较函数,之后再全局作用域中 调用它。
function compare(v1, v2) { if (v1 > v2) { return 1 ; } else { return 0 ; } } var result = compare( 1 , 2 ); // 0
第一次调用 compare()时,会创建一个包含 this,arguments,v1,v2的活动对象。
全局执行环境的变量对象(包含 this,result,compare) 在compare() 执行环境的作用域链中则处于第二位。关系如下图:
每个执行环境都有 一个 变量对象 ,全局环境的变量对象 始终存在.而像 compare()函数这样的 局部环境的变量对象 ,则只在
函数执行过程中存在.
那么在创建 compare()函数时,会创建一个 包含全局变量对象的作用域链, 这个作用域链 被保存在 [[scope]]属性内.
当调用 compare() 函数时,会为函数创建一个执行环境,然后通过复制 函数的 [[Scope]]属性中的对象 构建起执行环境的作用域链.
此后,又有一个活动对象 被创建,并推入执行环境作用域链的前端.
对于这个例子, compare()函数的执行环境而言,其作用域链中包含两个变量对象: 本地活动对象和全局变量对象.
显然,作用域链本质上 是一个指向变量对象的指针列表,它只引用但不实际包含变量对象.
无论什么时候在函数中访问一个变量时,就会从作用域中搜索具有相应名字的变量.一般来讲,当函数执行完毕后,局部活动 变量就会被销毁
内存中仅保存全局作用域.但是闭包 有所不同:
看下面函数:
// 闭包函数 function compare(propertyName) { return function (object1, object2) { var v1 = object1[propertyName]; var v2 = object2[propertyName]; if (v1 > v2) { return 1 ; } else if (v1 < v2) { return 2 ; } else { return 0 ; } } } var person1 = { name: " li " , age: 12 }; var person2 = { name: " ming " , age: 14 }; var f = compare( " age " ); var result = f(person1, person2); alert(result); // 2 alert(f); // 函数f的全部代码 // 普通函数 function asd() { document.getElementById( " asd " ); }; var s = asd(); alert(s); // undefined
在另一个函数内部定义的函数会将 包含函数(外部函数)的 活动对象添加到 它的作用域链中.
因此 compare()内部的构造函数 的作用域链将包含 compare()函数的活动对象.
在匿名函数从compare()中被返回后,它的作用域链就被初始化为 包含compare()函数的活动对象和全局变量对象.这样匿名函数就可
访问在 compare()中的所有变量. 更为重要的是,compare()函数在执行完毕后,其活动对象不会被销毁,因为匿名函数作用域仍然在引用这个活动对象.
换句话说,当compare()函数返回后,其执行环境的作用域链被销毁了,但他的活动对象 仍然保存在内存中,直到匿名函数被销毁,compare()函数的活动对象才被销毁.
看如下代码 (基于compare()函数的):
var person1 = { name: "li", age: 12 }; var person2 = { name: "ming", age: 14 }; var f = compare("age"); var result = f(person1, person2); alert(result); //2 f = null; alert(f); //undefined
上述代码 通过将f设置为 null,解除 该函数的引用,就等于通知垃圾回收历程,将其清除.随着匿名函数的作用域链被销毁,其他的作用域链也可以安全的销毁了(除了全局作用域).
下图展示了 f()的过程中产生的作用域链之间的关系:
由于闭包会携带包含它的函数的作用域,因此会比其他函数更占用内存.
闭包与变量
作用域链这种配置机制 引出了一个值得注意的副作用,就是 闭包只能取得包含函数中任何变量的最后一个值,
因为闭包所保存的一个变量对象,而不是某个特殊的变量,看下面的例子说明这个:
function a() { var array = new Array(); for (var i = 0; i < 6; i++) { array[i] = function () { return i; }; } return array; } var result; for (var j = 0; j < a().length; j++) { result += "\n" + "a()[" + j + "]:" + a()[j](); } alert(result);
result结果 如下图所示:
结果并没有和预想一样是 每个函数返回自己的索引.
因为每个函数的作用域中都保存着a()的活动对象,所以他们引用的都是同一个变量i,
当a()函数返回后,变量i的值为6, 此时每个函数都引用 着 保存变量i的同一个变量对象.所以每个函数内部i的值都是6.
但是可以通过 另一个匿名函数强制闭包的行为: 例子如下:
function a() { var array = new Array(); for (var i = 0; i < 6; i++) { array[i] = function (num) { return function () { return num; } }; } return array; } var result; for (var j = 0; j < a().length; j++) { result += "\n" + "a()[" + j + "]:" + a()[j](j)(); } alert(result);
结果如下图:
重写了a()函数,没有直接把闭包的复制给数组,而是定义了一个匿名函数.并将立即执行该匿名函数的结果赋值给数组.
这里的匿名函数有个参数num,也就是最终的函数要返回的值.在调用每个匿名函数时,我们传入了变量i,由于函数的参数是按值传递的,
所以就会将变量i的值复制给 参数num,而在这个匿名函数内部,有创建了并返回一个访问num的闭包.这样 array数组的每个函数都有自己的num变量的一个副本.
关于this对象
在闭包中使用this有可能 会导致一些问题.
this对象 是在运行时基于函数的执行环境绑定的. 在全局函数中, this等于window,而当被作为某个对象的方法调用时,this等于那个对象.
不过,匿名函数的执行环境具有全局性,因此其this 对象通常指向window, 看下面例子:
var name = "window"; var object = { name: "gao", getName: function () { return function () { return this.name; } } } alert(object.getName()());//非严格模式下 window
前面说过,每个函数被调用时,其活动对象都会自动取得两个特殊的变量,this和 arguments.内部函数在搜索这两个变量时,
只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量. 不过可以把外部作用域中的this对象保存在一个闭包
能够访问到的变量(that)里,就可以让闭包访问该对象(object)了:看下面例子:
var name = "window"; var object = { name: "gao", getName: function () { var that = this; return function () { return that.name; } } } alert(object.getName()()); //gao
提示:this和arguments 页存在同样的问题,如果想访问作用域中的arguments对象,必须将该对象的引用保存到另一个闭包可以访问的变量中.
内存泄露
如果闭包的作用域链中保存着一个html元素,那么就意味着该元素将无法销毁: 看下面例子:
(function () { var div = document.getElementsByTagName("div"); var className = div[0].className; if (div.length > 0) { div[0].onclick = function () { alert(className); }; } div = null; //解除对DOM对象的引用 })();
在上面代码中,通过把 div className存在一个变量中,并且在 闭包中引用该变量 消除了循环引用.但仅仅这么做还是不能避免内存泄露.
需要把 div变量设置为null,从而解除对DOM对象的引用,减少了其引用数,确保内存能够正常的回收.