命名函數

介紹

簡而言之,命名化的函數表達式只對一個有用——在解析器和調試器中的描述性的函數名。因此,存在着在遞歸中使用函數名的可能,可是你講看到這將是不可行的。如下你將看到你將面對的跨瀏覽器問題和一些解決技巧。javascript

函數表達式VS函數聲明

在ECMAScript中常見的建立函數的方式是函數聲明和函數表達式。二者之間的區別很讓人困惑,至少對我而言是這樣的。在ECMA的說明中只是講清楚了函數聲明必需要有一個標識符(若是你喜歡,能夠叫作函數名),然而函數表達式能夠省略函數名:html

函數聲明 :
function Identifier ( FormalParameterList opt ){ FunctionBody }java

函數表達式 :
function Identifier opt ( FormalParameterList opt ){ FunctionBody }web

咱們能夠看到函數名被省略的時候,函數就只能叫作函數表達式。express

可是若是函數名存在呢?json

怎麼判別函數表達式和函數聲明呢——他們看起來是那麼的類似?瀏覽器

鑑別兩個的不一樣是它們基於的環境。若是一個function foo(){}是一個賦值表達式的一部分,那麼它將被視爲函數表達式。安全

若是function foo(){}是在一個函數體中,或者是在(頂級)程序自己,那麼它將是函數聲明。app

function foo(){} // 函數聲明,由於它是(頂級)程序的一部分
var bar = function foo(){}; // 函數表達式,由於它是賦值表達式的一部分。

new function bar(){}; // 函數表達式,由於它是new表達式的一部分

(function(){
  function bar(){} // 函數聲明,由於他是函數體的一部分
})();

一個函數表達式比較不起眼的例子是函數被包含在大括號中,也就是 (function foo(){})。之因此成爲函數表達式是由於它的環境 "(" and ")" :組成了一個分組操做符()而分組操做符()只能包含一個表達式:less

function foo(){} // 函數聲明function declaration
(function foo(){}); //函數表達式:歸功於分組操做符 function expression: due to grouping operator

try {
  (var x = 5); // 分組操做符裏面只能包含函數表達式,而不是一個聲明語句(var用來聲明)
} catch(err) {
  // SyntaxError
}

你可能想到用eval來執行JSON,字符老是包含在括號中——eval('(' + json + ')')。這固然是由於相同個緣由——分組操做符,也就是圓括號,強迫JSON左右的方括號被解析成表達式而不是當作一個語句塊。

try {
  { "x": 5 }; // 這裏「{」和「}」將被當作語句塊
} catch(err) {
  // SyntaxError
}

({ "x": 5 }); //分組操做符強迫「{」和「}」解析成對象字面量

這裏有些函數表達式和函數聲明的細微差異。

1.函數聲明在任何函數表達式以前被解析和計算,即便函數聲明書寫在程序的最後,它也會優先於任何的函數表達式。

alert(fn());

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

2.另外一個重要特性是根據條件語句來選擇性的定義不一樣的函數聲明是不符合規範的,而且在不一樣的瀏覽器中效果不同。因此,你永遠不要這樣作,這樣的狀況下應該選擇函數表達式:

// Never do this!
// Some browsers will declare `foo` as the one returning 'first',
// while others — returning 'second'

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

// Instead, use function expressions:
var foo;
if (true) {
  foo = function() {
    return 'first';
  };
}
else {
  foo = function() {
    return 'second';
  };
}
foo();

函數聲明只能出如今函數體和頂級程序中,語法上而言,他們不能出如今語句塊中({ ... }) ——例如if,while亦或for語句中。

由於語句塊只能包含Statement,不能包塊源對象Element Source(也就是函數聲明)。函數表達式在語句塊中被容許的惟一解釋是函數表達式是表達式語句的一部分。

然而,表達式語句明確的表示不能以「function」關鍵字開頭,因此這就是爲何函數聲明不能在Statement和語句塊中出現(注意語句塊也是一系列的Statement)。

 由於這些限制,因此只要函數直接出如今語句塊中就被視爲語法錯誤,而不是被視爲一個函數聲明亦或函數表達式。問題是幾乎沒有嚴格按照上面規則執行的瀏覽器,它們按照本身的規則解析。有些瀏覽器將在語句塊中的函數聲明解析成和其餘的函數聲明同樣——進行函數聲明提早;其餘的瀏覽器則引入不一樣的語法而且按照複雜的規格執行。

函數語句

一個ECMAScript的語法擴展就是函數語句,最近被基於Gecko的瀏覽器實現(在Mac OS X上的Firefox1-3.7上測試)。除非是書寫專門針對基於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. 函數語句不是在變量實例化的時候被聲明,而是在運行的時候被聲明,就像函數表達式同樣。可是,一旦聲明,函數名能夠在函數整個做用域中使用。這正是函數語句和函數表達式之間的不一樣。
    // at this point, `foo` is not yet declared
    typeof foo; // "undefined"
    if (true) {
      // once block is entered, `foo` becomes declared and available to the entire scope
      function foo(){ return 1; }
    }
    else {
      // this block is never entered, and `foo` is never redeclared
      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. 最後,在基於Gecko的執行環境中出現的bug是函數語句將重寫函數聲明(出如今Firefox版本小於等於3的狀況下)早期版本中函數聲明卻不能重寫函數語句。
    // function declaration
    function foo(){ return 1; }
    if (true) {
      // overwritting with function statement
      function foo(){ return 2; }
    }
    foo(); // 1 in FF<= 3, 2 in FF3.5 and later
    
    // however, this doesn't happen when overwriting function expression
    var foo = function(){ return 1; };
    if (true) {
      function foo(){ return 2; }
    }
    foo(); // 2 in all versions

     

注意老版本的Safari(至少1.2.3,2.0-2.0.4和3.0.4版本亦或更早的版本)中函數語句的執行聽從SpiderMonkey(js的一種解釋引擎)。「函數語句」章節下的全部例子(除了最後關於「bug」的例子)也就是在firefox下執行的例子的結果和這些早期版本的Safari實現的效果同樣。另外一個符合相同語法的是Blackberry。函數語句在不一樣瀏覽器下面的不一樣表現再次說明了使用函數語句來建立函數是一個很糟糕的想法。

命名的函數表達式

能夠常常看到函數表達式。web開發中一個比較常見的模式是基於不一樣的特性分開來定義函數,來知足最佳性能。這些分開的定義一般發生在同一個做用域中,因此使用函數表達式很是有必要。由於畢竟函數聲明不能被有條件的執行(不能出如今if中)。

// `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;
  };
})();

 

很明顯的是,當函數有函數名的時候,它被叫作命名的函數表達式。就像你在第一個例子中看到的——var bar = function foo(){}:就是一個命名的函數表達式,用foo來當作函數名。須要注意的是函數名只有在函數中才能被使用,不能在函數外使用。

var f = function foo(){
  return typeof foo; // "foo" is available in this inner scope
};
// `foo` is never visible "outside"
typeof foo; // "undefined"
f(); // "function"

因此命名函數有什麼特殊呢?體如今調試的時候,用描述性的項目來操縱一個調用棧將產生巨大的不一樣。

在調試器中的函數名

當一個函數有對應的函數名時,在檢查調用棧的時候,調試器展示做爲函數名的標識符。一些調試器(例如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
(?)()
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」。

以上的全部緣由是命名函數表達式是惟一能夠得到一個真正強大的堆棧檢查(What it all boils down to is the fact that named function expressions is the only way to get a truly robust stack inspection)。讓咱們用命名的函數表達式重寫上面的代碼。注意從自執行的包裝器中返回的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();

// And, once again, we have a descriptive call stack!
baz
bar
foo
expr_test.html()

JScript bugs

不幸的是,JScript(好比IE的ECMAScript執行環境)將命名函數表達式弄得一團糟。

不少人之因此不推薦函數表達式,得歸咎於JScript。即時IE8中的JScript的5.8版本也有如下的怪癖行爲。

Example #1: 函數表達式中的函數名能夠在函數外使用

var f = function g(){};
typeof g; // "function"

記住我以前說的函數名只能在函數中使用,不能再函數外使用麼?可是JScript不符合這個標準——上例中的g被解析成一個函數對象。這樣講會污染環境——有多是全局環境——將致使對象的難追蹤。

 

Example #2: 命名的函數表達式將被同時當作函數聲明和函數表達式對待

typeof g; // "function"
var f = function g(){};

就像我以前說的,函數聲明有個函數聲明提高。上面的例子說明了命名的函數表達式在JScript中被捅死當作函數聲明和函數表達式被對待。這也引出了下面的例子:

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

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

f.expando = 'foo';
g.expando; // undefined

這正是有趣有讓人煩惱的地方,由於改變其中個一個對象另外一個對象不會隨之改變。

Example #4:函數聲明按照順序解析而且不會被條件語句塊影響 

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

像上面的例子更加難以追蹤bug。發生的原理很是簡單,第一,g被解析成函數聲明,由於函數聲明在JScript中是不受條件語句塊的影響的,從if的false條件語句中g被聲明成函數——function g(){ return 2 }。而後全部的常規表達式將被計算,f將被賦值給另外一個剛剛被建立的函數對象。if的false條件句在計算表達式的時候將永遠不會被執行,因此f保持指向第一個函數——function g(){ return 1 }。如今瞭解了,若是你不當心在f中調用g,你將調用一個徹底不相關的g函數對象。

 

你也許會懷疑argumens.callee的影響,callee指向f仍是g呢?

var f = function g(){
  return [
    arguments.callee == f,
    arguments.callee == g
  ];
};
f(); // [true, false]
g(); // [false, true]

你能夠看見argum.callee執行正在被觸發的函數。

 

在沒有聲明的賦值語句中使用命名的函數表達式,你將看到另外一個有趣的現象,可是隻能是賦值給和函數名同名的變量。

(function(){
  f = function f(){};
})();

 

沒有聲明的賦值語句(不推薦,這裏只是用於演示目的)經常被當作全局變量f的屬性。可是在JScript中f將被當作局部變量,因此左邊的函數只是被賦值給這個局部變量f,全局變量歷來都木有被創造。

 

看看JScript的不足,咱們能夠清晰的看到咱們應該避免什麼:

1.注意函數名會泄露到全局變量

2.永遠不要使用函數名來指代函數,使用函數賦值給的變量名亦或argu.callee,若是你使用了函數名,想一想函數的做用只是在調試的時候使用的

3.清除在命名函數表達式聲明中建立的無關的函數。

最後一條解釋可能還須要一些例子:

JScript memory management

Being familiar with JScript discrepancies, we can now see a potential problem with memory consumption when using these buggy constructs. Let’s look at a simple example:

var f = (function(){
  if (true) {
    return function g(){};
  }
  return function g(){};
})();

上面函數的做用是在匿名函數中返回一個函數名爲g的函數,並賦值給外面的變量f。

咱們知道了在JScript中建立了沒必要要的函數對象g,而且和f是兩個徹底不相關的對象,這就形成了必定的內存浪費,除非咱們故意打斷函數名對於函數的引用。

var f = (function(){
  var f, g;
  if (true) {
    f = function g(){};
  }
  else {
    f = function g(){};
  }
  // null `g`, so that it doesn't reference extraneous function any longer
  g = null;
  return f;
})();

注意咱們在匿名函數內部聲明瞭局部變量g,因此g=null賦值語句將不會產生局部變量。

把null賦值給g,咱們容許了垃圾回收器來回收g函數對象,以此釋放內存。

 

SpiderMonkey 怪癖

咱們知道了命名的函數表達式的函數名只能在函數內部使用,爲何會這樣呢?緣由以下:

當命名的函數表達式被執行的時候,一個特殊的對象被建立。

這個對象的惟一做用是持有一個和函數名同樣的屬性,屬性值和函數對應。

而後這個對象嵌入當前做用域鏈的最前面,而後這個被擴大的做用域鏈被用於初始化一個函數。

 

有趣的是ECMA-262定義特殊對象(持有函數名的對象)的方式。規則上說對象是經過new Obeject方式建立的,因此是內置Object對象的實例,然而只有一種javascript解析器SpiderMonkey是按照這樣的方式解析的。在SpiderMonkey中,能夠經過擴充Object.property的方式來處理函數局部變量。

Object.prototype.x = 'outer';

(function(){

  var x = 'inner';

  /*
    `foo`函數在這裏有一個特殊的對象——保存了函數名。 對象其實是`{ foo: <function object> }`。
   當在做用域鏈中搜索x的時候,先在‘foo’的上下文中搜索。沒有發現的話,在做用域鏈的上一級對象搜索,這個對象就是擁有函數名的對象————{ foo: <function object> }
  由於對象繼承自`Object.prototype`,因此x能夠在這裏發現,也就是`Object.prototype.x` (值是 'outer')。
  外部的函數做用域(也就是 x === 'inner')將永遠不會到達。
*/ (function foo(){ alert(x); // alerts `outer` })(); })();

 

注意最新版本的SpiderMonkey實際上改變了這個行爲,特殊的對象將再也不繼承Object.prototype.可是你仍然能夠在 Firefox <=3的版本中看見.

另外一個將其做爲全局對象的實例的是BlackBerry瀏覽器。這一次,是使用了Activation 對象(其繼承自  Object.prototype)。

Object.prototype.x = 'outer';

(function(){

  var x = 'inner';

  (function(){

    /*
    When `x` is being resolved against scope chain, this local function's Activation Object is searched first.
    There's no `x` in it, of course. However, since Activation Object inherits from `Object.prototype`, it is
    `Object.prototype` that's being searched for `x` next. `Object.prototype.x` does in fact exist and so `x`
    resolves to its value — 'outer'. As in the previous example, outer function's scope (Activation Object)
    with its own x === 'inner' is never even reached.
    */

    alert(x); // alerts 'outer'

  })();
})();

和已經存在的 Object.prototype 成員將致使衝突。

(function(){

  var constructor = function(){ return 1; };

  (function(){

    constructor(); // evaluates to an object `{ }`, not `1`

    constructor === Object.prototype.constructor; // true
    toString === Object.prototype.toString; // true

    // etc.

  })();
})();

 

解決Blackberry怪癖的方法很明顯:避免使用Object.prototype 的屬性來命名變量:toStringvalueOfhasOwnProperty,等等。

JScript 解決方案

var fn = (function(){

  // 聲明一個變量,用於以後函數賦值給該變量
  var f;

  // 有條件的建立一個命名的函數表達式
  // 並將f指向該對象
  if (true) {
    f = function F(){ };
  }
  else if (false) {
    f = function F(){ };
  }
  else {
    f = function F(){ };
  }

  // 給函數名變量賦值null
  // 這樣使得函數名變量能夠被垃圾回收機制回收
  var F = null;

  // 返回根據條件語句建立的函數
  return f;
})();

最後,咱們將使用這個技術到真實生活中,當書寫相似於跨瀏覽器的函數addEvent:

// 1) 在一個獨立的做用域中聲明函數
var addEvent = (function(){

  var docEl = document.documentElement;

  // 2) 聲明一個變量,以後的函數將賦值給該變量
  var fn;

  if (docEl.addEventListener) {

    // 3) 確保給函數一個描述性的函數名
    fn = function addEvent(element, eventName, callback) {
      element.addEventListener(eventName, callback, false);
    };
  }
  else if (docEl.attachEvent) {
    fn = function addEvent(element, eventName, callback) {
      element.attachEvent('on' + eventName, callback);
    };
  }
  else {
    fn = function addEvent(element, eventName, callback) {
      element['on' + eventName] = callback;
    };
  }

  // 4)清楚‘addEvent’被JScript建立的函數名 
  //  確保在以前使用var聲明變量名或者在函數的頂部聲明瞭‘addEvent’
  var addEvent = null;

  // 5)最後經過返回函數表達式賦值給的變量來返回函數
  return fn;
})();

 

可選擇的解決方案

可使用函數聲明而不是函數表達式,這個方法只能定義一種函數纔有用:

var hasClassName = (function(){

  // 定義一些私有變量
  var cache = { };

  //使用函數聲明
  function hasClassName(element, className) {
    var _className = '(?:^|\\s+)' + className + '(?:\\s+|$)';
    var re = cache[_className] || (cache[_className] = new RegExp(_className));
    return re.test(element.className);
  }

  // 返回函數
  return hasClassName;
})();

函數聲明明顯不能在條件語句中使用,然而,能夠在最開始的時候定義一系列的函數聲明,而後在不一樣的狀況下返回不一樣的函數聲明,從而達到有選擇性的返回不一樣函數的功能。

var addEvent = (function(){

  var docEl = document.documentElement;

  function addEventListener(){
    /* ... */
  }
  function attachEvent(){
    /* ... */
  }
  function addEventAsProperty(){
    /* ... */
  }

  if (typeof docEl.addEventListener != 'undefined') {
    return addEventListener;
  }
  else if (typeof docEl.attachEvent != 'undefined') {
    return attachEvent;
  }
  return addEventAsProperty;
})();

可是它有本身的不足,由於會形成內存損耗。將全部的函數在最開始聲明,你講蓄意的建立了n-1個沒有用的函數,你能夠看見,若是attacheEvent在document.documentElemnet中被發現,那麼addEventListener和addEventAsProperty都將永遠不會被使用,可是他們仍舊使用了內存。

更多的考慮

在ECMA-262,5th edition中介紹了嚴格模式,目的是不接受js中脆弱的,不可靠的亦或危險的語法代碼。出於安全的考慮,argu.callee也被禁止。

在嚴格模式下面,使用arguments.callee將會報錯TypeError。

之因此我在這裏提出嚴格模式的概念是由於嚴格模式下不能使用arguments.callee將刀子更多的使用命名的函數表達式。因此理解命名函數表達式的語法以及bug很重要。

// Before, you could use arguments.callee
(function(x) {
  if (x <= 1) return 1;
  return x * arguments.callee(x - 1);
})(10);

// In strict mode, an alternative solution is to use named function expression
(function factorial(x) {
  if (x <= 1) return 1;
  return x * factorial(x - 1);
})(10);

// or just fall back to slightly less flexible function declaration
function factorial(x) {
  if (x <= 1) return 1;
  return x * factorial(x - 1);
}
factorial(10);
相關文章
相關標籤/搜索