命名函数

介绍

简而言之,命名化的函数表达式只对一个有用——在解析器和调试器中的描述性的函数名。因此,存在着在递归中使用函数名的可能,可是你讲看到这将是不可行的。如下你将看到你将面对的跨浏览器问题和一些解决技巧。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);
相关文章
相关标签/搜索