本文深入解析JavaScript中匿名函数的三种定义方式及自执行函数的原理。通过区分函数声明与表达式的预编译差异,阐明自执行机制。同时探讨闭包在减少全局变量、优化代码结构中的应用,并分析闭包导致的变量引用陷阱及内存泄露风险,提供多种解决方案。
JavaScript匿名函数基础
在JavaScript中,函数是一种极其灵活的对象类型。本文主要聚焦于讲解匿名函数的具体应用场景。所谓匿名函数,即指那些没有显式函数名称的函数定义。
函数的定义方式大致可以归纳为以下三种主要形式:
第一种方式属于最常规的定义方法。
function double(x){
return 2 * x;
}
第二种方式利用了Function构造函数,它将参数列表和函数体均作为字符串传入。这种方式不仅使用不便,而且存在安全隐患,因此通常不建议采用。
var double = new Function('x', 'return 2 * x;');
第三种方式如下所示:
var double = function(x) { return 2* x; }
需要注意的是,等号右侧的函数部分即为一个匿名函数。在创建该函数后,将其赋值给了变量square。
接下来探讨匿名函数的创建方法。
第一种创建方式即前文提到的定义square函数,这是最为常用的一种手段。
第二种创建方式如下:
(function(x, y){
alert(x + y);
})(2, 3);
此处创建了一个匿名函数(位于第一对括号内),第二对括号用于调用该匿名函数并传入参数。由于括号代表表达式,而表达式必然产生返回值,因此可以在其后追加一对括号以触发执行。
自执行匿名函数原理
关于自执行的匿名函数,其核心机制如下:
首先需要明确什么是自执行的匿名函数。
它通常表现为如下形式:(function {// code})();
针对这一结构,常存在以下疑问。
为什么形如(function {// code})();的代码可以顺利执行,而function {// code}();却会引发报错?
对此现象的分析如下。
(1) 首先需明确两者的本质区别:(function {// code})属于表达式,而function {// code}属于函数声明。(2) 其次,JavaScript具有“预编译”特性:在预编译阶段,解释器会处理函数声明,但会忽略表达式。(3) 当JavaScript执行到function() {//code}();时,由于function() {//code}在预编译阶段已被解释,引擎会跳过函数体尝试执行后面的();,从而导致报错;而当执行到(function {// code})();时,由于(function {// code})是表达式,引擎会对其求值并获取返回值,既然返回值是一个函数,遇到随后的();时便会正常执行。
此外,将函数转换为表达式的方法并非必须依赖分组操作符(),还可以使用void操作符、~操作符或!操作符等来实现。
例如:
!function(){
alert("另类的匿名函数自执行");
}();
闭包与代码优化
匿名函数与闭包之间存在着紧密联系。
闭包的英文术语为closure,这是JavaScript中至关重要的概念。利用闭包可以显著减少代码量,使代码结构更加清晰,其功能十分强大。
闭包的基本含义是函数的嵌套。内层函数能够访问外层函数的所有变量,即使外层函数已经执行完毕(这一特性涉及JavaScript的作用域链机制)。
function checkClosure(){
var str = 'rain-man';
setTimeout(
function(){ alert(str); } //这是一个匿名函数
, 2000);
}
checkClosure();
此例看似简易,深入剖析其执行流程却蕴含诸多知识点:checkClosure函数的执行是瞬时的(耗时或许仅为0.00001毫秒),在checkClosure的函数体内创建了一个变量str,在checkClosure执行完毕之后str并没有被释放,这是因为setTimeout内的匿名函数存在这对str的引用。待到2秒后函数体内的匿名函数被执行完毕,str才被释放。
利用闭包可以优化代码结构:
function forTimeout(x, y){
alert(x + y);
}
function delay(x , y , time){
setTimeout('forTimeout(' + x + ',' + y + ')' , time);
}
/**
* 上面的delay函数十分难以阅读,也不容易编写,但如果使用闭包就可以让代码更加清晰
* function delay(x , y , time){
* setTimeout(
* function(){
* forTimeout(x , y)
* }
* , time);
* }
*/
匿名函数的最大用途之一是创建闭包(这是JavaScript的语言特性之一),此外还可以用于构建命名空间,从而减少全局变量的使用。
var oEvent = {};
(function(){
var addEvent = function(){ /*代码的实现省略了*/ };
function removeEvent(){}
oEvent.addEvent = addEvent;
oEvent.removeEvent = removeEvent;
})();
在这段代码中,函数addEvent和removeEvent均为局部变量,但可以通过全局变量oEvent来访问它们。这种做法大幅减少了全局变量的使用,从而增强了网页的安全性。
若要使用此段代码,需按以下方式调用:
oEvent.addEvent(document.getElementById('box') , 'click' , function(){});
var rainman = (function(x , y){
return x + y;
})(2 , 3);
/**
* 也可以写成下面的形式,因为第一个括号只是帮助我们阅读,但是不推荐使用下面这种书写格式。
* var rainman = function(x , y){
* return x + y;
* }(2 , 3);
在此处创建了一个变量rainman,并通过直接调用匿名函数将其初始化为5。这种技巧在实际开发中十分实用。
var outer = null;
(function(){
var one = 1;
function inner (){
one += 1;
alert(one);
}
outer = inner;
})();
outer(); //2
outer(); //3
outer(); //4
代码中的变量one属于局部变量(定义在函数内部),因此外部无法直接访问。然而,我们创建了inner函数,inner函数可以访问变量one;随后将全局变量outer引用了inner,因此三次调用outer会依次弹出递增的结果。
闭包注意事项与内存管理
在使用闭包时,需注意以下事项。
1. 闭包允许内层函数引用父函数中的变量,但该变量存储的是最终值。
/**
* <body>
* <ul>
* <li>one</li>
* <li>two</li>
* <li>three</li>
* <li>one</li>
* </ul>
*/
var lists = document.getElementsByTagName('li');
for(var i = 0 , len = lists.length ; i < len ; i++){
lists[ i ].onmouseover = function(){
alert(i);
};
}
你会发现,当鼠标移过每一个<li>元素时,弹出的总是4,而非预期的元素下标。原因已在注意事项中说明(最终值)。具体解释如下:当mouseover事件调用监听函数时,首先在匿名函数(function(){ alert(i); })内部查找是否定义了i,结果未定义;因此向上查找,发现已定义且i的值为4(循环结束后的i值);因此每次弹出的都是4。
解决方法一如下:
var lists = document.getElementsByTagName('li');
for(var i = 0 , len = lists.length ; i < len ; i++){
(function(index){
lists[ index ].onmouseover = function(){
alert(index);
};
})(i);
}
解决方法二如下:
var lists = document.getElementsByTagName('li');
for(var i = 0, len = lists.length; i < len; i++){
lists[ i ].$$index = i; //通过在Dom元素上绑定$$index属性记录下标
lists[ i ].onmouseover = function(){
alert(this.$$index);
};
}
解决方法三如下:
function eventListener(list, index){
list.onmouseover = function(){
alert(index);
};
}
var lists = document.getElementsByTagName('li');
for(var i = 0 , len = lists.length ; i < len ; i++){
eventListener(lists[ i ] , i);
}
2. 内存泄露问题。
使用闭包极易导致浏览器内存泄露,严重时甚至可能导致浏览器挂死。