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

  • 2020 年 3 月 26 日
  • 筆記

  從這篇開始,我們會用很長的章節來討論函數,這個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()被“看到”)。

  最後強調一下函數的兩個特徵:它們都是對象,它們提供局部作用域。

 

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