本文解析JavaScript中立即执行函数(IIFE)的两种常见写法及其底层原理。通过对比函数声明与函数表达式,阐明括号、运算符如何消除解析歧义并触发执行。同时介绍利用IIFE构建私有作用域以避免全局变量污染的方法,结合jQuery源码实例说明其实际应用场景。
JavaScript立即执行函数概述
JavaScript语言风格相对灵活,代码中常出现各种非标准写法。深入理解这些多样化的语法结构,有助于更透彻地掌握JavaScript的语言特性。
JavaScript中立即执行函数(IIFE)主要有两种常见写法:( function(){…} )() 和 ( function (){…} () )。起初可能误以为这只是用括号包裹匿名函数后加括号调用的简单操作,其核心目的是实现定义后立即执行。然而,加括号的真实原因涉及更深层的语法机制。要真正理解这一概念,需先厘清函数的基本分类。
函数声明与表达式的区别
函数声明、函数表达式与匿名函数的区别
函数声明:使用 function 关键字定义函数并指定函数名,例如 function fnName () {…};。
函数表达式:使用 function 关键字定义函数但不命名,并将该匿名函数赋值给一个变量,例如 var fnName = function () {…};。这是最常见的函数定义形式之一。
匿名函数:直接使用 function 关键字定义函数而不命名,例如 function () {};。匿名函数属于函数表达式的一种,其作用广泛,包括赋值给变量、作为事件处理程序或创建闭包等。
函数声明与函数表达式的关键区别在于两点:首先,JavaScript引擎在解析代码时会执行“函数声明提升”(Function declaration Hoisting),即当前作用域内的函数声明会被提升到作用域顶部;而函数表达式必须等到引擎执行到该行时才会从上至下解析。其次,函数表达式后可以直接加括号立即调用,而函数声明不能直接这样调用,必须通过 fnName() 的形式。以下示例展示了两者的差异。
fnName();
function fnName(){
...
}
//正常,因为‘提升'了函数声明,函数调用可在函数声明之前
fnName();
var fnName=function(){
...
}
//报错,变量fnName还未保存对函数的引用,函数调用必须在函数表达式之后
var fnName=function(){
alert('Hello World');
}();
//函数表达式后面加括号,当javascript引擎解析到此处时能立即调用函数
function fnName(){
alert('Hello World');
}();
//不会报错,但是javascript引擎只解析函数声明,忽略后面的括号,函数声明不会被调用
function(){
console.log('Hello World');
}();
//语法错误,虽然匿名函数属于函数表达式,但是未进行赋值操作,
//所以javascript引擎将开头的function关键字当做函数声明,报错:要求需要一个函数名
立即执行函数的语法机制
理解上述基本概念后,重新审视 ( function(){…} )() 和 ( function (){…} () ) 这两种立即执行函数的写法。起初可能认为这只是括号包裹匿名函数后加括号调用,但关键在于:要在函数体后加括号实现立即调用,该函数必须是函数表达式,而不能是函数声明。
(function(a){
console.log(a); //firebug输出123,使用()运算符
})(123);
(function(a){
console.log(a); //firebug输出1234,使用()运算符
}(1234));
!function(a){
console.log(a); //firebug输出12345,使用!运算符
}(12345);
+function(a){
console.log(a); //firebug输出123456,使用+运算符
}(123456);
-function(a){
console.log(a); //firebug输出1234567,使用-运算符
}(1234567);
var fn=function(a){
console.log(a); //firebug输出12345678,使用=运算符
}(12345678)
从输出结果可见,在 function 前添加 !、+、- 甚至逗号等运算符,均可实现函数定义后立即执行的效果。这些运算符(如 ()、!、+、-、= 等)的作用是将函数声明转换为函数表达式,从而消除JavaScript引擎在识别函数表达式和函数声明时的歧义。引擎因此明确这是一个函数表达式,允许在末尾加括号并立即执行代码。
使用括号包裹是最安全的做法。因为 !、+、- 等运算符会与函数的返回值进行运算,这可能导致不必要的副作用或错误。
利用IIFE构建私有作用域
那么,这种写法究竟有何实际用途?
JavaScript中没有原生的私有作用域概念。在多人协作的项目中,若在全局或局部作用域中声明变量,可能被其他开发者意外覆盖同名变量。利用JavaScript函数作用域链的特性,可以通过IIFE模仿一个私有作用域。匿名函数充当“容器”,内部可以访问外部变量,而外部环境无法访问容器内部的变量。因此,( function(){…} )() 内部定义的变量不会与外部变量冲突,这种模式俗称“匿名包裹器”或“命名空间”。
jQuery库便采用了这种方法。它将jQuery代码包裹在 ( function (window,undefined){…jquery代码…} (window) 中,从而在全局作用域调用jQuery代码时,有效保护了jQuery内部变量不被外部污染。
本文基于个人理解整理,如有错误欢迎指正。文中观点参考了以下资料:
《javascript权威指南》、《javascript高级程序设计》