Javascript函數聲明與遞歸調用

Javascript的函數的聲明方式和調用方式已是使人厭倦的老生常談了,但有些東西就是這樣的,你來講一遍而後我再說一遍。每次看到書上或博客裏寫的Javascript函數有四種調用方式,我就會想起孔乙己:茴字有四種寫法,你造嗎?程序員

儘管缺陷有一堆,但Javascript仍是使人着迷的。Javascript衆多優美的特性的核心,是做爲頂級對象(first-class objects)的函數。函數就像其餘普通對象同樣被建立、被分配給變量、做爲參數被傳遞、做爲返回值以及持有屬性和方法。函數做爲頂級對象,賦予了Javascript強大的函數式編程能力,也帶來了不太容易控制的靈活性。編程

一、函數聲明閉包

變量式聲明先建立一個匿名函數,而後把它賦值給一個指定的變量:app

var f = function () { // function body };

一般咱們沒必要關心等號右邊表達式的做用域是全局仍是某個閉包內,由於它只能經過等號左邊的變量f來引用,應該關注的是變量f的做用域。若是f指向函數的引用被破壞(f = null),且函數沒有被賦值給任何其它變量或對象屬性,匿名函數會由於失去全部引用而被垃圾回收機制銷燬。函數式編程

也可使用函數表達式建立函數:函數

function f() { // function body }

與變量式不一樣的是,這種聲明方式會爲函數的一個內置屬性name賦值。同時把函數賦值給當前做用域的一個同名變量。(函數的name屬性,configurable、enumerable和writable均爲false)性能

function f() { // function body }
console.log(f.name); // "f"
console.log(f); // f()

Javascript變量有一個的特別之處,就是會把變量的聲明提早,表達式式的函數聲明,也會把整個函數的定義前置,所以你能夠在函數定義以前使用它:this

console.log(f.name); // "f"
console.log(f); // f()
function f() { // function body }

函數表達式的聲明會被提高到做用域頂層,試試下面的代碼,它們不是本文的重點:spa

var a = 0;
console.log(a); // 0 or a()?
function a () {}

Crockford建議永遠使用第一種方式聲明函數,他認爲第二種方式放寬了函數必須先聲明後使用的要求從而會致使混亂。(Crockford是一個相似於羅素口中用來比喻維特根斯坦的"有良心的藝術家"那樣的"有良心的程序員",這句話很拗口吧)code

函數式聲明

function f() {}

看起來是

var f = function f(){};

的簡寫。

var a = function b(){};

的表達式,建立一個函數並把內置的name屬性賦值爲"b",而後把這個函數賦值給變量a,你能夠在外部使用a()來調用它,但卻不能使用b(),由於函數已被賦值給a,因此不會再自動建立一個變量b,除非你使用var b = a聲明一個變量b。固然這個函數的name是"b"而不是"a"。

使用Function構造函數也可用來建立函數:

var f = new Function("a,b,c","return a+b+c;");

這種方式實際上是在全局做用域內生成一個匿名函數,並把它賦值給變量f。

二、遞歸調用

遞歸被用來簡化許多問題,這須要在一個函數體中調用它本身:

// 一個簡單的階乘函數
var f = function (x) {
    if (x === 1) {
        return 1;
    } else {
        return x * f(x - 1);
    }
};

Javascript中函數的巨大靈活性,致使在遞歸時使用函數名遇到困難,對於上面的變量式聲明,f是一個變量,因此它的值很容易被替換:

var fn = f;
f = function () {};

函數是個值,它被賦給fn,咱們期待使用fn(5)能夠計算出一個數值,可是因爲函數內部依然引用的是變量f,因而它不能正常工做了。

函數式的聲明看起來好些,但很惋惜:

function f(x) {
    if (x === 1) {
        return 1;
    } else {
        return x * f(x - 1);
    }
}
var fn = f;
f = function () {}; // may been warning by browser
fn(5); // NaN

看起來,一旦咱們定義了一個遞歸函數,便須注意不要輕易改變變量的名字。

上面談論的都是函數式調用,函數還有其它調用方式,好比看成對象方法調用。

咱們經常這樣聲明對象:

var obj1 = {
    num : 5,
    fac : function (x) {
        // function body
    }
};

聲明一個匿名函數並把它賦值給對象的屬性(fac)。

若是咱們想要在這裏寫一個遞歸,就要引用屬性自己:

var obj1 = {
    num : 5,
    fac : function (x) {
        if (x === 1) {
            return 1;
        } else {
            return x * obj1.fac(x - 1);
        }
    }
};

固然,它也會遭遇和函數調用方式同樣的問題:

var obj2 = {fac: obj1.fac};
obj1 = {};
obj2.fac(5); // Sadness

方法被賦值給obj2的fac屬性後,內部依然要引用obj1.fac,因而…失敗了。

換一種方式會有所改進:

var obj1 = {
     num : 5,
     fac : function (x) {
        if (x === 1) {
            return 1;
        } else {
            return x * this.fac(x - 1);
        }
    }
};
var obj2 = {fac: obj1.fac};
obj1 = {};
obj2.fac(5); // ok

經過this關鍵字獲取函數執行時的context中的屬性,這樣執行obj2.fac時,函數內部便會引用obj2的fac屬性。

但是函數還能夠被任意修改context來調用,那就是萬能的call和apply:

obj3 = {};
obj1.fac.call(obj3, 5); // dead again

因而遞歸函數又不能正常工做了。

咱們應該試着解決這種問題,還記得前面提到的一種函數聲明的方式嗎?

var a = function b(){};

這種聲明方式叫作內聯函數(inline function),雖然在函數外沒有聲明變量b,可是在函數內部,是可使用b()來調用本身的,因而

var fn = function f(x) {
    // try if you write "var f = 0;" here
    if (x === 1) {
        return 1;
    } else {
        return x * f(x - 1);
    }
};
var fn2 = fn;
fn = null;
fn2(5); // OK

// here show the difference between "var f = function f() {}" and "function f() {}"
var f = function f(x) {
    if (x === 1) {
        return 1;
    } else {
        return x * f(x - 1);
    }
};
var fn2 = f;
f = null;
fn2(5); // OK

var obj1 = {
    num : 5,
    fac : function f(x) {
        if (x === 1) {
            return 1;
        } else {
            return x * f(x - 1);
        }
    }
};
var obj2 = {fac: obj1.fac};
obj1 = {};
obj2.fac(5); // ok

var obj3 = {};
obj1.fac.call(obj3, 5); // ok

就這樣,咱們有了一個能夠在內部使用的名字,而不用擔憂遞歸函數被賦值給誰以及以何種方式被調用。

Javascript函數內部的arguments對象,有一個callee屬性,指向的是函數自己。所以也可使用arguments.callee在內部調用函數:

function f(x) {
    if (x === 1) {
        return 1;
    } else {
        return x * arguments.callee(x - 1);
    }
}

但arguments.callee是一個已經準備被棄用的屬性,極可能會在將來的ECMAscript版本中消失,在ECMAscript 5中"use strict"時,不能使用arguments.callee。

最後一個建議是:若是要聲明一個遞歸函數,請慎用new Function這種方式,Function構造函數建立的函數在每次被調用時,都會從新編譯出一個函數,遞歸調用會引起性能問題——你會發現你的內存很快就被耗光了。

相關文章
相關標籤/搜索