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變數有一個的特別之處,就是會把變數的聲明提前,運算式式的函式宣告,也會把整個函數的定義前置,因此你可以在函數定義之前使用它:
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是一個類似於羅素口中用來比喻維特根斯坦的"有良心的藝術家"那樣的"有良心的程式員",這句話很拗口吧)
函數式聲明
看起來是
的簡寫。而
的運算式,建立一個函數並把內建的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。
2、遞迴調用
遞迴被用來簡化許多問題,這需要在一個函數體中調用它自己:
// 一個簡單的階乘函數 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
於是遞迴函式又不能正常工作了。
我們應該試著解決這種問題,還記得前面提到的一種函式宣告的方式嗎?
這種聲明方式叫做內嵌函式(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建構函式建立的函數在每次被調用時,都會重新編譯出一個函數,遞迴調用會引發效能問題——你會發現你的記憶體很快就被耗光了。
以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援雲棲社區。