函数定义几乎是我们做的最多的一件事了,有时候我们以 function a(){} 来申明,有时候以 var a = function(){} 来申明,他们是不是一样呢?
比如我在写代码的过程中,经常写下如下代码,但其实就在刚刚,我才似乎终于懂得了一些下面的函数定义到底发生了什么(附注释):

1
2
3
4
5
6
7
8
9
10
(function () { // 函数表达式
'use strict'
angular.module('moduleA')
.controller('controllerA', ControllerA)

ControllerA.$inject = []
function ControllerA () { // 函数声明,变量提升
// the actual body
}
})() // 通过里面的(), 使函数体代码块被当做函数表达式执行,函数名可以忽略

函数声明

简单地说,我们直接在顶部,或函数内部直接定义的函数代码块就是函数声明:

1
2
3
function func () {
// func body
}

他有如下约定:

  1. 形如function <funcName> () {}, 里面的四个元素缺一不可
  2. 不属于表达式的一部分

第2点可能有些混淆,什么是表达式的一部分呢?我们以promise的调用为例:

1
2
3
4
5
var promise = new Promise(...)

promise.then(function success() {
//
}, function failed () {})

可以看到在第3行和第5行中,我们定义的函数方式与函数声明的要求一致,然而这里的函数定义就是做为表达式的一部分而存在的,因此不是函数定义。这里的表示式是什么概念呢?我们稍后会提到。

函数声明会直接申明一个可调用的函数,其函数名就是申明时的函数名。同时函数声明具有变量提升的效果,故而我们可以在函数声明的前面,直接调用该函数变量。函数声明还是比较单纯的。

函数表达式

在周爱民老师的《JAVASCRIPT语言精髓与编程实践》一书中,这样定义“表达式”和“程序”:
表达式:有运算符和操作符构成,并产生运算结果的语法结构。比如 1+2+3 就是一个表达式,他会返回6。
程序:程序是由语句构成的,语句则是由“;(分号,有时候可以省略)”分隔的句子或命令。在表达式后面加上”;”分隔符,则该表达式就是表达式语句。

简单地说,var func = function () {} 就是一个最常见的函数表达式,他与函数声明有如下区别:

  1. 他是整个“赋值表达式”一部分,而不是独立存在的。比如示例中,该function 及后面的语句,整体都是赋值运算符=中的,完成这整个赋值运算符的整个过程就是”赋值表达式“在发挥作用
  2. 他可以省略函数名,比如创建匿名函数

还记得我们前面Promise例子中的 function success(){}function failed(){} 吗?我们在这里的定义中也为他们加上了函数名 success 和 failed, 但是他们的实际意义其实就是在源码中给我们看的。。从JS效果上来说,他们与匿名函数没有任何区别,其函数名其实会被直接忽略。我们可以通过一个简单的例子求证:

1
2
3
4
5
6
7
var func1 = function func () {console.info('Here is the func body')}

// 输入: 'Here is the func body'
func1()

// 异常: Uncaught ReferenceError: func is not defined
func()

也就是说,我们虽然在定义时申明了 func 的变量,但是执行后并没有任何效果,其值被忽略,仍然被当做了匿名函数。

有趣的函数调用

无论是函数声明还是函数表达式,他们产生的函数都是用来被调用的,而且我们有多种调用函数的方式,如下:

下面主要内容来自周爱民老师的《JAVASCRIPT语言精髓与编程实践》一书,此书和常见的JavaScript书籍略有不同,作者以自己的方式讲述了JavaScript语句背后的原理,很值得一读!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 示例1 函数声明
function func1 () {

}
func1()

// 示例2 匿名函数,即函数表达式,通过引用来调用
var func2 = function () {

}
func2()

// 示例3 匿名函数,但没有引用(1)
(function () {

}())

// 示例4 匿名函数,但没有引用(2)。和文章开始的困惑示例一样
(function () {

})()

// 示例5 匿名函数,但没有引用(3)
void function () {

}()

示例1和示例2都是我们最常用的函数定义和调用方式,就不再细叙。我就按照书本里的内容直白地解释一下示例3、4、5。

在这之前,我们需要知道: ()当不用做函数调用时,也具有强制语句执行的效果,也就是可以把它包裹的内容当做表达式来执行。他也是一种运算符,书里称之为“优先级运算符”。

示例3

通过最外面的 () 强制执行了一次函数调用运算,该运算同时也执行了匿名函数(函数表达式)的定义,返回函数引用,故而后面的函数调用得以成功。

示例4

首先借助 () 使得匿名函数(函数表达式)执行,由于该表达式是括号内唯一的也是最后的一个表达式,因此其值得以返回(即匿名函数的引用), 然后后面的 () 作为函数调用的语法,通过引用调用了该匿名函数。

示例5

void 是JavaScript中特殊的一个运算符,他使得运算的对象返回undefined。例如 void 1; 也是合法的语句。作为运算符,他会使运算对象参与运算

运算符需要针对进行运算,因此JS引擎会把后面的语句当做表达式执行以获取值。将其执行的结果作为 void 的参数,void运算结束后,会返回 undefined。因此从某种角度来说,示例5和示例3类似,只是他们触发表达式执行的关键字不一样罢了。

在示例3、4、5我们总是不停地重复一个名词:表达式得以执行。 其实也就是使得里面的函数定义被当做”函数表达式“,并且得以执行,然后返回其引用。

为什么特别提到”使得里面的函数定义被当做函数表达式“呢?我们知道”函数表达式“和”函数声明“之间的区别,尤其是在都具备了函数名的时候,其实是取决于其是不是属于整个表达式的一部分,故而,如下的代码会不会正常调用呢?

1
2
3
function func(name) {
console.info('yes, the func again: ' + name)
}('the name')

答案是否定的,由于没有 ()void 的运算符,代码中的 function 定义部分,会被解析成函数声明,而不会返回引用;实际上解析后该代码块如下,函数声明和小括号后面都被加上了;

1
2
3
4
5
6
// 解析成函数声明
function func(name) {
console.info('yes, the func again: ' + name)
};
// 执行一次”优先级运算“并返回 'the name'
('the name');

如果在前面加上 void 关键字,那么就是我们期待的结果了,由于此时函数定义是表达式中的一部分,故没有被解析成函数声明。

总结

函数声明是一个很纯粹的函数声明方法,其函数名就是申明中的函数名,而且其申明必须是干干净净的。同时其申明具有变量提升的效果。

函数表达式, 比如匿名函数,或者任何作为表达式一部分的函数定义(此处特意与”函数声明“区别开来)
,都是函数表达式(好吧,其实是我目前自己这么理解的,还不能说得这么绝对==)。其被调用的方式,就是通过调用函数表达式的执行所返回的引用。

如果函数表达式中的函数设置了函数名,如 void function func (){}。里面的函数名 func , 其实只在源码中存在意义,编译后它没有意义,也不可以被引用到,即我们无法通过 func() 来调用该函数。

同时参考:深入理解JavaScript系列(2):揭秘命名函数表达式