# 函数

在 ECMAScript 中有三种函数类型:函数声明,函数表达式和函数构造器创建的函数

# 函数声明

格式:function 函数名称 (参数:可选){ 函数体 }

函数声明(缩写为FD)的特征:

  • 有一个特定的名称

  • 在源码中的位置:要么处于程序级(Pr函数表达式是出现在表达示的位置ogram level),要么处于其它函数的主体(FunctionBody)中

  • 在进入上下文阶段创建

  • 影响变量对象

function exampleFunc() {
  ...
}

函数声明的主要特点在于它对变量对象的影响,在代码执行阶段它们已经可用(因为FD在进入上下文阶段已经存在于VO中——代码执行之前)

例如(函数在其声明之前被调用)

foo();
 
function foo() {
  alert('foo');
}

另外一个重点知识点是上述定义中的第二点——函数声明在源码中的位置:

// 函数可以在如下地方声明:
// 1) 直接在全局上下文中
function globalFD() {
  // 2) 或者在一个函数的函数体内
  function innerFD() {}
}

只有这2个位置可以声明函数,也就是说: 不可能在表达式位置或一个代码块中定义它

函数声明只能出现在程序或函数体内。从句法上讲,它们不能出现在 Block(块)({ ... })中,例如不能出现在 ifwhilefor 语句中。因为 Block (块) 中只能包含 Statement 语句, 而不能包含函数声明这样的源元素。

# 函数表达式

格式:function 函数名称(可选)(参数:可选){ 函数体 }

函数表达式(缩写为FE)的特征:

  • 出现在表达式的位置

  • 有可选的名称

  • 不会影响变量对象

  • 在代码执行阶段创建

函数表达式是出现在表达式的位置:

function foo(){} // 声明,因为它是程序的一部分

(function(){
    function bar(){} // 声明,因为它是函数体的一部分
})();

var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分

// 例子三
(function foo(){}); // 函数表达式:包含在分组操作符内


1, function baz() {}; // 表达式 逗号也是操作符
 
// 在数组初始化器内只能是表达式
[function bar() {}];

new function bar(){}; // 表达式,因为它是new表达式

上面 例子三 之所以是函数表达式,原因是因为括号 () 是一个分组操作符,它的内部只能包含表达式,我们来看几个例子:

function foo(){} // 函数声明
(function foo(){}); // 函数表达式:包含在分组操作符内
  
try {
  (var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句
} catch(err) {
  // SyntaxError
}

你可以会想到,在使用evalJSON进行执行的时候,JSON字符串通常被包含在一个圆括号里:eval('(' + json + ')'),这样做的原因就是因为使用分组操作符让解析器强制将JSON的花括号解析成表达式而不是代码块

try {
  { "x": 5 }; // "{" 和 "}" 做解析成代码块
} catch(err) {
  // SyntaxError
}

({ "x": 5 }); // 分组操作符强制将"{" 和 "}"作为对象字面量来解析

函数表达式只能在代码执行阶段创建而且不存在于变量对象中,让我们来看一个示例行为:

// FE在定义阶段之前不可用(因为它是在代码执行阶段创建)
 
alert(foo); // 报错 VM14656:3 Uncaught ReferenceError: foo is not defined
 
(function foo() {});
 
// 定义阶段之后也不可用,因为他不在变量对象VO中
 
alert(foo);  // 报错 VM14656:3 Uncaught ReferenceError: foo is not defined

# 函数表达式作用

1. 在表达式中使用它们,”不会污染”变量对象。最简单的例子是将一个函数作为参数传递给其它函数

function foo(callback) {
  callback();
}
 
// 将 bar 函赋值给参数
foo(function bar() {
  alert('foo.bar');
});

// 将 baz 函赋值给参数
foo(function baz() {
  alert('foo.baz');
});

2. 在代码执行阶段通过条件语句进行创建FE,不会污染变量对象VO。

var foo = 10;
 
var bar = (foo % 2 == 0
  ? function () { alert(0); }
  : function () { alert(1); }
);
 
bar(); // 0

3. 使用表达式创建闭包

var foo = {};
 
(function initialize() {
 
  var x = 10;
 
  foo.bar = function () {
    alert(x);
  };
 
})();
 
foo.bar(); // 10;
 
alert(x); // "x" 未定义

initialize 的内部变量 “x” 在外部不能直接访问的, 但是通过 foo.bar (通过 [[Scope]] 属性)就可以访问得到

# 命名函数表达式

命名函数表达式就是带有名字的表达式,如 var bar = function foo(){}

但有一点需要记住:这个名字(上例中的 foo )只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效:

var f = function foo(){
  return typeof foo; // foo是在内部作用域内有效
};
// foo在外部是不可见的
typeof foo; // "undefined", 如果使用 console.log(foo),就会直接报错了
f(); // "function"

# 函数语句

如果函数声明在块中如 if , while 等语句中,这些就是函数语句, 在块中声明的函数跟正常的函数表现也有点区别

函数语句不是在变量初始化期间声明的,而是在运行时声明的

// 此刻,foo还没用声明
console.log(fofo); // "undefined"
if (true) {
  // 进入这里以后,foo就被声明在整个作用域内了
  function fofo(){ return 1; }
}
else {
  // 从来不会走到这里,所以这里的foo也不会被声明
  function fofo(){ return 2; }
}
console.log(fofo); // "ƒ fofo(){ return 1; }"

# 通过函数构造器创建的函数

既然这种函数对象也有自己的特色,我们将它与函数声明和函数表达式区分开来。其主要特点在于这种函数的 [[Scope]] 属性仅包含全局对象:

var x = 10;
 
function foo() {
 
  var x = 20;
  var y = 30;
 
  var bar = new Function('alert(x); alert(y);');
 
  bar(); // 10, "y" 未定义
 
}

我们看到,函数 bar[[Scope]] 属性不包含 foo 上下文的 Ao ——变量 ”y” 不能访问,变量 ”x” 从全局对象中取得。顺便提醒一句,Function 构造器既可使用new 关键字也可以没有

# 立即调用的函数表达式

立即调用的函数表达式即函数定义之后就直接执行函数,也叫自执行函数

当你声明类似 function foo(){}var foo = function(){} 函数的时候,通过在后面加个括弧就可以实现自执行,但不是像下面这么使用

function(){ /* code */ }(); // Uncaught SyntaxError: Function statements require a function name

上面代码会出错,是因为在解析器解析全局的 function 或者 function 内部 function 关键字的时候,默认认为是函数声明,而不是函数表达式,如果你不显示告诉编译器,它默认会声明成一个缺少名字的 function ,并且抛出一个语法错误信息,因为 function 声明需要一个名字

那就算加个名字,也是会出错的

// 下面运行后还是出错
// 其实这个function在语法上是没问题的,出错的是 右边的 ()
 
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
// 上面代码 相当于
function foo(){ /* code */ };
(); // SyntaxError: Unexpected token )
// 但是如果你在括弧()里传入一个表达式,将不会有异常抛出
// 但是foo函数依然不会执行
function foo(){ /* code */ }(1); // (1)这只是一个分组操作符,不是函数调用!
 
// 因为它完全等价于下面这个代码,一个function声明后面,又声明了一个毫无关系的表达式: 
function foo(){ /* code */ };
(1);

// 直接在控制台输出 ()
() // 报错
(1) // 1

所以这是一个没有任何表达式的分组操作符错误,而不是函数声明错误,也就浊说此时在函数声明后面的 () 他是一个分组操作符,而不是一个函数调用所使用的圆括号

自执行函数表达式

要解决上述问题,我们只需要在函数外围添加一个大括弧就行了,因为 JavaScript 里括弧 () 里面不是语句而是表达示,所以在这一点上,解析器在解析 function 关键字的时候,会将相应的代码解析成 表达式(这里就是函数表达),而不是函数声明。

// 下面2个括弧()都会立即执行

(function () { /* code */ } ()); // 推荐使用这个
(function () { /* code */ })(); // 但是这个也是可以用的

所以能立即执行的函数本质是需要这个函数是函数表达式, 除了 () 能生成表达式,JS 的 &&异或逗号 等操作符等可以生成表达式

// 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的
// 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了

var i = function () { return 10; } ();
true && function () { /* code */ } ();
0, function () { /* code */ } ();

// 如果你不在意返回值,或者不怕难以阅读
// 你甚至可以在function前面加一元操作符号

!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();

// 还有一个情况,使用new关键字,也可以用,但我不确定它的效率
// http://twitter.com/kuvos/status/18209252090847232

new function () { /* code */ }
new function () { /* code */ } () // 如果需要传递参数,只需要加上括弧()

注意,下面一个立即执行的函数,周围的括号不是必须的,因为函数已经处在表达式的位置,解析器知道它处理的是在函数执行阶段应该被创建的FE,这样在函数创建后立即调用了函数

var foo = {
 
  bar: function (x) {
    return x % 2 != 0 ? 'yes' : 'no';
  }(1)
 
};
 
alert(foo.bar); // 'yes'

因此,”关于圆括号”问题完整的答案如下:当函数不在表达式的位置的时候,分组操作符圆括号是必须的——也就是手工将函数转化成FE。 如果解析器知道它处理的是FE,就没必要用圆括号。

# 用闭包保存状态

来个常见利用特循给DOM节点添加事件的例子:

var elems = document.getElementsByTagName('a');
for (var i = 0; i < elems.length; i++) {

    elems[i].addEventListener('click', function (e) {
        e.preventDefault();
        alert('I am link #' + i);
    }, 'false');

}

上面代码中,成功给 elems 节点都添加了点击事件,但是实际上每次 alert 输出的 i 总是最后一个,因为上面的循环中 i 是共享的, 所以循环内存的代码块使用的是同一个 i

接下使用闭包解决来这个问题

// 这个是可以用的,因为他在自执行函数表达式闭包内部
// i的值作为locked的索引存在,在循环执行结束以后,尽管最后i的值变成了a元素总数(例如10)
// 但闭包内部的lockedInIndex值是没有改变,因为他已经执行完毕了
// 所以当点击连接的时候,结果是正确的

var elems = document.getElementsByTagName('a');

for (var i = 0; i < elems.length; i++) {

    (function (lockedInIndex) {

        elems[i].addEventListener('click', function (e) {
            e.preventDefault();
            alert('I am link #' + lockedInIndex);
        }, 'false');

    })(i);

}

// 你也可以像下面这样应用,在处理函数那里使用自执行函数表达式
// 而不是在addEventListener外部
// 但是相对来说,上面的代码更具可读性

var elems = document.getElementsByTagName('a');

for (var i = 0; i < elems.length; i++) {

    elems[i].addEventListener('click', (function (lockedInIndex) {
        return function (e) {
            e.preventDefault();
            alert('I am link #' + lockedInIndex);
        };
    })(i), 'false');

}

使用闭包直接可以引用传入的这些参数, 闭包内相当于锁住传入的参数,所以利用自执行函数表达式可以有效地保存状态

# 最后的旁白:Module模式

自执行函数除了上面所说的锁住参数的功能以外,还有一个作用就是实现 Module 模式

// 创建一个立即调用的匿名函数表达式
// return一个变量,其中这个变量里包含你要暴露的东西
// 返回的这个变量将赋值给counter,而不是外面声明的function自身

var counter = (function () {
    var i = 0;

    return {
        get: function () {
            return i;
        },
        set: function (val) {
            i = val;
        },
        increment: function () {
            return ++i;
        }
    };
} ());

// counter是一个带有多个属性的对象,上面的代码对于属性的体现其实是方法

counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5

counter.i; // undefined 因为i不是返回对象的属性
i; // 引用错误: i 没有定义(因为i只存在于闭包)

# 总结

函数声明与函数表达式区别

定义函数的方法有函数表达式和函数声明两种,两者的区别 ECMA规范 只明确了一点:函数声明必须带有标示符(Identifier)(就是大家常说的函数名称),而函数表达式则可以省略这个标示符:

  • 函数声明: function 函数名称 (参数:可选){ 函数体 }

  • 函数表达式: function 函数名称(可选)(参数:可选){ 函数体 }

那如果这个函都带了函数名称呢,如何判断是函数声明还是函数表达式呢?其实 ECMAScript 实质是通过上下文来区分的,如果 function foo(){} 是作为表达式的一部分的话,那它就是一个函数表达式,如果 function foo(){} 被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明

函数声明存在变量提升

函数声明会在任何表达式被解析和求值之前先被解析和求值:

console.log(fn); // ƒ fn() {return 'Hello world!';}
function fn() {
  return 'Hello world!';
}
console.log(fn2); // undefined
var fn2 = function() {
  return 'Hello world!';
}

另外,还有一点需要提醒一下,函数声明在条件语句内虽然可以用,但是没有被标准化,也就是说不同的环境可能有不同的执行结果,所以这样情况下,最好使用函数表达式:

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();