深入理解递归和闭包
函数表达式的几种不同的语法形式
var functionName = function(arg0, arg1, arg2){ //函数体 };
这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量 functionName。这种情况下创建的函数叫做匿名函数(拉姆达函数),因为 function关键字后面没有标识符,
eg1 函数表达式与其他表达式一样,在使用前必须先赋值。以下代码会导致错误。 sayHi(); //错误:函数还不存在 var sayHi = function(){ alert("Hi!"); };
eg2 * : if(true){ function sayHi(){ alert("Hi!"); } } else { function sayHi(){ alert("Yo!"); }
}
表面上看,以上代码表示在为 true 时,使用一个 sayHi()的定义;否则,就使用另 一个定义,实际上,浏览器尝试修正错误的做法并不一致。如果是使用函数表达式,那就没有什么问题了。
var sayHi; if(true){ sayHi = function(){ alert("Hi!"); }; } else { sayHi = function(){ alert("Yo!"); }; }
递归函数
递归函数是在一个函数通过名字调用自身的情况下构成的
function factorial(num){ if (num <= 1){ return 1; } else { return num * factorial(num-1); } } factorial(4); //24
下面是一个经典的递归阶乘函数,先把 factorial()函数保存在变量 anotherFactorial 中,然后将 factorial 变量设 置为 null,factorial 已经不再是函数,就会导致错误,
function factorial(num){ if (num <= 1){ return 1; } else { return num * factorial(num-1); } } var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4));
为了防止调用途中factorial被改变,下面这个这个方式可以解决这个问题
eg: function factorial(num){ if (num <= 1){ return 1; } else { return num * arguments.callee(num-1); } } factorial(4); //24
eg2 function factorial(num){ if (num <= 1){ return 1; } else { return num * arguments.callee(num-1); } } var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4));
因此,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。 但在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误
eg 'use strict'; function factorial(num){ if (num <= 1){ return 1; } else { return num * arguments.callee(num-1); } } var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4));//出错
可以使用命名函数表达式来达成相同的结果
eg var factorial = (function f(num){ if (num <= 1){ return 1; }else { return num * f(num-1); } });
这种方式在严格模式和 非严格模式下都行得通。
闭包
官方解释:闭包是指有权访问另一个 函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数
白话解释:闭包是一种特殊的对象。 它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。 当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。
对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,突破闭包的瓶颈可以使你功力大增。
eg1: function createComparisonFunction(propertyName) { return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2){ return -1; } else if (value1 > value2){ return 1; } else { return 0; } }; }
这两行代码访问了外部 函数中的变量 propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可 以访问变量 propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含 createComparisonFunction()的作用域 ,而有关如何创建作用域链以及作用域链有什么作用的细节,对彻底 理解闭包至关重要
function foo() { var a = 20; var b = 30; function bar() { return a + b; } return bar; } var bar = foo(); bar();
上面的例子,首先有执行上下文foo,在foo中定义了函数bar,而通过对外返回bar的方式让bar得以执行。当bar执行时,访问了foo内部的变量a,b。因此这个时候闭包产生。
闭包与变量
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); } 输出的值 6 6 6 6 6
我们直接这样写,根据setTimeout定义的操作在函数调用栈清空之后才会执行的特点,for循环里定义了5个setTimeout操作。而当这些操作开始执行时,for循环的i值,已经先一步变成了6。因此输出结果总为6
利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
for (var i=1; i<=5; i++) { (function(i){ setTimeout( function timer() { console.log(i); }, i*1000 ); })(i) }
借助闭包的特性,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值即可。而我们知道在函数中闭包判定的准则,即执行时是否在内部定义的函数中访问了上层作用域的变量。因此我们需要包裹一层自执行函数为闭包的形成提供条件。 因此,我们只需要2个操作就可以完成题目需求,一是使用自执行函数提供闭包条件,二是传入i值并保存在闭包中。
闭包中的this对象
匿名函数的执行环境具有全局性,因此其this对象通常指向window。但有时候由于编写闭包的方式不同,这一点可能不会那么明显
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()()); //"The Window"(在非严格模式下)
由于 getNameFunc() 返回一个函数,因此调用 object.getNameFunc()()就会立即调用它返回的函数,结果就是返回一个字符串,为什么匿名函数没有取得其包含作用域(或外部作用域)的 this 对象呢?
每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量
内存泄漏
function assignHandler(){ var element = document.getElementById("someElement"); element.onclick = function(){ alert(element.id); }; }
由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此 就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所 占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决
function assignHandler(){ var element = document.getElementById("someElement"); var id = element.id; element.onclick = function(){ alert(id); }; element = null; }
把 element 变量设置为 null。这样就能够解除对 DOM 对象的引 用,顺利地减少其引用数,确保正常回收其占用的内存。