本文经过 JavaScript Anti-Pattern Organization 认证,予以公示。阅读前请大呼三声「我决不使用文中代码模式!」。

我们都知道,在 JavaScript 的世界里(文中指 ECMAScript 5 及更早期版本)有三种方式来声明函数[1]

I. 函数声明

function name([param] [, param] [..., param]) {  
   statements
}

II. 函数表达式

function [name]([param] [, param] [..., param]) {  
   statements
}

III. Function 构造函数

[new] Function (arg1, arg2, ... argN, functionBody)

OK,那么让我们看看如何玩坏它。

1. 好玩的变量提升(Hoisting)

先来看这样一段代码:

console.log(a);

var a = function() {};  

某同学:a 在声明之前就被调用,会出错的吧!

一运行,控制台赫然打印出 undefined。为什么不会出错?这是因为在 JavaScript 中,变量声明会被提升到作用域顶部[2],于是上面的代码等同于这样:

var a;

console.log(a);

a = function() {};  

那么,下面这段代码的运行结果会是什么?

console.log(typeof b);

var b = 1;

function b() {};

console.log(b);  

某同学:嗯……我猜是 undefined, function b() {};

运行……诶↘↗→→→→→为什么是 function, 1?同样是因为变量提升:函数声明和变量声明都会被提升到作用域顶部。请跟我读 All declarations in JavaScript will get hoisted。

于是,上面那段代码在解释器里看起来是这样的:

var b;  
function b() {};

console.log(typeof b);

b = 1;

console.log(b);  

所以要记住:由于 JavaScript 没有块级作用域,所以 永远不要在 if, for 之类的代码块中使用函数声明。比如下面这段代码,它可能会带来你意料之外的结果,因为所有的函数声明都被提升到了作用域顶部:

if (true) {  
    function c() {
        console.log('this is true');
    };
} else {
    function c() {
        console.log('this is false');
    };
}

c(); // this is false  

相反,这样的写法是安全的(但不推荐):

if (true) {  
    var d = function() {
        console.log('this is true');
    };
} else {
    var d = function () {
        console.log('this is false');
    };
}

d(); // this is true  

2. 函数声明?函数表达式?

函数表达式与函数声明看起来简直是同一个模子里出来的,但是又有些细微的差别:

var e = function f() {};

console.log(typeof e, typeof f);  

某同学:看起来像是函数声明和函数表达式的结合体!结果应该是 function, function

Pia!控制台输出了 function, undefined!浏览器坏了?不对不对,运算符调用起的是函数表达式而非函数声明,所以此时并未声明 f 变量。那么,这个函数名 f 有什么用呢?请看下面这段代码:

var g = function h() {  
    console.log(typeof h);
};

g();  

某同学(捂着脸):我知道!是 undefined

Pia!控制台输出了 function

不是说没有声明 f 变量吗?!此时出现了一个叫做 Named Function Expressions(下文简称 NFE)的东西[3]一个被命名的函数表达式,可以在其作用域中通过这个变量名访问到此函数,相当于非标准的 arguments.callee

var g = function h() {  
    console.log(g === h, h === arguments.callee, g === arguments.callee);
};

g(); // true, true, true  

3. 伟大的 NFE

某同学(从地上撑起身子):我并不想在函数表达式中创建递归,为什么要给它命名?省去名字我还能节约几个字节呢!

咳咳!那句话怎么说来着?Talk is cheap, show me the code:

(function() {
    throw new Error('error');
})();

(function() {
    throw new Error('error');
})();

(function() {
    throw new Error('error');
})();

艾玛!控制台打印出这样的信息:

Uncaught Error: error  
   (anonymous function)
   ...

anonymous?什么鬼!这三个都是匿名函数,到底是哪个抛出的错?别急少年,我们来改造下看看:

(function jin() {
    throw new Error('error');
})();

(function ke() {
    throw new Error('error');
})();

(function la() {
    throw new Error('error');
})();

控制台输出了这样的信息:

Uncaught Error: error  
   jin
   ...

某同学(一跃而起):原来是发生在第一个函数里!

是的!通过 NFE 我们可以很方便地从错误的堆栈信息查到具体的出错位置,金坷垃!妈妈再也不用担心我的程序老出错!Pia!

(某同学,卒)

参考:

  1. 函数和函数作用域 - JavaScript | MDN
  2. Explaining function and variable hoisting in JavaScript
  3. Named function expressions demystified