2.5 作为值的函数

到目前为止,我们已经介绍了JavaScript中作为主要作用域机制的函数。回忆一下典型的函数声明语法是怎样的:

        function foo() {
            // ..
        }

虽然从这个语法上看可能不是很明显,但foo基本上就是一个外层作用域中的一个变量,这个作用域赋予被声明函数一个引用。也就是说,这个函数本身是一个值,就像42或者[1,2,3]一样。

这个概念乍听起来可能很奇怪,你需要花点时间来理解它。不仅你可以向函数传入值(参数),函数本身也可以作为值赋给变量或者向其他函数传入,又或者从其他函数传出。

因此,应该将函数值视为一个表达式,与其他的值或者表达式类似。

考虑:

        var foo = function() {
            // ..
        };


        var x = function bar(){
            // ..
        };

第一个赋给变量foo的函数表达式被称为是匿名的,因为这个函数表达式没有名称。

第二个函数表达式是已命名的(bar),即使它的引用赋值给了变量x。虽然匿名函数表达式的使用仍然极为广泛,但通常更需要已命名函数表达式

要想获取更多信息,参见本系列中的《你不知道的JavaScript(上卷)》第一部分。

2.5.1 立即调用函数表达式

在前面的代码片段中,两个函数表达式都没有运行——如果加上foo()或者x(),那么就可以执行了。

还有另一种方法可以执行函数表达式,这种方法通常被称为立即调用函数表达式(immediately invoked function expression, IIFE):

        (function IIFE(){
            console.log( "Hello! " );
        })();
        // "Hello! "

(function IIFE(){ .. })函数表达式外面的( .. )就是JavaScript语法能够防止其成为普通函数声明的部分。

表达式最后的()(即 })();这一行)实际上就表示立即执行前面给出的函数表达式。

这看起来可能有点奇怪,但实际上并不像初看上去那么诡异。思考foo和这里的IIFE的类似之处:

        function foo() { .. }


        // foo函数引用表达式,然后()执行它
        foo();


        // IIFE函数表达式,然后()执行它
        (function IIFE(){ .. })();

正如你看到的,在运行()前列出(function IIFE(){ .. })本质上和执行()之前的foo是一样的;两种情况都是使用()执行了在它之前的函数引用。

因为IIFE就是一个函数,而且函数会创建新的变量作用域,所以使用IIFE的这种风格也常用于声明不会影响IIFE外代码的变量:

        var a = 42;


        (function IIFE(){
            var a = 10;
            console.log( a );    // 10
        })();


        console.log( a );        // 42

IIFE也可以有返回值:

        var x = (function IIFE(){
            return 42;
        })();


        x;  // 42

以上执行的IIFE命名函数返回了值42,并被赋给了x。

2.5.2 闭包

闭包是JavaScript中一个非常重要,且经常被误解的概念。这里不作深入介绍,可以参见本系列中的《你不知道的JavaScript(上卷)》第一部分。但我将会解释相关的几个要点,以帮助你理解一般概念。这将会是你JavaScript技巧集中最重要的技术之一。

你可以将闭包看作“记忆”并在函数运行完毕后继续访问这个函数作用域(其变量)的一种方法。

考虑:

        function makeAdder(x) {
            // 参数x是一个内层变量


            // 内层函数add()使用x,所以它外围有一个“闭包”
            function add(y) {
              return y + x;
            };


            return add;
        }

每次调用外层makeAdder(..)返回的、指向内层add(..)函数的引用能够记忆传入makeAdder(..)的x值。现在,我们来使用makeAdder(..):

        // plusOne获得指向内层add(..)的一个引用
        // 带有闭包的函数在外层makeAdder(..)的x参数上
        var plusOne = makeAdder( 1 );


        // plusTen获得指向内层add(..)的一个引用
        // 带有闭包的函数在外层makeAdder(..)的x参数上
        var plusTen = makeAdder( 10 );


        plusOne( 3 );       // 4 <--1 + 3
        plusOne( 41 );      // 42 <--1 + 41


        plusTen( 13 );      // 23 <--10 + 13

我们来详细说明一下这段代码是如何执行的。

(1) 调用makeAdder(1)时得到了内层add(..)的一个引用,它会将x记为1。我们将这个函数引用命名为plusOne()。

(2) 调用makeAdder(10)时得到了内层add(..)的另一个引用,它会将x记为10,我们将这个函数引用命名为plusTen()。

(3) 调用plusOne(3)时,它会向1(记住的x)加上3(内层y),从而得到结果4。

(4) 调用plusTen(13)时,它会向10(记住的x)加上13(内层y),从而得到结果23。

如果这在刚开始看上去很奇怪,也令人迷惑的话,不要着急!你需要大量实践才能完全理解这个过程。

不过相信我,一旦你理解了,它就会成为所有编程技术中最为强大有用的技术。它绝对值得你花费一些脑力去理解。在下一节中,我们将针对闭包进行更深入的实践。

模块

在JavaScript中,闭包最常见的应用是模块模式。模块允许你定义外部不可见的私有实现细节(变量、函数),同时也可以提供允许从外部访问的公开API。

考虑:

        function User(){
            var username, password;


            function doLogin(user, pw) {
              username = user;
              password = pw;


              // 执行剩下的登录工作
            }
            var publicAPI = {
              login: doLogin
            };


            return publicAPI;
        }
        // 创建一个User模块实例
        var fred = User();


        fred.login( "fred", "12Battery34! " );

函数User()用作外层作用域,持有变量username和password,以及内层的函数doLogin();这些都是这个User模块私有的内部细节,无法从外部访问。

我们有意没有调用new User(),尽管这事实上可能对多数读者来说更为熟悉。User()只是一个函数,而不是需要实例化的类,所以只是正常调用就可以了。使用new是不合适的,实际上也是浪费资源。

执行User()创建了User模块的一个实例,这创建了一个新的作用域,因而创建了所有内层变量/函数的一个新副本。我们将这个实例赋给fred。如果再次运行User(),那么会得到一个不同于fred的全新实例。

内层的函数doLogin()在username和password上有一个闭包,这意味着即使在User()函数运行完毕之后,函数doLogin()也保持着对它们的访问权。

publicAPI是带有一个属性/方法login的对象,login是对内层函数doLogin()的一个引用。当我们从User()返回publicAPI时,它就变成了我们命名为fred的那个实例。

此时,外层的函数User()已经运行完毕。我们通常认为像username和password这样的内层变量也就随之消失了。但上述示例并不会这样,因为login()函数的内部有一个可以使得它们依然保持活跃的闭包。

这就是我们可以调用fred.login(..)的原因,这等同于调用内层doLogin(..),并且fred. login(..)仍然可以访问内层变量username和password。

这里只是对闭包和模块模式的匆匆一瞥,它们的某些细节很可能还是有点令人迷惑。没关系!让你的大脑完全理解它还需要一些努力。

从现在开始,阅读本系列《你不知道的JavaScript(上卷)》第一部分,这有助于你更进一步地理解这些知识。