【譯】節選--揭祕命名函數表達式(Named function expressions )


簡介

使人驚訝的是,在網上,關於命名函數表達式的討論彷佛並很少。這可能由於有不少誤解在流傳。在本文中,我會試着從理論和實踐兩個方面總結這些精彩的Javascript構念,包括其中好的、壞的以及「醜陋」的部分。javascript

簡單說,命名函數表達式只對一種東西有用——調試工具(debugger)和分析器(profiler)中的描述性函數名。在遞歸時也可能用到函數名,但你很快會發現這種用法在當今每每不怎麼實用。若是你不關心調試代碼的體驗,那就不用操心;否則的話,就往下讀,你會看到一些你必須處理的跨瀏覽器問題,以及關於如何處理它們的建議。html

I’ll start with a general explanation of what function expressions are how modern debuggers handle them. 請隨意跳到最終方案,該方案解釋瞭如何安全使用這些構念。java

函數表達式和函數聲明

ECMAScript中有兩種最多見的方式能夠建立function對象:函數聲明和函數表達式。兩者的區別確實很讓人迷惘,至少對我來講是這樣的。ECMA規範惟一明確的就是,函數聲明必須帶有一個標識符(Identifier),或者說函數名,而函數表達式則能夠省略它:git

FunctionDeclaration : function Identifier ( FormalParameterListopt ){ FunctionBody }github

FunctionExpression : function Identifieropt ( FormalParameterListopt ){ FunctionBody }web

咱們能夠看到,當標識符被省略,那段代碼就是表達式了。但若是標識符存在呢?怎麼能分清它是一個表達式仍是一個聲明?它們看起來如出一轍。ECMAScript彷佛是經過上下文來區分它們。若是function foo(){}是一個賦值表達式的一部分,那它就是一個函數表達式。相反地,若是function foo(){}被包含在一個函數體內,或在一個程序(頂層)自己中,那它就是一個函數表達式。express

function foo(){} //函數聲明
(function foo(){}); //函數表達式:由於被分組操做符括號包圍

try {
  (var x = 5); //分組操做符只能包含表達式,不能包含語句
} catch(err) {
  // SyntaxError
}
複製代碼

你可能會想到在用eval計算JSON時,字符串一般被括號包圍——eval('(' + json + ')')。這固然也是出於相同緣由——分組操做符,圓括號強制把JSON括號解析爲表達式,而不是一個代碼塊。(原文:grouping operator, which parenthesis are, forces JSON brackets to be parsed as expression rather than as a block):json

try {
  { "x": 5 }; // "{"和"}"解析爲代碼塊
} catch(err) {
  // SyntaxError
}

({ "x": 5 }); //分組操做符強制把"{"和"}"解析爲對象字面量
複製代碼

聲明和表達式的行爲有個微妙的不一樣瀏覽器

首先,即便聲明位於源代碼的最後,函數聲明仍然要比做用域中其餘表達式先被解析和計算。下面例子示範了fn函數在alert執行時就已經被定義了,即便它在alert後面:緩存

alert(fn());

function fn() {
  return 'Hello world!';
}
複製代碼

函數聲明的另外一個重要特性就是,根據條件聲明函數是不符合標準的,而且在不一樣環境中表現不一樣。絕對不要使用根據條件聲明的函數,而應該使用函數表達式。

// 千萬別這麼寫!
//有些瀏覽器會聲明「foo」爲返回「first」的那個,
// 另外一些則會聲明爲返回「second」的那個

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();
複製代碼

若是你對函數聲明的實際生成規則好奇的話,就往下看。不然能夠跳過下面的摘錄。

函數聲明只容許出如今程序或另外一個函數體中。按照語法,它們不能出如今代碼塊中({..})——例如ifwhilefor語句。由於代碼塊只能包含語句,不能包含SourceElement,也就是函數聲明。若是仔細觀察生成規則,就能發現,只有當表達式是表達式語句(ExpressionStatement)的一部分時,它才被容許直接包含在代碼塊中。然而,表達式語句明肯定義了不能以「function」開頭,這就是爲什麼函數聲明不能直接出如今語句或代碼塊中(記住,代碼塊也只是一系列語句)。

由於這些限制,無論是函數聲明仍是函數表達式,只要直接出如今代碼塊中(如上例),就會被認爲是一個語法錯誤(syntax error)。問題是,我見到的幾乎全部實現都沒有嚴格聽從該規則(BESENDMDScript是例外)。他們用專有方式來解釋(原文:They interpret them in proprietary ways instead)。

值得一提的是,按照規範,實現(implementations)容許引入語法擴展(見第十六章),但仍然徹底一致。這正是現現在這麼多客戶端存在的狀況。Some of them interpret function declarations in blocks as any other function declarations —只是爲了把函數聲明提高到做用域頂端;另外一些引入不一樣的語法,遵循稍微複雜的規則。

函數語句

其中一個語法擴展就是函數語句,目前在基於Gecko的瀏覽器中實現(測試於Mac OS X中的Firefox 1-3.7a1pre)。不知爲什麼,不管好的壞的方面,這個擴展彷佛並不廣爲人知(MDC說起了該擴展,但很簡單)。請記住,咱們在此僅以學習爲目的討論,知足咱們的好奇心;除非你正在寫針對基於Gecko的環境的腳本,不然我不推薦依賴該擴展。

因此,這些非標準的構念有這些特性:

  1. 在任何容許使用純語句的地方,均可以使用函數語句。這也固然包括代碼塊:

    if (true) {
      function f(){ }
    }
    else {
      function f(){ }
    }
    複製代碼
  2. 函數語句像任何其餘語句同樣解析,包括條件執行:

    if (true) {
      function foo(){ return 1; }
    }
    else {
      function foo(){ return 2; }
    }
    foo(); // 1
    // 注意,其餘環境把這裏的「foo」解讀爲函數聲明,
    //第二個「foo」覆寫了第一個, 併產生結果"2",而不是「1」
    複製代碼
  3. 函數聲明並不在變量實例化的時候被聲明。它們被聲明於運行時,就像函數表達式同樣。然而,一旦聲明瞭,函數語句的標識符在函數做用域內就可用了。該標識符的可用性使得函數語句區別於函數表達式(你會在下一章看到命名函數表達式的確切行爲)。

    //此時,「foo」尚未被聲明
    typeof foo; // "undefined"
    if (true) {
      // 一旦進入代碼塊,「foo」就變成被聲明狀態,
      //在整個做用域內可用
      function foo(){ return 1; }
    }
    else {
      // 沒進入這個代碼塊,
      //這裏的「foo」永遠不會被聲明
      function foo(){ return 2; }
    }
    typeof foo; // "function"
    複製代碼

    一般來講,咱們能夠根據以前的例子,用標準代碼模擬函數語句行爲:

    var foo;
    if (true) {
      foo = function foo(){ return 1; };
    }
    else {
      foo = function foo() { return 2; };
    }
    複製代碼
  4. 函數語句的字符串表示與函數聲明以及命名函數表達式相似(在本例中包括「foo」標識符):

    if (true) {
      function foo(){ return 1; }
    }
    String(foo); // function foo() { return 1; }
    複製代碼
  5. 最終,在早期(低於FireFox 3)基於Gecko的實現中出現了一個bug,那就沒法用函數語句覆寫函數聲明:

    //函數聲明
    function foo(){ return 1; }
    if (true) {
      //用函數語句覆寫
      function foo(){ return 2; }
    }
    foo(); // 低於FF 3的結果是1,FF 3.5及更高版本是2
    // 然而,覆寫函數表達式就不會這樣
    var foo = function(){ return 1; };
    if (true) {
      function foo(){ return 2; }
    }
    foo(); // 在全部版本中結果都是2
    複製代碼

注意,舊版Safari(至少1.2.3, 2.0到2.0.4以及3.0.4,更早版本也可能)中,執行函數語句的方式與SpiderMonkey相同。本章全部例子,除了最後一個「bug」例子,在這些版本的Safari中產生與Firefox相同的結果。另外一個遵循相同語法的瀏覽器就是黑莓瀏覽器(8230機型起,9000和9350機型)。這種行爲的多樣性,再次印證了依賴這些擴展是多麼糟糕的主意。

命名函數表達式

函數表達式確實常見。web開發中的一個常見模式就是,基於某種功能測試復刻函數定義,以得到最佳實踐。這些復刻一般出如今相同做用域,因此老是頗有必要使用函數表達式。總之,如目前所知,函數聲明不該該按條件執行:

// `contains` is part of "APE Javascript library" (http://dhtmlkitchen.com/ape/) by Garrett Smith
var contains = (function() {
  var docEl = document.documentElement;

  if (typeof docEl.compareDocumentPosition != 'undefined') {
    return function(el, b) {
      return (el.compareDocumentPosition(b) & 16) !== 0;
    };
  }
  else if (typeof docEl.contains != 'undefined') {
    return function(el, b) {
      return el !== b && el.contains(b);
    };
  }
  return function(el, b) {
    if (el === b) return false;
    while (el != b && (b = b.parentNode) != null);
    return el === b;
  };
})();
複製代碼

很明顯,當一個函數表達式有一個名字(標識符),它就是命名函數表達式(named function expression)了。你在的一個例子中看到的——var bar=function foo(){};——偏偏就是一個命名函數表達式,其名字是foo。一個重要細節須要謹記:它的名字只在新定義的函數的做用域中可用;規範要求一個標識符不應跨做用域使用:

var f = function foo(){
  return typeof foo; // "foo"在最近的大括號內可用
};
// `foo`在外面無效
typeof foo; // "undefined"
f(); // "function"
複製代碼

因此命名函數表達式有什麼特別嗎?爲何咱們要給它們命名?

由於命名了的函數可以提高代碼調試體驗。當咱們調試一個程序時,有一個描述性的子項的調用棧很是有用。

調試工具(debugger)中的函數名

當一個函數有一個相關鏈的標識符,調試工具在檢查調用棧時將其做爲函數名。某些調試工具(好比Firebug)會幫你顯示函數名,即便是匿名函數。不幸的是,這些調試工具一般依賴簡單的解析規則;這種抽象一般脆弱,常常產生錯誤結果。

來看一個簡單例子:

function foo(){
  return bar();
}
function bar(){
  return baz();
}
function baz(){
  debugger;
}
foo();
//這裏,咱們用函數聲明定義三個函數
// 當調試工具停在「debugger」語句,
// (firebug中的)調用棧很具備描述性:
baz
bar
foo
expr_test.html()
複製代碼

可見expr_test.html的全局做用域調用foofoo調用barbar調用baz。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
(?)()
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和另外一個函數交換了引用——報出「spoofed」的那個。這樣的解析——簡單狀況下很棒——在複雜腳本中無用。

綜上,命名函數表達式是得到可靠、健壯調用棧檢查的惟一方式。讓咱們來重寫一下以前的例子:

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()
複製代碼

JScript bug

不幸的是,JScript(IE的ECMAScript實現)完全搞亂了命名函數表達式。那時候命名函數表達式被不少人反對,JScript要爲負責。可悲的是,即便是上一個版本的JScript(5.8,IE 8),仍然保留着每個下面說到的怪癖。

讓咱們來看看這個破玩意到底哪裏不對勁。理解這些問題能使咱們正確處理它們。注意,我把這些差別拆分到不一樣例子中——清晰起見——即便它們更像是一個主要bug的一系列後果。

例#1: 函數表達式標識符泄漏進封閉做用域

var f = function g(){};
typeof g; // "function"
複製代碼

記得嗎?我提到過,一個命名函數的標識符在封閉做用域中無效。可是,JScript並不認同這點——上面例子中的g解析到了一個函數對象上。這是最普遍觀察到的差別。這種污染封閉做用域的行爲是危險的——由於做用域多是全局的。這種bug不容易排查。

例#2: 命名函數表達式被當成是聲明和表達式

typeof g; // "function"
var f = function g(){};
複製代碼

正如我以前解釋的,一個特定上下文中的函數聲明要比其餘表達式先被解析。上面的例子證實了JScript確實把命名函數表達式看成是函數聲明。你能夠看到,在聲明以前能夠解析了。

例#3: 命名函數表達式建立兩個不一樣的函數對象

var f = function g(){};
f === g; // false

f.expando = 'foo';
g.expando; // undefined
複製代碼

這裏事情就變得有趣了,或者說,徹底扯蛋了。在這裏咱們要面對這樣的危險性:兩個對象,給一個賦值並不會修改另外一個;若是你要使用諸如緩存機制之類的,或者在f的屬性中存儲東西再以g的屬性去讀取,就會很麻煩,由於你覺得是同一個對象。

例#4: 函數聲明是按序解析的,不受條件代碼塊影響

var f = function g() {
  return 1;
};
if (false) {
  f = function g(){
    return 2;
  };
}
g(); // 2
複製代碼

這種例子甚至可能更難追蹤bug。

相關文章
相關標籤/搜索