自调用匿名函数的N种写法

作者: Nanayai | 来源:发表于2019-07-06 13:42 被阅读56次

    孔乙己自己知道不能和他们谈天,便只好向孩子说话。有一回对我说道,“你读过JavaScript高程么?”我略略点一点头。他说,“读过书,……我便考一考。自调用匿名函数,怎样写的?”我想,讨饭一样的人,也配考我么?便回过脸去,不再理会。孔乙己等了许久,很恳切的说道,“不能写罢?……我教给你,记着!这些写法应该记着。将来做架构师的时候,写代码要用。”我暗想我和架构师的等级还很远呢,而且我们架构师也从不写代码;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是两个括号加一个匿名函数么?”孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说,“对呀对呀!……自调用匿名函数有N样写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚用指蘸了酒,想在柜上写代码,见我毫不热心,便又叹了口气,显出极惋惜的样子。

    自调用匿名函数(这里指立即执行的匿名函数)是很常见的创建命名空间方法,在这篇文章我整理一下关于自调用匿名函数的相关知识:

    函数声明和函数表达式

    想知道自调用匿名函数的N种写法,必须先了解函数声明和函数表达式的概念,声明函数的时候,除了new Function构造函数方式,下面两种是我们常用的方式的语法定义:

    函数声明:function FunctionName(FormalParameterList) { FunctionBody }

    函数表达式:function [FunctionName](FormalParameterList) { FunctionBody }

    函数声明的典型格式:

    function functionName(arg1, arg2, ...){
        <!-- function body -->
    }
    

    函数表达式的典型格式:

    var variable=function(arg1, arg2, ...){
        <!-- function body -->
    }
    var  variable=function functionName(arg1, arg2, ...){
        <!-- function body -->
    }
    

    从语法的定义上看,这两者几乎是一模一样的(唯一的区别是函数表达式可以省略函数名称),那么就解释器而言,当遇到这个结构的语句时,判定为函数表达式还是函数声明呢?就javascript的语法而言,如果一条语句是以function关键字开始,那么这段会被判定为函数声明。而函数声明是不能被立即执行的,这无疑会导致语法的错误(SyntaxError),因此就必须有一个办法,使解析器可以将之识别为函数表达式

    函数声明立即执行报错:

    function runNow(){return "sss"}() //Uncaught SyntaxError: Unexpected token )
    

    解析器识别函数定义的条件是以function关键字开始 ,那么只要在function关键字的前面添加任何其他的元素,就会让一个函数声明语句变成了一个表达式

    所以,任何消除函数声明和函数表达式间歧义的方法,都可以被解析器正确识别。比如:

    var i = function(){return 10}();       // undefined
    1 && function(){return true}();        // true
    1, function(){alert('areyouok')}();      // undefined
    

    赋值,逻辑,甚至是逗号,各种操作符都可以告诉解析器,这个不是函数声明,它是个函数表达式。并且,对函数一元运算可以算的上是消除歧义最快的方式,感叹号只是其中之一,如果不在乎返回值,这些一元运算都是有效的

    !function(){alert('areyouok')}()        // true
    +function(){alert('areyouok')}()        // NaN
    -function(){alert('areyouok')}()        // NaN
    ~function(){alert('areyouok')}()        // -1
    

    甚至下面这些关键字,都能很好的工作:

    void function(){alert('areyouok')}()       // undefined
    new function(){alert('areyouok')}()        // Object
    delete function(){alert('areyouok')}()     // true
    

    最后,括号做的事情也是一样的,消除歧义才是它真正的工作,而不是把函数作为一个整体,所以无论括号括在声明上还是把整个函数都括在里面,都是合法的:

    (function(){alert('areyouok')})()        // undefined
    (function(){alert('areyouok')}())        // undefined
    

    既然有这么多种构建立即执行函数的方法,那他们的速度如何呢?在别的博客看到有人做了速度测试,结果如下:

    根据上图中的数据可以看出:new方法永远最慢,其它方法很多差距其实不大,传统的括号,在测试里表现始终很快,在大多数情况下比感叹号更快,所以平时我们常用的方式毫无问题,甚至可以说是最优的。加减号在chrome表现惊人,而且在其他浏览器中速度也不错。

    常用写法

    格式

    其实上文已经列举了十多种写法,那么我们平时该用哪一种写法呢,我们推荐

    (function(){
        //....
    }());
    

    为什么(function(){})() 不推荐呢,明明两种效果一模一样啊,那是为了预防一种少见的情况,别人拷贝了你的代码并放在他的代码下面,如果他的代码结尾处漏掉了分号,那就会报个让你一头雾水的错Uncaught TypeError: (intermediate value)(...) is not a function,为了预防万一,可以在我们的代码前加个分号:

    var foo=function(){
        //别人的代码
    }//注意这里没有用分号结尾
    
    //开始我们的代码
    ;(function(){
        //我们的代码。。
        alert('Hello!');
    })();
    

    我们推荐的写法就不会出现这种问题哈哈,需要注意的是不要忘记结尾的分号:

    (function() {
        console.log('test');
    }())   //上下文如此这般时,这里不加分号会报错
    (function() {
        console.log('test1');
    }())
    
    传参

    下面介绍一下常用的系统变量 传参选择,我们知道,jQuery2.1.0中是这样做的:

    (function(window, factory) {
        factory(window)
    }(typeof window !== "undefined" ? window : this, function() {
        return function() {
           //jQuery的调用
        }
    }))
    

    为什么要把window作为参数传入呢?答案是当我们这样做之后,window在函数内部就有了一个局部的引用,使用window时不需要将作用域链退回到顶部作用域,可以提高访问速度,我们在开发jQuery插件时,常常选择将jQuery,window,document 传入插件内部以期获得更好的性能。

    最后要传的参数就是undefined ,至于这个undefined,稍微有意思一点,在一些旧浏览器中可以修改undefined的值(这个行为在2009年的ECMAScript 5被修复了),为了得到没有被修改的undefined,我们并没有传递这个参数,但却在接收时接收了它,因为实际并没有传,所以undefined那个位置接收到的就是真实的undefined了:

    (function(window, document,undefined) {
        //...
    })(window, document);
    

    上述代码为最终格式。

    小用法:用自执行表达式保存状态

    利用自执行函数表达式锁住传入的参数,可以有效地保存状态。此处摘选自Ben Alman的博客: 原文出处

    // 这个代码是错误的,因为变量i从来就没背locked住
    // 相反,当循环执行以后,我们在点击的时候i才获得数值
    // 因为这个时候i操真正获得值
    // 所以说无论点击那个连接,最终显示的都是I am link #10(如果有10个a元素的话)
    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');
    }
    
    // 这个是可以用的,因为他在自执行函数表达式闭包内部
    // i的值作为locked的索引存在,在循环执行结束以后,尽管最后i的值变成了a元素总数(例如10)
    // 但闭包内部的lockedInIndex值是没有改变,因为他已经执行完毕了
    // 所以当点击连接的时候,结果是正确的
    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);
    }
    
    // 你也可以像下面这样应用,在处理函数那里使用自执行函数表达式
    // 而不是在addEventListener外部
    // 但是相对来说,上面的代码更具可读性
    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');
    }
    

    虽然本文中使用自调用匿名函数这一名称,但Ben Alman在博客中倡议使用立即调用的函数表达式(Immediately-Invoked Function Expression)这一名称,作者又举了一堆例子来解释,好吧,其实我是同意Ben Alman的叫法的,我们来看看:

    // 这是一个自执行的函数,函数内部执行自身,递归
    function foo() { foo(); }
    
    // 这是一个自执行的匿名函数,因为没有标示名称
    // 必须使用arguments.callee属性来执行自己
    var foo = function () { arguments.callee(); };
    
    // 这可能也是一个自执行的匿名函数,仅仅是foo标示名称引用它自身
    // 如果你将foo改变成其它的,你将得到一个used-to-self-execute匿名函数
    var foo = function () { foo(); };
    
    // 有些人叫这个是自执行的匿名函数(即便它不是),因为它没有调用自身,它只是立即执行而已。
    (function () { /* code */ } ());
    
    // 为函数表达式添加一个标示名称,可以方便Debug
    // 但一定命名了,这个函数就不再是匿名的了
    (function foo() { /* code */ } ());
    
    // 立即调用的函数表达式(IIFE)也可以自执行,不过可能不常用罢了
    (function () { arguments.callee(); } ());
    (function foo() { foo(); } ());
    
    // 另外,下面的代码在黑莓5里执行会出错,因为在一个命名的函数表达式里,他的名称是undefined
    // 呵呵,奇怪
    (function foo() { foo(); } ());
    

    希望这里的一些例子,可以让大家明白,什么叫自执行,什么叫立即调用。

    注:arguments.callee在ECMAScript 5 strict mode里被废弃了,所以在这个模式下,其实是不能用的。

    号外:函数的声明提前

    既然前面提到了函数的声明和函数表达式,整好在这里介绍下两者的区别:大家都知道声明提前的说法, 声明提前是函数声明和函数表达式的一个重要区别,对于我们进一步理解这两种函数定义方法有着重要的意义。

    var声明提前

    但是再说函数声明提前之前呢,有必要说一下var声明提前。先给出var声明提前的结论:

    变量在声明它们的脚本或函数中都是有定义的,变量声明语句会被提前到脚本或函数的顶部。但是,变量初始化的操作还是在原来var语句的位置执行,在声明语句之前变量的值是undefined。

    上面的结论中可以总结出三个简单的点:

    1. 变量声明会提前到函数的顶部;
    2. 只是声明被提前,初始化不提前,初始化还在原来初始化的位置进行初始化;
    3. 在声明之前变量的值是undefined。

    还是来例子实在:

    var handsome='handsome';
    function handsomeToUgly(){
        alert(handsome);
        var handsome='ugly';
        alert(handsome);
    }
    handsomeToUgly();
    

    正确的输出结果是:先输出undefined,然后输出ugly。

    而不是:先输出handsome,然后输出ugly。

    这里正是变量声明提前起到的作用。该handsome局部变量在整个函数体内都是有定义的,在函数体内的handsome变量覆盖了同名的handsome全局变量,因为变量声明提前,即var handsome被提前至函数的顶部,就是这个样子:

    var handsome='handsome';
    function handsomeToUgly(){
        var handsome; //声明提前,覆盖了全局变量,使函数体内的handsome变为undefined
        alert(handsome);
        var handsome='ugly';
        alert(handsome);
    }
    handsomeToUgly();
    

    所以说在alert(handsome)之前,已经有了var handsome声明,由上面提到的

    在声明之前变量的值是undefined

    所以第一个输出undefined

    又因为上面提到的:

    只是声明被提前,初始化不提前,初始化还在原来初始化的位置进行初始化

    所以第二个输出ugly

    函数声明提前

    接下俩我们结合var声明提前开始聊函数声明的声明提前

    函数声明的声明提前小伙伴们应该很熟悉,举个再熟悉不过的例子。

    sayTruth();<!-- 函数声明 -->
    function sayTruth(){
        alert('money is handsome.');
    }
    
    sayTruth();<!-- 函数表达式 -->
    var sayTruth=function(){
        alert('money is handsome.');
    }
    

    小伙伴们都知道,对于函数声明的函数定义方法,即上面的第一种函数调用方法是正确的,可以输出money is handsome.的真理,因为函数调用语句可以放在函数声明之前。而对于函数表达式的函数定义方法,即上面的第二种函数调用的方法是不能输出money is handsome.的正确结果的。

    结合上面的money is handsome.例子,函数声明提前的结论似乎很好理解,不就是在使用函数声明的函数定义方法的时候,函数调用可以放在任意位置嘛,这一点即为:

    函数声明提前的时候,函数声明和函数体均提前了。

    而且:

    函数声明是在预执行期执行的,就是说函数声明是在浏览器准备执行代码的时候执行的。因为函数声明在预执行期被执行,所以到了执行期,函数声明就不再执行。

    函数表达式为什么不能声明提前

    我们再说一点:为什么函数表达式不能像函数声明那样进行函数声明提前呢?

    我们上面说了var的声明提前,注意我上面提过的:

    只是声明被提前,初始化不提前,初始化还在原来初始化的位置进行初始化

    Ok,我们把函数表达式摆在这看看:

    var  variable=function(arg1, arg2, ...){
        <!-- function body -->
    }
    

    函数表达式就是把函数定义的方式写成表达式的方式,就是把一个函数对象赋值给一个变量,所以我们把函数表达式写成这个样子:

    var varible = 2333;
    

    看到这,也许小伙伴们会明白了,一个是把一个值赋值给一个变量,一个是把函数对象赋值给一个变量,两者在声明时都遵循var 声明规则,所以对于函数表达式,变量赋值是不会提前的,即function(arg1, arg2, ...){}是不会提前的,变量varible为undefined,所以函数表达式不能像函数声明那样进行函数声明提前。

    函数声明提前的实例分析

    还是那句话,还是例子来的实在:

    sayTruth();
    function sayTruth(){alert('handsome')};
    function sayTruth(){alert('ugly')};
    

    浏览器的输出结果是输出ugly,因为函数声明提前,所以函数声明会在代码执行前进行解析,执行顺序是这样的,先解析function sayTruth(){alert('handsome')},在解析function sayTruth(){alert('ugly')},覆盖了前面的函数声明,当我们调用sayTruth()函数的时候,也就是到了代码执行期间,声明会被忽略,所以自然会输出ugly。忘了的可以看上面说过的:

    函数声明是在预执行期执行的,就是说函数声明是在浏览器准备执行代码的时候执行的。因为函数声明在预执行期被执行,所以到了执行期,函数声明就不再执行了。

    不被遵守的函数声明规范

    ECMAScript规范中表示,函数声明语句可以出现在全局代码中,或者内嵌在其他函数中,但是不能出现在循环、条件判、或者try/finally以及with语句中。

    的确是这样,但是规定下发了,遵守不遵守就是另一回事了。JavaScript对于这条规范的实现并不是严格遵守的。

    以下的代码在Chrome和Firefox中的运行结果为:

    sayTruth() //Uncaught TypeError: sayTruth is not a function
    if(1) {
        sayTruth() //m is handsome
        function sayTruth() {
            alert('m is handsome')
        };
    }
    sayTruth() //m is handsome
    

    号外部分摘自myvin函数声明和函数表达式——函数声明的声明提前

    写在最后

    中秋过后,秋风是一天凉比一天,看看将近初冬;我整天的靠着火,也须穿上棉袄了。一天的下半天,没有一个项目,我正合了眼坐着。忽然间听得一个声音,“兄弟买挂么。”这声音虽然极低,却很耳熟。看时又全没有人。站起来向外一望,那孔乙己便在服务器下对了门槛坐着。他脸上黑而且瘦,已经不成样子;穿一件破夹袄,盘着两腿,下面垫一个蒲包,用草绳在肩上挂住;见了我,又说道,“兄弟买挂么。”项目经理也伸出头去,一面说,“孔乙己么?你还有十九个bug没改呢!”孔乙己很颓唐的仰面答道,“这……下回再改罢。这一回卖的是大罗金仙挂,比斗圣挂要好。”项目经理仍然同平常一样,笑着对他说,“孔乙己,你又写了bug了!”但他这回却不十分分辩,单说了一句“不要取笑!”“取笑?要是不写bug,怎么会打断腿?”孔乙己低声说道,“跌断,跌,跌……”他的眼色,很像恳求项目经理,不要再提。此时已经聚集了几个人,便和架构师都笑了。我温了酒,端出去,放在门槛上。他从破衣袋里摸出四文大钱,放在我手里,见他满手是泥,原来他便用这手走来的。不一会,他喝完酒,便又在旁人的说笑声中,坐着用这手慢慢走去了。

    至此,自调用匿名函数的知识点就交代的差不多了,本文完。


    相关文章

      网友评论

        本文标题:自调用匿名函数的N种写法

        本文链接:https://www.haomeiwen.com/subject/basphctx.html