深入理解遞歸和閉包


函數表達式的幾種不同的語法形式

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 對象的引 用,順利地減少其引用數,確保正常回收其占用的內存。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM