你不知道的JavaScript--Item7 函式和(命名)函式表示式

itKingOne發表於2018-06-25

1、函式宣告與函式表示式

在ECMAScript中,建立函式的最常用的兩個方法是函式表示式和函式宣告,兩者期間的區別是有點暈,因為ECMA規範只明確了一點:函式宣告必須帶有標示符(Identifier)(就是大家常說的函式名稱),而函式表示式則可以省略這個標示符:

函式宣告:

function 函式名稱 (引數:可選){ 函式體 }

函式表示式:

function 函式名稱(可選)(引數:可選){ 函式體 }

所以,可以看出,如果不宣告函式名稱,它肯定是表示式,可如果宣告瞭函式名稱的話,如何判斷是函式宣告還是函式表示式呢?ECMAScript是通過上下文來區分的,如果function foo(){}是作為賦值表示式的一部分的話,那它就是一個函式表示式,如果function foo(){}被包含在一個函式體內,或者位於程式的最頂部的話,那它就是一個函式宣告。

function foo(){} // 宣告,因為它是程式的一部分
var bar = function foo(){}; // 表示式,因為它是賦值表示式的一部分

new function bar(){}; // 表示式,因為它是new表示式

(function(){
    function bar(){} // 宣告,因為它是函式體的一部分
})();

表示式和宣告存在著十分微妙的差別,首先,函式宣告會在任何表示式被解析和求值之前先被解析和求值,即使你的宣告在程式碼的最後一行,它也會在同作用域內第一個表示式之前被解析/求值,參考如下例子,函式fn是在alert之後宣告的,但是在alert執行的時候,fn已經有定義了:

alert(fn());

function fn() {
    return 'Hello world!';
}

另外,還有一點需要提醒一下,函式宣告在條件語句內雖然可以用,但是沒有被標準化,也就是說不同的環境可能有不同的執行結果,所以這樣情況下,最好使用函式表示式: 因為在條件語句中沒有塊級作用域這個概念

// 千萬別這樣做!
// 因為有的瀏覽器會返回first的這個function,而有的瀏覽器返回的卻是第二個

if (true) {
  function foo() {
    return 'first';
  }
}
else {
  function foo() {
    return 'second';
  }
}
foo();

// 相反,這樣情況,我們要用函式表示式
var foo;
if (true) {
  foo = function() {
    return 'first';
  };
}
else {
  foo = function() {
    return 'second';
  };
}
foo();

函式宣告的實際規則如下:

函式宣告只能出現在程式或函式體內。從句法上講,它們 不能出現在Block(塊)({ … })中,例如不能出現在 if、while 或 for 語句中。因為 Block(塊) 中只能包含Statement語句, 而不能包含函式宣告這樣的源元素。另一方面,仔細看一看規則也會發現,唯一可能讓表示式出現在Block(塊)中情形,就是讓它作為表示式語句的一部分。但是,規範明確規定了表示式語句不能以關鍵字function開頭。而這實際上就是說,函式表示式同樣也不能出現在Statement語句或Block(塊)中(因為Block(塊)就是由Statement語句構成的)。

2、命名函式表示式

提到命名函式表示式,理所當然,就是它得有名字,前面的例子var bar = function foo(){};就是一個有效的命名函式表示式,但有一點需要記住:這個名字只在新定義的函式作用域內有效,因為規範規定了標示符不能在外圍的作用域內有效:

var f = function foo(){
  return typeof foo; // function --->foo是在內部作用域內有效
};
// foo在外部用於是不可見的
typeof foo; // "undefined"
f(); // "function"

既然,這麼要求,那命名函式表示式到底有啥用啊?為啥要取名?

正如我們開頭所說:給它一個名字就是可以讓除錯過程更方便,因為在除錯的時候,如果在呼叫棧中的每個項都有自己的名字來描述,那麼除錯過程就太爽了,感受不一樣嘛。

tips:這裡提出一個小問題:在ES3中,命名函式表示式的作用域物件也繼承了 Object.prototype 的屬性。這意味著僅僅是給函式表示式命名也會將 Object.prototype 中的所有屬性引入到作用域中。結果可能會出人意料。

var constructor = function(){return null;}
var f = function f(){
    return construcor();
}
f(); //{in ES3 環境}

該程式看起來會產生 null, 但其實會產生一個新的物件。因為命名函式表示式在其作用域內繼承了 Object.prototype.constructor(即 Object 的建構函式)。就像 with 語句一樣,這個作用域會因 Object.prototype 的動態改變而受到影響。幸運的是,ES5 修正了這個錯誤。

這種行為的一個合理的解決辦法是建立一個與函式表示式同名的區域性變數並賦值為 null。即使在沒有錯誤地提升函式表示式宣告的環境中,使用 var 重宣告變數能確保仍然會繫結變數 g。設定變數 g 為 null 能確保重複的函式可以被垃圾回收。

var f = function g(){
    return 17;
}
var g =null;

3、偵錯程式(呼叫棧)中的命名函式表示式

剛才說了,命名函式表示式的真正用處是除錯,那到底怎麼用呢?如果一個函式有名字,那偵錯程式在除錯的時候會將它的名字顯示在呼叫的棧上。有些偵錯程式(Firebug)有時候還會為你們函式取名並顯示,讓他們和那些應用該函式的便利具有相同的角色,可是通常情況下,這些偵錯程式只安裝簡單的規則來取名,所以說沒有太大價值,我們來看一個例子:不用命名函式表示式

function foo(){
  return bar();
}
function bar(){
  return baz();
}
function baz(){
  debugger;
}
foo();

// 這裡我們使用了3個帶名字的函式宣告
// 所以當偵錯程式走到debugger語句的時候,Firebug的呼叫棧上看起來非常清晰明瞭 
// 因為很明白地顯示了名稱
baz
bar
foo
expr_test.html()

通過檢視呼叫棧的資訊,我們可以很明瞭地知道foo呼叫了bar, bar又呼叫了baz(而foo本身有在expr_test.html文件的全域性作用域內被呼叫),不過,還有一個比較爽地方,就是剛才說的Firebug為匿名錶達式取名的功能:

function foo(){
  return bar();
}
var bar = function(){
  return baz();
}
function baz(){
  debugger;
}
foo();

// Call stack
baz
bar() //看到了麼? 
foo
expr_test.html()

然後,當函式表示式稍微複雜一些的時候,偵錯程式就不那麼聰明瞭,我們只能在呼叫棧中看到問號:

function foo(){
  return bar();
}
var bar = (function(){
  if (window.addEventListener) {
    return function(){
      return baz();
    };
  }
  else if (window.attachEvent) {
    return function() {
      return baz();
    };
  }
})();
function baz(){
  debugger;
}
foo();

// Call stack
baz
(?)() // 這裡可是問號哦,顯示為匿名函式(anonymous function)
foo
expr_test.html()

另外,當把函式賦值給多個變數的時候,也會出現令人鬱悶的問題:

function foo(){
  return baz();
}
var bar = function(){
  debugger;
};
var baz = bar;
bar = function() { 
  alert('spoofed');
};
foo();

// Call stack:
bar()
foo
expr_test.html()

這時候,呼叫棧顯示的是foo呼叫了bar,但實際上並非如此,之所以有這種問題,是因為baz和另外一個包含alert(‘spoofed’)的函式做了引用交換所導致的。

歸根結底,只有給函式表示式取個名字,才是最委託的辦法,也就是使用命名函式表示式。我們來使用帶名字的表示式來重寫上面的例子(注意立即呼叫的表示式塊裡返回的2個函式的名字都是bar):

function foo(){
  return bar();
}
var bar = (function(){
  if (window.addEventListener) {
    return function bar(){
      return baz();
    };
  }
  else if (window.attachEvent) {
    return function bar() {
      return baz();
    };
  }
})();
function baz(){
  debugger;
}
foo();

// 又再次看到了清晰的呼叫棧資訊了耶!
baz
bar
foo
expr_test.html()

OK,又學了一招吧?

相關文章