首頁  >  文章  >  web前端  >  詳解Javascript函數聲明與遞歸調用

詳解Javascript函數聲明與遞歸調用

高洛峰
高洛峰原創
2016-12-09 13:21:521349瀏覽

Javascript的函數的聲明方式和調用方式已經是令人厭倦的老生常談了,但有些東西就是這樣的,你來說一遍然後我再說一遍。每次看到書上或部落格裡寫的Javascript函數有四種呼叫方式,我會想起孔乙己:麋字有四種寫法,你造嗎?

儘管缺陷有一堆,但Javascript還是令人著迷的。 Javascript眾多優美的特性的核心,是作為頂級物件(first-class objects)的函數。函數就像其他普通物件一樣被建立、被指派給變數、被傳遞為作為傳回值、持有屬性和方法。函數作為頂級對象,賦予了Javascript強大的函數式程式設計能力,也帶來了較不容易控制的靈活性。

1、函數宣告

變數式宣告先建立一個匿名函數,然後把它賦值給一個指定的變數:

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變數有一個的特別之處,就是會把變數的宣告提前,表達式

Javascript變數有一個的特別之處,就是會把變數的宣告提前,表達式的函數宣告,也會把整個函數的定義前置,因此你可以在函數定義之前使用它:

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

   

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

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


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

函數式聲明

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;");

   


這種方式其實是在全域作用域內產生匿名函數,並且將它賦值給變數

這種方式其實是在全域作用域內產生一個匿名函數,並且將它賦值給變數
這個變數。

2、遞歸調用

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

// 一个简单的阶乘函数
var f = function (x) {
  if (x === 1) {
    return 1;
  } else {
    return x * f(x - 1);
  }
};

   

函數名稱遇到困難,對於上面的變量式聲明,f是一個變量,所以它的值很容易被替換:

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

   


函數是個值,它被賦給fn,我們期待使用fn(

函數是個值,它被賦予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);
    }
  }
};

   


當然,它也會遭遇和功能被賦值給obj2的fac屬性後,內部仍要引用obj1.fac,於是…失敗了。


換一種方式會有所改進:

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

   


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


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

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

   


於是遞歸函數又不能正常工作了。


我們應該試著解決這個問題,還記得前面提到的一種函數宣告的方式嗎?

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构造函数创建的函数在每次被调用时,都会重新编译出一个函数,递归调用会引发性能问题——你会发现你的内存很快就被耗光了。


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn