深入解析JavaScript中的立即执行函数
本文详细解析JavaScript中立即执行函数表达式(IIFE)的语法机制、作用域隔离原理及常见误区。通过对比函数声明与表达式的解析差异,阐明如何利用IIFE创建私有变量、避免全局污染,并结合闭包特性解决循环中的变量锁定问题,最后介绍基于IIFE的模块模式实现。
立即执行函数的基本概念
在 JavaScript 中,每次调用函数都会生成一个新的执行上下文。由于函数内部定义的变量和函数仅在内部可见,而无法被外部直接访问,这种机制提供了一种简便的方式来创建私有变量。
function makeCounter() {
var i = 0;
return function(){
console.log(++i);
};
}
//记住:`counter`和`counter2`都有他们自己的变量 `i`
var counter = makeCounter();
counter();//1
counter();//2
var counter2 = makeCounter();
counter2();//1
counter2();//2
i;//ReferenceError: i is not defined(它只存在于makeCounter里)
在许多场景下,你可能并不需要像 makeWhatever 这样的函数多次返回累加值,只需调用一次获取单一结果即可。在某些情况下,甚至不需要明确知晓返回值。
无论你将函数定义写为 function foo(){} 还是 var foo = function(){}, 调用时都必须在函数名后添加一对圆括号,例如 foo()。
函数声明与表达式的语法差异
//向下面这样定义的函数可以通过在函数名后加一对括号进行调用,像这样`foo()`,
//因为foo相对于函数表达式`function(){/* code */}`只是一个引用变量
var foo = function(){/* code */}
//那这可以说明函数表达式可以通过在其后加上一对括号自己调用自己吗?
function(){ /* code */}(); //SyntaxError: Unexpected token (
这里会捕获到一个错误。当圆括号出现在函数后面以尝试调用时,无论处于全局还是局部环境,JavaScript 引擎默认将 function 关键字视为函数声明而非函数表达式。如果你未明确指示圆括号将其识别为表达式,引擎会将其视为无名称的函数声明并抛出错误,因为函数声明必须包含名称。
问题1:此处可思考,是否可直接调用函数 var foo = function(){console.log(1)}(),答案是可以的。 问题2:进一步思考,函数声明后加圆括号直接调用会怎样?解答如下。
有趣的是,如果你为函数指定名称并在其后添加圆括号,同样会抛出错误,但原因不同。当圆括号位于函数表达式之后时,表明这是一个被调用的函数;而当圆括号位于声明之后时,意味着它与前面的函数声明完全分离,此时圆括号仅被视为控制运算优先级的普通括号。
//然而函数声明语法上是无效的,它仍然是一个声明,紧跟着的圆括号是无效的,因为圆括号里需要包含表达式
function foo(){ /* code */ }();//SyntaxError: Unexpected token
//现在,你把一个表达式放在圆括号里,没有抛出错误...,但是函数也并没有执行,因为:
function foo(){/* code */}(1)
//它等同于如下,一个函数声明跟着一个完全没有关系的表达式:
function foo(){/* code */}
(1);
修正语法错误的方法很简单。最流行且被广泛接受的做法是将函数声明包裹在圆括号中,以告知语法分析器这是一个函数表达式,因为在 JavaScript 中,圆括号内不能包含声明。因此,当圆括号包裹函数并遇到 function 关键词时,引擎会将其解析为函数表达式而非函数声明。请注意理解这里的圆括号与之前圆括号遇到函数时的表现不同。
当圆括号出现在匿名函数末尾以尝试调用函数时,它默认将函数视为函数声明。而当圆括号包裹函数时,它默认将函数作为表达式进行解析,而非函数声明。
IIFE 的语法修正与最佳实践
//这两种模式都可以被用来立即调用一个函数表达式,利用函数的执行来创造私有变量
(function(){/* code */}());//Crockford recommends this one,括号内的表达式代表函数立即调用表达式
(function(){/* code */})();//But this one works just as well,括号内的表达式代表函数表达式
// Because the point of the parens or coercing operators is to disambiguate
// between function expressions and function declarations, they can be
// omitted when the parser already expects an expression (but please see the
// "important note" below).
var i = function(){return 10;}();
true && function(){/*code*/}();
0,function(){}();
//如果你并不关心返回值,或者让你的代码尽可能的易读,你可以通过在你的函数前面带上一个一元操作符来存储字节
!function(){/* code */}();
~function(){/* code */}();
-function(){/* code */}();
+function(){/* code */}();
// Here's another variation, from @kuvos - I'm not sure of the performance
// implications, if any, of using the `new` keyword, but it works.
// http://twitter.com/kuvos/status/18209252090847232
new function(){ /* code */ }
new function(){ /* code */ }() // Only need parens if passing arguments
在某些情况下,当额外的、带有歧义的括号围绕在函数表达式周围时并非必要(因为此时括号已将其作为表达式表达),但当括号用于调用函数表达式时,这仍然是一个好习惯。
这样的括号指明函数表达式将被立即调用,并且变量将存储函数的结果,而非函数本身。当这是一个非常长的函数表达式时,这可以节省他人阅读你代码的时间,无需滚动到页面底部去查看该函数是否被调用。
作为一条规则,当你编写清晰明确的代码时,有必要阻止 JavaScript 抛出错误,同样也有必要阻止其他开发者对你抛出“WTFError!”。
就像通过名称调用函数时会传递参数一样,当函数表达式被立即调用时,参数也会被传递。一个立即调用的函数表达式可以用来锁定值并有效保存当前状态,因为任何定义在函数内部的函数都可以使用外部函数传递进来的参数和变量(这种关系被称为闭包)。
利用闭包保存状态
// 它的运行原理可能并不像你想的那样,因为`i`的值从来没有被锁定。
// 相反的,每个链接,当被点击时(循环已经被很好的执行完毕),因此会弹出所有元素的总数,
// 因为这是 `i` 此时的真实值。
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);
}
// 而像下面这样改写,便可以了,因为在IIFE里,`i`值被锁定在了`lockedInIndex`里。
// 在循环结束执行时,尽管`i`值的数值是所有元素的总和,但每一次函数表达式被调用时,
// IIFE 里的 `lockedInIndex` 值都是`i`传给它的值,所以当链接被点击时,正确的值被弹出。
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);
}
//你同样可以像下面这样使用IIFE,仅仅只用括号包括点击处理函数,并不包含整个`addEventListener`。
//无论用哪种方式,这两个例子都可以用IIFE将值锁定,不过我发现前面一个例子更可读
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);
}
请记住,在这最后两个例子中,lockedInIndex 可以毫无问题地访问 i,但作为函数参数使用不同的命名标识符可以使概念更容易被解释。
立即执行函数的一个显著优势是,即使它没有命名或为匿名函数,函数表达式也可以在没有使用标识符的情况下被立即调用,闭包也可以在没有当前变量污染的情况下被使用。
术语辨析:IIFE 与自执行匿名函数
自执行匿名函数(“Self-executing anonymous function”)存在什么问题?
你看到它已被多次提及,但它仍未得到清晰的解释。我建议将术语改为“Immediately-Invoked Function Expression”,即 IIFE,如果你喜欢缩写的话。
什么是 Immediately-Invoked Function Expression?它使一个被立即调用的函数表达式。就像引导你去调用的函数表达式。
我想 JavaScript 社区的成员应该可以在他们的文章或陈述中接受术语 Immediately-Invoked Function Expression 和 IIFE,因为我觉得这样更容易让概念被理解,并且术语“self-executing anonymous function”确实不够精确。
//下面是个自执行函数,递归的调用自己本身
function foo(){foo();};
//这是一个自执行匿名函数。因为它没有标识符,它必须是使用`arguments.callee`属性来调用它自己
var foo = function(){arguments.callee();};
//这也许算是一个自执行匿名函数,但是仅仅当`foo`标识符作为它的引用时,如果你将它换成用`foo`来调用同样可行
var foo = function(){foo();};
//有些人像这样叫'self-executing anonymous function'下面的函数,即使它不是自执行的,因为它并没有调用它自己。然后,它只是被立即调用了而已。
(function(){ /*code*/ }());
//为函数表达式增加标识符(也就是说创造一个命名函数)对我们的调试会有很大帮助。一旦命名,函数将不再匿名。
(function foo(){/* code */}());
//IIFEs同样也可以自执行,尽管,也许他不是最有用的模式
(function(){arguments.callee();}())
(function foo(){foo();}())
// One last thing to note: this will cause an error in BlackBerry 5, because
// inside a named function expression, that name is undefined. Awesome, huh?
(function foo(){ foo(); }());
希望上面的例子可以让你更清楚地知道术语“self-executing”是有些误导性的,因为它并不是执行自己的函数,尽管函数已经被执行。同样,匿名函数也没有必要特别指出,因为 Immediately Invoked Function Expression 既可以是命名函数也可以是匿名函数。
最后:模块模式。当我调用函数表达式时,如果我不至少一次提醒我自己关于模块模式,我便很可能会忽略它。如果你不熟悉 JavaScript 里的模块模式,它与我下面的例子很像,但返回值用对象代替了函数。
模块模式的应用
var counter = (function(){
var i = 0;
return {
get: function(){
return i;
},
set: function(val){
i = val;
},
increment: function(){
return ++i;
}
}
}());
counter.get();//0
counter.set(3);
counter.increment();//4
counter.increment();//5
conuter.i;//undefined (`i` is not a property of the returned object)
i;//ReferenceError: i is not defined (it only exists inside the closure)
模块模式方法不仅相当厉害而且简单。用极少的代码,你可以有效地利用与方法和属性相关的命名,在一个对象里组织全部的模块代码,既最小化了全局变量的污染,也创造了使用变量的机会。
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
JS使用constructor判断数据类型可靠吗
2026-11-05 13:49:41
