深入理解递归和闭包

函数表达式的几种不同的语法形式

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 对象的引 用,顺利地减少其引用数,确保正常回收其占用的内存。