定义
立即调用的匿名函数又被称作立即调用的函数表达式(IIFE, Immediately Invoked Function Expression)。
它类似于函数声明,但由于被包含在括号中,所以会被解释为**函数表达式**。
// IIFE
(function () {
console.log("I am IIFE");
})();
// w3c 标准建议使用
(function () {
console.log("I am IIFE");
}());
// ES6 箭头函数写法
(() => {
console.log("I am IIFE");
})();
如果以function开头,则会被识别为函数声明,函数声明是不能被被执行符号()执行的!
function (){
console.log('a'); // Uncaught SyntaxError:
// Function statements require a function name
}(); // 这个报错是因为函数声明一定要有函数名
function foo(){
console.log('a'); // Uncaught SyntaxError: Unexpected token ')'
}();
只要是函数表达式,就可以加执行符号达到立即执行的效果了。
也可以加上一元操作符将函数转成表达式 (不推荐使用)。
var bar = function(){
console.log('Hello World');
}();
// 加上操作符,编译器不再认为这是一个函数声明
+function(){
console.log(1);// 1
}();
-function(){
console.log(2);// 2
}();
!function(){
console.log(3);// 3
}()
,function(){
console.log(4);// 4
}();
很多人习惯在前面加**;**,就是为了避免两个立即执行函数连在一起时发生如下错误:以下代码只能输出一个a。
(function(){
console.log('a');
})()
(function(){
console.log('b');
})()
// Uncaught TypeError: (intermediate value)(...) is not a function
再补充一个奇怪的情况:
function foo(a) {
console.log("hello");
}(1);
这里不会报错,也不会输出,实际上这种写法会被解释成一个函数声明,还有一个无意义的表达式。也就是下面的样子:
// 函数声明
function foo() {
console.log("hello");
}
// 一个表达式
(1);
关于非匿名自执行函数
注意:立即执行函数也是可以加名字的,但是要注意,函数名只读。看下面这个例子
var b = 10;
(function b(){
b = 20;
console.log(b);
}())
在非严格模式下会输出**[Function b]**,在严格模式下会报错!Uncaught TypeError: Assignment to constant variable.
原因就在于匿名函数属于表达式的范畴,如果添加了名字,遵从具名函数表达式的规范。
函数表达式中函数的识别名是不需要的,有名称的函数表达式,就是**具名函数表达式 (Named function expressions, NFE)**,其函数的识别名,它的作用域是只在函数的主体内部。
var b = 0;
var foo = function b(){
b = 10; // 严格模式下改行报错
console.log(b); // [Function b]
console.log(window.b); // 0
}
foo();
console.log(b); // 0
应用
进行初始化
在ES6的let和const,可以用立即执行函数来模拟块级作用域,避免全局变量污染。
(function() {
var foo = "bar";
console.log(foo);
})();
foo; // ReferenceError: foo is not defined
类似的,有一些操作需要在页面加载完成立即执行,比如绑定事件、创建对象等,也需要一些临时的变量,但是之后不会再用到。这时候使用立即执行函数,将这些初始化代码包裹在其局部作用域中,就是个很好的方案。
下面这个例子在初始化时绑定监听事件,count变量不会被泄漏出去,而且点击事件也能正常运作。
(function() {
var count = 0;
document.body.addEventListener('click', function() {
console.log("hi", count++);
});
}());
模块化封装
使用一个立即执行函数创建的闭包,实现对象字面量创建对象的私有成员。以此来封装模块,暴露的接口成为公有方法以供调用,私有成员外部无法取得。
var myobj = (function () {
// 私有成员
var name = "my, oh my";
// 实现公有部分
return {
getName: function () {
return name;
}
};
}());
myobj.getName(); // "my, oh my"
其他
经典题
下面这道经典题目
for(var i = 0; i < 5; i++ ) {
setTimeout(function(){
console.log(i);
},1000)
}
网上很多解释是说是因为事件队列的原因,说因为回调函数被放到事件队列中,for循环执行完毕i的值已经变成了5,再执行5个console.log(i)所以会输出5个5。
开始我看到这种解释有些困惑,为什么换成let就能解决呢?如果说是因为事件循环的原因,那换成let,循环完毕i也变成5了。
之后我补充了作用域,作用域链,执行上下文等知识,才了解到这种现象是由于作用域造成的。
之所以使用let就可以得到期望结果,是由于let的块级作用域,每一轮循环都会有一个新的词法作作用域环境保存每一轮的**i**。
blockLexicalEnvironment = {
i: 0,
outer: <globalLexicalEnvironment>
}
blockLexicalEnvironment = {
i: 1,
outer: <globalLexicalEnvironment>
}
blockLexicalEnvironment = {
i: 2,
outer: <globalLexicalEnvironment>
}
......
之后执行console.log(i),由于当前作用域没有i,所以沿着作用域链找,即找到了他上一层的块级词法环境中的i。
所以使用这里使用立即执行函数也是同样的原理。
for(var i = 0; i < 5; i++ ) {
(function(j){
setTimeout(function(){
console.log(j);
},1000);
}(i))
}
在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的 作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
立即执行函数的递归
立即执行函数是如果不加名字,又想要自身递归调用怎么办呢?可以使用[arguments.callee](<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arguments/callee>),已在ES5严格模式中被废弃,了解原因可以看这里。
callee是arguments对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。
const init = (function(n){
if(n==1){
return 1;
}else{
return n * arguments.callee(n-1);
}
}(10))
console.log(init); // 3628800
当然,可以使用具名函数直接调用,如下:
const init = (function a(n){
if(n==1){
return 1;
}else{
return n * a(n-1);
}
}(10))
console.log(init); // 3628800
关于变量提升
匿名函数属于函数表达式,创建执行上下文时不会被提升。
console.log(foo); // 正常输出
console.log(sayName); // Uncaught ReferenceError: sayName is not defined
(function sayName(name) {
console.log(name)
})('Millzie')
function foo() {
console.log("foo");
}
参考
- Understanding Scope and Scope Chain in JavaScript
- Kyle Simpson. 2014. You Don't Know JS: Scope & Closures (1st. ed.). O'Reilly Media, Inc.