1. 程式人生 > >JavaScript 模式》讀書筆記(4)— 函式1

JavaScript 模式》讀書筆記(4)— 函式1

  從這篇開始,我們會用很長的章節來討論函式,這個JavaScript中最重要,也是最基本的技能。本章中,我們會區分函式表示式與函式宣告,並且還會學習到區域性作用域和變數宣告提升的工作原理。以及大量對API、程式碼初始化、程式效能等有幫助的模式。

  我們首先,要來回顧一些基礎知識以明確一些概念和定義。

 

一、背景

  JavaScript中的函式有兩個主要特點顯使其顯得比較特殊。第一個特點在於函式是第一類物件(first-class object),第二個特點在於它們可以提供作用域。函式就是物件:

  • 函式可以在執行時動態建立,還可以在程式執行過程中建立。
  • 函式可以分配給變數,可以將它們的引用複製到其他變數,可以被擴充套件,此外,除少數特殊情況,函式還可以被刪除。
  • 可以作為引數傳遞給其他函式,並且還可以由其他函式返回。
  • 函式可以由自己的屬性和方法。

  因此,對於函式A來說,他可能是一個物件,並且具有自己的屬性和方法,而且其中的方法之一可能恰好又是另一個函式B。此外,函式B可以接受函式C作為引數,並且在執行時可以返回另外的函式D。

function A(){};
A.name = "aName";
function D(){console.log(1234)};
function C(){
    return D();
};
A.B = function (callback){
    callback()
};
A.B(C);

  程式碼看起來就像上面這樣。乍看之下,有許多的函式要記錄。但是適應各種函式應用後,將開始欣賞函式所提供的能力、靈活性、以及表現力。一般來說,當考慮JavaScript中的函式、物件時,其唯一的特性在於該物件(即函式)是可呼叫的,這意味著它是可執行的。

  事實上,當看到new Function()建構函式執行時,函式就是物件的意義就變得非常明確了:

// 反模式
// 僅用於演示目的
var add = new Function('a','b','return a + b');
add(1,2); // returns 3

  在以上這段程式碼中,毫無疑問,add()是一個物件,畢竟它是由一個建構函式所建立。然而,使用Function()建構函式並不是一個好主意,如同使用eval()一樣不好。這是由於程式碼是以字串方式傳遞並重新計算。同樣,這也不便於編寫和閱讀,這是因為必須使用引號隔開程式碼,並且如果出於可讀性的目的而希望在函式中正確地縮排程式碼,那麼還需要格外注意。

  第二個重要的特徵在於函式提供了作用域。在JavaScript中並沒有塊級作用域(當然,let出現之後,已經有了塊級作用域,這裡我們不討論)。函式內部以var關鍵詞定義的任何變數都是區域性變數,對於函式外部是不可見的。考慮到花括號{}並不提供作用域(這句話是沒問題的,哪怕是在現在的ES6出現之後,因為提供作用域的並不是花括號,而是花括號內使用let宣告),因此如果在if條件語句或在for以及while迴圈中,使用var關鍵詞定義一個變數,這並不意味著定義了一個區域性變數。它僅對於包裝函式來說是區域性變數,並且如果沒有包裝函式,它將成為一個全域性變數。

 

消除術語的歧義

  讓我嗯話費一點時間討論用於定義函式的相關程式碼的術語,因為在談論到模式時,使用準確、約定的名稱與程式碼是同等重要的。

// 命名函式表示式
var add = function add(a,b) {
    return a + b;
};

  上面的程式碼顯示了一個函式,它使用了命名函式表示式(named function expression)。如果跳過函式表示式中的名稱(例子中的第二個add),將會得到一個未命名函式表示式,也簡稱為函式表示式,或者最常見的是將之稱為匿名函式。

// 函式表示式,又名匿名函式,未命名函式表示式
var add = function(a,b) {
    return a + b;
};

  因此,廣義上稱為函式表示式,並且命名函式表示式是一個函式表示式的一種特殊情況,通常發生在定義可選的命名時。

  當省略了第二個add並且以一個未命名函式表示式作為結束,這並不會影響該函式的定義以及後續的呼叫。唯一的區別在於該函式物件的name屬性將會變成一個空字串。name屬性是JavaScript語言的一個擴充套件(它並不是ECMA標準的一部分),但是在許多環境中得到了廣泛的應用。如果保留了第二個add,那麼add.name屬性將會包含字串“add”。當使用偵錯程式時,或者當從自身遞迴呼叫同一個函式時,name屬性時非常有用的,否則,可以跳過該屬性。

  最後,獲得了函式宣告(function declaration)。這些宣告看起來與其他語言中所使用的函式極為相似:

function foo() {
    // 此處為函式主體
}

  就語法而言,命名函式表示式與函式的宣告看起來很相似,尤其是如果不將函式表示式的結果分配給變數(後面的回撥模式中會看到)的時候。有時候,沒有其他方法可以區分出函式宣告和命名函式表示式的差異,除非檢視函數出現的上、下文預警,正如將在下一節中所看到的。

  在尾隨的分號中,這兩者之間在語法上存在差異。函式宣告中並不需要分號結尾,但在函式表示式中需要分號,並且應該總是使用分號,及時編輯其中分號自動插入機制可能幫您完成了這個工作。

  函式字面量,這個術語也經常被使用,它可能表示一個函式表示式或命名函式表示式。由於這種模糊性含義,並不推薦使用該術語。

 

宣告vs表示式:名稱和變數宣告提升

  因此,應該使用哪種方法?函式宣告還是函式表示式?在不能使用宣告的情況下,下面將為您解決這種困境。

// 這是一個函式表示式
// 它作為引數傳遞給函式“callMe”
callMe(function (){
    // 這裡,即該函式是一個匿名函式表示式
    // 也被稱為匿名函式
});

// 這是一個命名函式表示式
callMe(function me() {
    // 這裡,即me,是命名函式表示式
    // 名稱是me
});

// 另一個函式表示式
var myobj = {
    say:function() {
        // 這裡是函式表示式
    }
};

  上面的程式碼,展示了將函式物件作為引數傳遞,或者在物件字面量中定義方法。

  注意了:函式宣告只能出現在“程式程式碼”中,這表示它僅能在其它函式體內部或全域性空間中。它們的定義不能分配給變數或者屬性,也不能以引數形式出現在函式呼叫中。

// 全域性作用域
function foo() {}

function local() {
    // 區域性作用域
    function bar() {}
    return bar;
}

  上面的程式碼,foo()、bar()、local()都是以函式宣告模式進行定義的。

 

函式的命名屬性

  當選擇函式定義模式的時候,另一個需要考慮的事情是有關制度name屬性的可用性。同樣,這個屬性並不是標準,但在許多環境中都可以使用它。在函式宣告和命名函式表示式中,已經定義了name屬性。在匿名函式表示式中,他依賴於其實現方式。其name可能是為定義的,也可能是空字串來定義name屬性。

function foo(){} //宣告
var bar = function (){}; //表示式
var baz = function baz() {}; //命名錶達式

console.log(foo.name); //輸出“foo”
console.log(bar.name); //輸出“bar”
console.log(baz.name); //輸出“baz”

  注意,這裡的一個區別,就是在現代瀏覽器中,若把一個匿名函式表示式賦值給一個變數,那麼此時,匿名函式表示式的name屬性即該變數的名字。因版本迭代原因,這與書中描述有些出入。

  name屬性在除錯bug和遞迴呼叫自身時很有用。其他場景可選擇使用匿名函式表示式即可。

var foo = function bar() {};
console.log(foo.name)

  這樣做也是可以的,打印出得結果是bar。這在技術上是沒問題的,但是會存在一些相容問題,所以不建議這樣使用。

 

函式的提升

  從前面的討論中,可能會得出函式宣告的行為幾乎等同於命名函式表示式的行為。然而這並不是完全正確,其區別在於提升(hoisting)行為。

  對於所有變數,無論在函式體的何處進行宣告,都會在後臺被提升到函式頂部。而這對於函式同樣適用,其原因在於函式只是分配給變數的物件。“明白”的地方在於當使用函式宣告時,函式定義也被提升,而不僅僅是函式宣告被提升。

  

// 反模式
// 全域性函式
function foo() {
    console.log("global foo");
}

function bar() {
    console.log("global bar");
}

function hoistMe() {
    // 在這裡是為了判斷提升的內容到底是什麼,僅僅是變數名?還是連帶函式體一起?
    console.log(typeof foo);
    console.log(typeof bar);

    // 執行
    foo();
    bar();

    // 函式宣告
    // 變數“foo”以及其實現者被提升
    function foo() {
        console.log('local foo');
    }

    // 函式表示式
    // 僅變數‘bar’被提升
    // 函式實現未被提升
    var bar = function (){
        console.log('local bar');
    };
}
hoistMe();

  在這個例子中我們可以看到,如同正常的變數一樣,僅存在與hoistMe()函式中的foo和bar移動到了頂部,從而覆蓋了全域性foo和bar函式。兩者之間的區別在於區域性foo()的定義被提升到頂部且能正常執行,即使在後面才定義它。bar()的定義並沒有被提升,僅有他的宣告被提升。這就是為什麼程式碼執行到達bar()的定義時,其顯示結果是undefined且並沒有作為函式來呼叫(然而,在作用域鏈中,仍然防止全域性bar()被“看到”)。

  最後強調一下函式的兩個特徵:它們都是物件,它們提供區域性作用域。

 

  好了,這篇有關函式的基本情況和定義大家都瞭解了。下一篇我們繼續。