# 函数
在 ECMAScript 中有三种函数类型:函数声明,函数表达式和函数构造器创建的函数
# 函数声明
格式:function 函数名称 (参数:可选){ 函数体 }
函数声明(缩写为FD)的特征:
有一个特定的名称
在源码中的位置:要么处于程序级(Pr函数表达式是出现在表达示的位置ogram level),要么处于其它函数的主体(FunctionBody)中
在进入上下文阶段创建
影响变量对象
function exampleFunc() {
...
}
函数声明的主要特点在于它对变量对象的影响,在代码执行阶段它们已经可用(因为FD在进入上下文阶段已经存在于VO中——代码执行之前)
例如(函数在其声明之前被调用)
foo();
function foo() {
alert('foo');
}
另外一个重点知识点是上述定义中的第二点——函数声明在源码中的位置:
// 函数可以在如下地方声明:
// 1) 直接在全局上下文中
function globalFD() {
// 2) 或者在一个函数的函数体内
function innerFD() {}
}
只有这2个位置可以声明函数,也就是说: 不可能在表达式位置或一个代码块中定义它
函数声明只能出现在程序或函数体内。从句法上讲,它们不能出现在
Block(块)({ ... })
中,例如不能出现在if
、while
或for
语句中。因为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
}
你可以会想到,在使用eval
对JSON
进行执行的时候,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();