函数表达式和函数声明

众所周知在JavaScritp中有两种方法来创建函数,但作为有着悠远历史的函数声明逐渐被函数表达式所取代。函数声明和函数表达式的定义是相当混乱的,其中ECMA只定义了函数声明必须有一个函数名以及函数表达式可以省略他们。但即使这样两种方法之间各自都有着自己独有的优势,在不同的情景下都有其存在的必要。

区分函数声明和函数表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//函数声明
function declaration(){
}

//函数表达式
new function expressions(){}


//函数表达式
var expressions = function(){
}

//函数声明
(function(){
function declaration(){}
}());

看了以上的代码是不是感觉到十分疑惑,他们之间看上去完全一样。那么他们之间又是怎么定义函数声明和函数表达式的呢?在此看来ECMAScript是基于上下文来进行区分的。如果function foo(){}作为赋值表达式的一部分那么这段代码则被认为是函数表达式,同理当function foo(){}处于函数内部或程序本身的最顶层则被认为是函数声明

1
(function foo(){}); //函数表达式

其中使用()括号括起的代码构成一个上下文的分组操作,里面包含赋值表达式。

函数声明

函数声明的方式中有一个很重要的特性就是:函数声明提升(function declaration hoisting)。函数声明提升意味着在执行代码之前,所有函数声明都会被优先解释声明。也就是说函数声明可以放在最后定义也无所谓。

1
2
3
4
5
6
7
//成功执行
inwoo();

//函数声明可以放在代码最后,但必须不在其他代码块下。
function inwoo(){
console.log('hi');
}

避免有条件创建函数声明

函数是可以被有条件地被声明的,也就是意味着:在一个IF判断语句里面可以嵌套着函数声明。但有的浏览器会将这种声明视为无条件的声明。无论判断是truefalse,函数都会被声明。

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
27
28
29
// 千万别这么做!!
// 某些浏览器会把函数foo声明为"first"
// 也有些浏览器会声明为"second"

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

函数声明理应只在程序本身和函数内部的顶级,在语法上他们不应出现在{}代码块内(函数内部除外),其中包括ifforwhile这些方法内。因为这些代码块内只是包含语句,而非源元素(即非顶级域)。

如果需要更多关于函数声明的资料可以访问 http://kangax.github.io/nfe/#function-statements,文中介绍了更多别开生面的声明方法和各个版本浏览器的一些Bug。

函数表达式的好处

根据使用习惯,函数表达式比函数声明更有优势。以下是函数表达式的一些实用场景。

  • 闭包
  • 作为其他函数的参数
  • 作为立即调用表达式(IIFE)

闭包的使用

当你想在执行一个函数前传递一些参数给该函数时,闭包此时就显得十分有用了。一个最好的例子就是当你想循环遍历一个节点列表时,闭包可以使得在函数执行的情况下保留其它参数如索引(index),以不至于丢失不见。

1
2
3
4
5
6
7
8
9
10
11
12
function tabsHandler(index) {
return function tabClickEvent(evt) {
//这时候index可以作为内部参数使用。
};
}

var tabs = document.querySelectorAll('.tab'),
i;

for (i = 0; i < tabs.length; i += 1) {
tabs[index].onclick = tabsHandler(i);
}

该事件处理会在稍后执行(点击事件),所以这里我们需要一个闭包来保留循环过程中的参数。

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
27
28
29
30
31
32
33
34
35
36
37
38
// Bad code
var i;

for (i = 0; i < list.length; i += 1) {
document.querySelector('#item' + i).onclick = function doSomething(evt) {
// 当方法执行时,无论点击哪个元素 i 都是 list.length。
}
}


// Bad code

var list = document.querySelectorAll('.item'),
i,
doSomething = function(evt) {
//如果使用这个方法,则没办法获得i在循环中的值。
};

for (i = 0; i < list.length; i += 1) {
item[i].onclick = doSomething;
}


// good code

var list = ['item1', 'item2', 'item3'],
i,
doSomethingHandler = function(itemIndex) {
return function doSomething(evt) {
//此时的索引作为参数itemIdenx保存在内部函数当中,
//所以调用时并不会存在以上问题。
console.log('Doing something with ' + list[itemIndex]);
};
};

for (i = 0; i < list.length; i += 1) {
list[i].onclick = doSomethingHandler(i);
}

下面两个Bad code的例子索引(index)的值都不是循环过程中产生的值,所以这里的解决办法是我们将循环过程时的索引传递到一个外部的函数以便传递到一个内部函数(此时作为内部函数该值不会再改变)。

作为参数传递

函数表达式可以作为参数传递到一个另一个函数中,而无需一个临时变量。最典型的例子就是jQuery的ready方法的匿名函数。

1
$(function(){});

同时我们在使用forEachmapreduce等方法时,函数表达式可以用来处理列表中每个对象,而且我们可以为函数命名或者采用匿名函数。为函数命名有一个好处就是可以清晰的知道该函数是处理什么样的业务。

1
2
3
4
5
var productIds = ['12356', '13771', '15492'];

productIds.forEach(function showProduct(productId) {
alert(productId);
});

立即调用的函数表达式(IIFE)

IIFE可以很好的帮助我们在编写JavaScritp代码时避免函数、变量产生全局的污染或冲突。所有的属性都被限制在一个匿名函数内进行定义。

一个最简单的IFFE

1
2
3
(function(){
//code
}());

当需要实现模块化的时候,IFFE可帮助简单地实现高可维护的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var myModule = (function() {
var privateMethod = function() {
console.log('A private method');
},
someMethod = function() {
console.log('A public method');
},
anotherMethod = function() {
console.log('Another public method');
};

return {
someMethod: someMethod,
anotherMethod: anotherMethod
};
}());

除此之外,IFFE还可以实现对代码的优化。如:我们需要判断浏览器内核获得一个特有的对象。

1
2
3
4
5
6
7
8
9
10
11
12
var getSpecObj = (function(){
var isWebkit = true;
if(isWebkit){
return function(){
//code
}
}else{
return function(){
//code
}
}
}());

总结

通过以上内容介绍,清晰了解到函数表达式和函数声明之间的差异,并且函数表达式能使更加清晰、简单并且维护性更高。函数表达式已经被广泛使用到各个库或代码片段当中。

参考