资讯专栏INFORMATION COLUMN

深入理解JavaScript系列4:立即调用的函数表达式

roadtogeek / 2455人阅读

摘要:前言大家学的时候,经常遇到自执行匿名函数的代码,今天我们主要就来想想说一下自执行。其实,前面两个例子里的变量,也可以换成,因为和外面的不在一个作用于,所以不会出现问题,这也是匿名函数闭包的威力。

前言

大家学JavaScript的时候,经常遇到自执行匿名函数的代码,今天我们主要就来想想说一下自执行。

在详细了解这个之前,我们来谈了解一下“自执行”这个叫法,本文对这个功能的叫法也不一定完全对,主要是看个人如何理解,因为有的人说立即调用,有的人说自动执行,所以你完全可以按照你自己的理解来取一个名字,不过我听很多人都叫它为“自执行”,但作者后面说了很多,来说服大家称呼为“立即调用的函数表达式”。

本文英文原文地址:http://benalman.com/news/2010/11/immediately-invoked-function-expression/

什么是自执行?

在JavaScript里,任何function在执行的时候都会创建一个执行上下文,因为为function声明的变量和function有可能只在该function内部,这个上下文,在调用function的时候,提供了一种简单的方式来创建自由变量或私有子function。

// 由于该function里返回了另外一个function,其中这个function可以访问自由变量i
// 所有说,这个内部的function实际上是有权限可以调用内部的对象。
function makeCounter() {
    // 只能在makeCounter内部访问i
    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

alert(i); // 引用错误:i没有defind(因为i是存在于makeCounter内部)。

很多情况下,我们不需要makeCounter多个实例,甚至某些case下,我们也不需要显示的返回值,OK,往下看。

问题的核心

当你声明类似function foo(){}或var foo = function(){}函数的时候,通过在后面加个括弧就可以实现自执行,例如foo(),看代码:

// 因为想下面第一个声明的function可以在后面加一个括弧()就可以自己执行了,比如foo(),
// 因为foo仅仅是function() { /* code */ }这个表达式的一个引用

var foo = function(){ /* code */ }

// ...是不是意味着后面加个括弧都可以自动执行?

function(){ /* code */ }(); // SyntaxError: Unexpected token (
//

上述代码,如果运行,第2个代码会出错,因为在解析器解析全局的function或者function内部function关键字的时候,默认是认为function声明,而不是function表达式,如果你不显示告诉编译器,它默认会声明成一个缺少名字的function,并且抛出一个语法错误信息,因为function声明需要一个名字。

函数,括弧,语法错误(SyntaxError)

有趣的是,即便你为上面那个错误的代码加上一个名字,他也会提示语法错误,只不过和上面的原因不一样。在一个表达式后面加上括号(),该表达式会立即执行,但是在一个语句后面加上括号(),是完全不一样的意思,他的只是分组操作符。

// 下面这个function在语法上是没问题的,但是依然只是一个语句
// 加上括号()以后依然会报错,因为分组操作符需要包含表达式

function foo(){ /* code */ }(); // SyntaxError: Unexpected token )

// 但是如果你在括弧()里传入一个表达式,将不会有异常抛出
// 但是foo函数依然不会执行
function foo(){ /* code */ }(1);

// 因为它完全等价于下面这个代码,一个function声明后面,又声明了一个毫无关系的表达式: 
function foo(){ /* code */ }(1);

你可以访问ECMA-262-3 in detail. Chapter 5. Functions 获取进一步的信息。

自执行函数表达式

要解决上述问题,非常简单,我们只需要用大括弧将代码的代码全部括住就行了,因为JavaScript里括弧()里面不能包含语句,所以在这一点上,解析器在解析function关键字的时候,会将相应的代码解析成function表达式,而不是function声明。不明白的,可以看深入理解JavaScript系列2:揭秘命名函数表达式中的函数表达式和函数声明

// 下面2个括弧()都会立即执行

(function () { /* code */ } ()); // 推荐使用这个
(function () { /* code */ })(); // 但是这个也是可以用的

// 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的
// 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了
// 不过,请注意下一章节的内容解释

var i = function () { return 10; } ();
true && function () { /* code */ } ();
0, function () { /* code */ } ();

// 如果你不在意返回值,或者不怕难以阅读
// 你甚至可以在function前面加一元操作符号

!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();

// 还有一个情况,使用new关键字,也可以用,但我不确定它的效率
// http://twitter.com/kuvos/status/18209252090847232

new function () { /* code */ }
new function () { /* code */ } () // 如果需要传递参数,只需要加上括弧()

上面所说的括弧是消除歧义的,其实压根就没必要,因为括弧本来内部本来期望的就是函数表达式,但是我们依然用它,主要是为了方便开发人员阅读,当你让这些已经自动执行的表达式赋值给一个变量的时候,我们看到开头有括弧(,很快就能明白,而不需要将代码拉到最后看看到底有没有加括弧。

用闭包保存状态

和普通function执行的时候传参数一样,自执行的函数表达式也可以这么传参,因为闭包直接可以引用传入的这些参数,利用这些被lock住的传入参数,自执行函数表达式可以有效地保存状态。

下面是错误的使用:

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获得数值,所以说无论点击哪个连接,最终显示的都是I am link #10(如果有10个a元素的话)

下面是正确的使用:

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);

}

由于在自执行函数表达式闭包内部i的值作为locked的索引存在,在循环执行结束以后,尽管最后i的值变成了a元素总数(例如10)但闭包内部的lockedInIndex值是没有改变,因为他已经执行完毕了所以当点击连接的时候,结果是正确的。

或者你也可以像这样使用:

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");

}

上面的代码在处理函数那里使用自执行函数表达式,而不是在addEventListener外部,这样也可以达到locked的效果,但是前面的代码更具有可读性。

其实,前面两个例子里的lockedInIndex变量,也可以换成i,因为和外面的i不在一个作用于,所以不会出现问题,这也是匿名函数+闭包的威力。

自执行匿名函数和立即执行的函数表达式区别

在这篇文章中,我们一直叫自执行函数,确切的说是自执行匿名函数(Self-executing anonymous function),但英文原文作者一直倡议使用立即调用的函数表达式(Immediately-Invoked Function Expression)这一名称,作者又举了一堆例子来解释,好吧,我们来看看:

// 这是一个自执行的函数,函数内部执行自身,递归
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(); } ());

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

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

Module模式

在讲到这个立即调用的函数表达式的时候,我又想起来了Module模式,如果你还不熟悉这个模式,我们先来看看代码:

// 创建一个立即调用的匿名函数表达式
// return一个变量,其中这个变量里包含你要暴露的东西
// 返回的这个变量将赋值给counter,而不是外面声明的function自身

var counter = (function () {
    var i = 0;

    return {
        get: function () {
            return i;
        },
        set: function (val) {
            i = val;
        },
        increment: function () {
            return ++i;
        }
    };
} ());

// counter是一个带有多个属性的对象,上面的代码对于属性的体现其实是方法

counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5

counter.i; // undefined 因为i不是返回对象的属性
i; // 引用错误: i 没有定义(因为i只存在于闭包)

关于更多Module模式的介绍,请访问我的上一篇文章:深入理解JavaScript系列2:揭秘命名函数表达式

更多阅读

希望上面的一些例子,能让你对立即调用的函数表达(也就是我们所说的自执行函数)有所了解,如果你想了解更多关于function和Module模式的信息,请继续访问下面列出的网站:

ECMA-262-3 in detail. Chapter 5. Functions. - Dmitry A. Soshnikov

Functions and function scope - Mozilla Developer Network

Named function expressions - Juriy “kangax” Zaytsev

深入理解JavaScript系列3:全面解析Module模式 - hiyangguo

Closures explained with JavaScript - Nick Morgan

关于本文

本文转自TOM大叔的深入理解JavaScript系列

【深入理解JavaScript系列】文章,包括了原创,翻译,转载,整理等各类型文章,原文是TOM大叔的一个非常不错的专题,现将其重新整理发布。谢谢大叔。如果你觉得本文不错,请帮忙点个推荐,支持一把,感激不尽。

更多优秀文章欢迎关注我的专栏

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78428.html

相关文章

  • JavaScript-立即调用函数达式(IIFE)

    摘要:将匿名函数赋予一个变量,叫函数表达式,这是最常见的函数表达式语法形式。组成这是一个被称为自执行匿名函数的设计模式,主要包含两部分。 一、函数声明&函数表达式 1.1 函数声明 (函数语句) showImg(https://segmentfault.com/img/bVbbqvT?w=278&h=166); (1)使用 function 关键字声明一个函数,再指定一个函数名,叫函数声明。...

    XUI 评论0 收藏0
  • 一段关于JS中this应用奇葩代码引发思考

    摘要:,对外公开的接口。更需要注意立即执行函数,返回的是一个匿名函数,也是一个闭包,在这里一定要注意一个问题是在进入可执行上下文时创建的。三在方法中,注意如下代码省略代码的实际参数是一个自执行匿名函数,这个匿名函数接受了两个参数,但只返回了。 function DemoFunction(){ this.init = function(){ var func = (fu...

    AlanKeene 评论0 收藏0
  • 由setTimeout深入JavaScript执行环境异步机制

    摘要:图片转引自的演讲和两个定时器中回调的执行逻辑便是典型的机制。异步编程关于异步编程我的理解是,在执行环境所提供的异步机制之上,在应用编码层面上实现整体流程控制的异步风格。 问题背景 在一次开发任务中,需要实现如下一个饼状图动画,基于canvas进行绘图,但由于对于JS运行环境中异步机制的不了解,所以遇到了一个棘手的问题,始终无法解决,之后在与同事交流之后才恍然大悟。问题的根节在于经典的J...

    codeGoogle 评论0 收藏0
  • JavaScript立即执行函数

    摘要:函数名可以省略省略函数名的话该函数就成为了匿名函数被传入函数的参数的名称一个函数最多可以有个参数这些语句组成了函数的函数体。使用那我们通常为什么使用函数立即表达式呢,以及我如何使用呢通常情况下,只对匿名函数使用这种立即执行的函数表达式。 注:此文只在理解立即执行函数,不在所谓原创,文中大量引用阮一峰的JavaScript标准参考教程、MDN的JavaScript 参考文档和深入理解Ja...

    W4n9Hu1 评论0 收藏0
  • 深入理解javascript立即执行函数(function(){…})()

    摘要:要理解立即执行函数,需要先理解一些函数的基本概念。函数表达式使用关键字声明一个函数,但未给函数命名,最后将匿名函数赋予一个变量,叫函数表达式,这是最常见的函数表达式语法形式。 javascript和其他编程语言相比比较随意,所以javascript代码中充满各种奇葩的写法,有时雾里看花,当然,能理解各型各色的写法也是对javascript语言特性更进一步的深入理解。 ( functio...

    melody_lql 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<