在javaScript中,每个函数被调用时,都会创建一个新的执行上下文。因为在一个函数里面定义的变量和函数只能在里面访问,在外面是不行的,上下文提供了一种很容易的方法来创建私有性。
//makeCounter函数返回另外一个匿名函数,这个匿名函数能够访问到“私有”变量i, 好像有一点“特权”性。 function makeCounter() { // i只能在makeCounter的里面被访问到 var i = 0; return function() { console.log( ++i ); }; } // 注意'counter'和'counter2'有他们自己的作用域'i' var counter = makeCounter(); counter(); // logs: 1 counter(); // logs: 2 var counter2 = makeCounter(); counter2(); // logs: 1 counter2(); // logs: 2 i; // ReferenceError: i没有被定义(它只存在于makeCounter的内部)
在很多情况下,你不需要函数返回多个“实例”,你仅仅需要一个单例,又或者在其他情况下,你可能连一个返回值都不需要。
问题的关键
现在你定义了像function foo(){}或var foo = function(){}这样的函数,这样你得到了一个函数的标识符,你可以通过()来调用它,如foo()。
//像这样定义的函数可以通过后面的()来调用,如foo(),因为foo仅仅是一个 //函数表达式'function(){/*...*/}'的引用 var foo = function(){ /* ... */ } //仅仅能过后面的() ,函数表达式就能被执行,这是不合常理的。 function(){ /* ... */ }(); // SyntaxError: Unexpected token (
正如你以上所看见的,出现了异常。当JS解释器在全局作用域或函数的里面遇到function关键字时,会默认为是一个函数声明,而不是一个函数表达式。如果你不确切地告诉解释器是一个表达式,它会认为是一个没有名字的函数声明,所以导致语法错误异常,因为函数声明是需要名字的。
顺便说一下:函数、括号、语法错误
有趣的是,如果你为函数指定了一个名字,并且把括号放在它后面,出于不同的原因,解析器也会抛出一个语法错误。当括号放在表达式的的后面意味着表达式是一个将被调用的函数,括号放在声明的后面,会与前面的声明完全分开,仅仅代表一个分组操作符(作为一种控制优先级的一种控制方式)。
<span style="font-weight: normal;">//这个函数在语法声明上是有效的,它仅仅是一个声明,紧跟着的括号是无效的, //因为分组操作符需要包括一个表达式 function foo(){ /* ... */ }(); // SyntaxError: Unexpected token ) //现在如果你在括号中放一个表达式,没有异常抛出 //但是函数仍然没有执行 function foo(){ /* ... */ }( 1 ); //其实上面的相当于以下两个:一个是函数声明,一个是不相关的表达式 function foo(){ /* ... */ } ( 1 )</span>
想要了解更多,请访问, ECMA-262-3 in detail. Chapter 5. Functions
立即调用表函数达式(IIFE)
幸运的是以上语法错误可以简单地被修复。最广泛的做法是告诉解析器函数表达式仅仅是用括号括起来的,因为在javaScript中,括号不能包括声明。根据这一点,当解析器遇到function关键字时,会把它解析成一个表达式而不是函数声明。
//以下两种都可以被用来立即执行一个函数表达式,用函数的执行上下文去创建 //私有性 (function(){ /* ... */ }()); // Crockford推荐使用这个 (function(){ /* ... */ })(); // 但是这一个也工作得很好 //从括号或强制操作符来看,消除了函数声明和函数表达式的歧义, //当解析器已经预期是一个表达式时,他们也可以被忽略,请看以下的例子 var i = function(){ return 10; }(); true && function(){ /* ...*/ }(); 0, function(){ /* ... */ }(); //如果你不关心函数的返回值或你的代码可读性 //你可以节省一个字节,通过在函数前面加一个一元操作符 !function(){ /* code */ }(); ~function(){ /* code */ }(); -function(){ /* code */ }(); +function(){ /* code */ }(); //还有另外的版本,但我不清楚它的实现性能会怎样,用'new'关键字实现, //运行是正常的 new function(){ /* code */ } new function(){ /* code */ }()
关于括号需要注意的重要点
特别在“消除歧义”的括号(即包着函数表达式的括号),是不需要的(因为解析器已经认为它是表达式了),对于用在赋值中还是一个不错的想法。
这样的括号很明显暗示了函数表达式可以立即被调用,并且这个变量将包括函数的返回结果,不是函数本身。这可以免去一些人阅读你代码的麻烦,因为如果你的函数声明很长时,可能要滚动到下面才知道你这个函数有没有被调用。
一般来说,想写出清晰的代码,从技术上说要阻止解析器抛出语法错误异常,编写清晰的代码也需要阻止其他开发者把错误异常抛向你。
用闭包来保存状态
通过传递参数来调用立即函数,调用具有名称标识的函数并同时向它传递参数,这两者的调用方式是很像的。任何定义在一个函数里面的函数都能访问它外面函数的所传递进来的参数以及外层函数的变量(这种关系被称为闭包),一个立即调用函数表达式可以被用来“锁住”值和保存状态值。
如果你想学习更多关于闭包的内容,请阅读Closures explained with JavaScript.
下面例2和例3会按照你期望的执行,立即执行函数表达式有一个很大的缺点是它是匿名或没有命名的。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <a href="###">点我</a> <a href="###">点我</a> <a href="###">点我</a> <a href="###">点我</a> <a href="###">点我</a> <script type="text/javascript"> var elems = document.getElementsByTagName( 'a' ); //例1 for ( var i = 0; i < elems.length; i++ ) { console.log(i); elems[ i ].addEventListener( 'click', function(e){ e.preventDefault(); alert( 'I am link #' + i ); }, 'false' ); } //执行结果: //I am link #5 //I am link #5 //I am link #5 //I am link #5 //I am link #5 //例2 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 ); } //执行结果: //I am link #0 //I am link #1 //I am link #2 //I am link #3 //I am link #4 //例3 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' ); } //执行结果: //I am link #0 //I am link #1 //I am link #2 //I am link #3 //I am link #4 </script> </body> </html>