详解 JavaScript 的 IIFE 语法

  • IIFE 语法
  • IIFE 语法的一些变体
    • 小括号去哪儿了?
    • 命名的 IIFE
    • 防止连接文件时出现问题
    • 使用箭头函数代替函数表达式
    • 一些不推荐的立即调用函数写法
  • 案例解析
    • 参数
    • 关于 undefined
    • Minifying
  • 非浏览器的全局环境

在 JavaScript 中,我们经常会遇到以下这种模式。这种模式被称之为 IIFE(Immediately-Invoked Function Expression),即立即调用的函数表达式:

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

IIFE 意味着该函数会在运行时立即被调用——我们也无法再次调用它们,它们只运行一次。

在很多时候,我们更多的是利用 IIFE 的函数作用域来防止局部变量泄漏到全局作用域(污染全局变量)。此外,我们也会使用 IIFE 来包装意图为私有的状态(或数据)。

但是,我们可能会奇怪为什么要以这种方式来编写 IIFE。毕竟,这种写法看上去有一点奇怪。接下来,让我们剖析 IIFE 语法并对其进行拆解说明。

IIFE 语法

IIFE 的核心是函数本身。它从 function 关键字开始直至右大括号:

function() {// ...
}

但是,这段代码本身并不是有效的 JavaScript 代码。当解析器在语句的开头看到 function 关键字时,它会认为这是函数声明。由于该函数没有名称,因此它不符合函数声明的语法规则。所以,解析失败,并且提示我们语法错误(Uncaught SyntaxError: Unexpected token ()。

这让我们得想办法让 JavaScript 引擎将上面的语句解析为函数表达式(function expression)而非函数声明(function declaration)。

实际上,诀窍很简单。我们可以通过将函数包装在小括号内来修复语法错误,从而产生以下代码:

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

一旦解析器遇到左括号后,它就会认为这是一个后面跟着一个右括号的表达式。与函数声明相反,函数表达式不必命名,因此上面(带括号的)函数表达式是一段有效的 JavaScript 代码。

现在,我们创建了一个函数表达式,但该函数永远不会执行,因为它从未被调用过,并且由于该函数没有被作任何分配,所以之后我们也无法再次获取它。然后,我们在上面函数表达式的结尾增加一对小括号来立即执行该函数表达式(更标准的写法还应该在最后加上一个分号):

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

至此为止,我们就得到了 IIFE。让我们再考虑一下 IIFE 这个名字,是不是只要把我们在上面拆分的两部分组合起来就是这个名字了:立即调用的函数表达式

IIFE 语法的一些变体

由于各种原因,IIFE 语法还存在一些变体。

小括号去哪儿了?

到目前为止,我们是把调用小括号立即放置在包装小括号的后面:

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

然而,有些人可能不喜欢这种将调用小括号悬挂在最后的写法,所以他们把调用小括号放置在包装小括号内:

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

上面两种写法都是完全正确的 IIFE 写法,并且语义也完全一样。平时,我们只要挑选自己喜欢的写法即可。

命名的 IIFE

被包装的函数是常规函数表达式,这意味着我们可以为它命名并将其转换为命名函数表达式。如果我们愿意,还可以这样写:

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

请注意,此时我们还是不能忽略函数周围的包装小括号。以下这段代码是无效的 JavaScript 代码:

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

虽然解析器现在可以成功解析函数声明,但是随后就是语法错误(Uncaught SyntaxError: Unexpected token ))。这是因为与函数表达式不同,函数声明不能​​立即调用。

防止连接文件时出现问题

有时,我们会看到在包装小括号前面还有一个前导分号的 IIFE:

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

这个防御分号的存在是为了防止将两个 JavaScript 文件连接在一起时可能出现的问题。假设,第一个文件包含以下代码:

var foo = bar

注意,这里没有分号来终止变量声明语句。如果第二个 JavaScript 文件包含没有前导分号的 IIFE,则连接结果如下:

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

这可能看起来像是将标识符 bar 分配给变量 foo,然后是 IIFE,但事实并非如此。相反,解析器会认为 bar 是一个函数,它将另一个函数作为其参数进行传递。我们可以删除 bar 之后的换行符,此时代码将显示得更加清晰:

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

前导分号可防止这种意料之外的函数调用:

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

即使前导分号之前没有任何其他代码,这也是一种语法正确的语言结构。在这种情况下,它将被解析为一个空语句,它根本不做任何事情,因此也不会有任何有害的事情发生。

JavaScript 的自动分号插入规则很棘手,很容易导致意外错误。所以,我们总是明确写出分号而不是依赖于自动插入分号。

使用箭头函数代替函数表达式

在 ECMAScript 2015 中,为 JavaScript 函数定义提供了箭头函数语法的扩展。与函数表达式一样,箭头函数是表达式,而非语句。如果我们想要,我们可以创建一个立即调用的箭头函数:

(() => {// ...
})();

请注意,此时我们还是不能忽略箭头函数周围的包装小括号。以下这段代码是无效的 JavaScript 代码:

() => {// ...
}();

虽然解析器现在可以成功解析箭头函数,但是随后就是语法错误(Uncaught SyntaxError: Unexpected token ()。

一些不推荐的立即调用函数写法

除了我们上面介绍的,还有一些其它技巧可以欺骗 JavaScript 使以下语句可以正常工作:

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

以下方法都可以迫使 JavaScript 解析器将字符 !+-~ 后面的语句作为表达式:

!function () {// ...
}();+function () {// ...
}();-function () {// ...
}();~function () {// ...
}();

但是,正常情况下,我们都不会这样去写 IIFE。我们只要了解即可,但不推荐自己去这样写。

案例解析

在写 JavaScript 插件时,我们经常是以类似如下的代码开始的:

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

下面我们开始对这段代码进行详细分析。

参数

我们可以将参数传递给我们的 IIFE:

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

我们知道 (window); 代表调用函数,并传入 window 对象。然后将其传递给函数,其中参数名也命名为 window

接下来,我们再传入 document 对象:

(function (window, document) {// 我们引用了常规的 `window` 和 `document` 对象
})(window, document);

关于 undefined

在 ECMAScript 3 中,undefined 是可变的。这意味着它的值可以被重新分配,例如 undefined = true;。值得庆幸的是, ECMAScript 5 严格模式('use strict';)解析器会抛出一个错误,告诉我们这是个白痴行为。在此之前,我们通过以下写法保护我们的 IIFE:

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

这意味着,就算某个脑袋短路了的家伙,做了以下的事情,对于我们的 IIFE 也不会有影响:

undefined = true;
(function (window, document, undefined) {// `undefined` 是 `undefined` 局部变量
})(window, document);

Minifying

Minifying 局部变量是 IIFE 模式一个很酷的地方。如果对象作为参数被传入,则我们可以将它们重命名为任意我们喜欢的名称。

改变:

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

为:

(function (a, b, c) {console.log(a); // Object window
})(window, document);

此时,windows 对象和 document 对象都被很好的缩小了。当然不止于此,我们还可以传入 jQuery 或者词法范围内的任何可用内容:

(function ($, window, document, undefined) {// 使用 `$` 引用 `jQuery`// $(document).addClass('test');
})(jQuery, window, document);(function (a, b, c, d) {// 变为// a(c).addClass('test');
})(jQuery, window, document);

这也意味着我们不需要调用 jQuery.noConflict(); 或者将 $ 之类的任何东西分配给模块。

而对于 undefined 重命名为 d,则显示更无关紧要。我们只需要知道引用的对象是未定义的,因为 undefined 没有特殊含义—— undefined 是 JavaScript 赋予已声明但没有值的东西的值。

非浏览器的全局环境

由于诸如 Node.js 之类的东西,浏览器并不总是全局对象,如果我们尝试创建跨多个环境工作的 IIFE,这可能会很痛苦。出于这个原因,我们倾向于坚持以下写法作为基础:

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

在浏览器中,全局环境 this 引用的是 window 对象。所以我们根本不需要传递 windows,我们总是可以将其简化为 this

我们通常使用 root 这个名称,因为它既可以指浏览器的根,也可以指非浏览器环境。

如果我们需要一个更通用的解决方案,尤其是在创建开源项目插件时,我们还可以使用 UMD 包装器。比如以下是一个 jQuery 插件的 UMD 包装器模板代码:

(function (root, factory) {if (typeof define === 'function' && define.amd) {// AMDdefine(['jquery'], function (jQuery) {if (!jQuery.fn) jQuery.fn = {}; // webpack 服务器渲染return factory(jQuery, root, root.document);});} else if (typeof module === 'object' && typeof module.exports) {// Node / Browserify / CommonJSvar jQuery = (typeof window != 'undefined') ? window.jQuery : undefined;if (!jQuery) {jQuery = require('jquery');if (!jQuery.fn) jQuery.fn = {};}module.exports = factory(jQuery, root, root.document);} else {// 浏览器全局对象root.MY_PLUGIN = factory(root.jQuery, root, root.document);}
}(this, function ($, window, document, undefined) {var MY_PLUGIN = function(){// ...};$.fn.MY_PLUGIN = MY_PLUGIN;return MY_PLUGIN;
}));

这很酷。该函数被传入到另一个函数中进行调用,然后我们可以为其分配相关的内部环境。在浏览器中,root.MY_PLUGIN = factory(root.jQuery, root, root.document); 是我们的 IIFE 模块,在如 Node.js 中,它将使用 module.exports,而在 requireJS 中又会被 typeof define === 'function' && define.amd 命中。

详解 JavaScript 的 IIFE 语法相关推荐

  1. window 程序报错 自动重启_好程序员web前端教程之详解JavaScript严格模式

    好程序员web前端教程之详解JavaScript严格模式,严格模式(Strict mode)是由ECMA-262规范定义的新兴JavaScript标准,发布于2009年12月第五版.旨在改善错误检查功 ...

  2. 详解 javascript中offsetleft属性的用法(转)

    详解 javascript中offsetleft属性的用法 转载  2015-11-11   投稿:mrr    我要评论 本章节通过代码实例介绍一下offsetleft属性的用法,需要的朋友可以做一 ...

  3. es6字符串添加html标签,JavaScript_详解JavaScript ES6中的模板字符串,在 ES6 中引入了一种新的字符 - phpStudy...

    详解JavaScript ES6中的模板字符串 在 ES6 中引入了一种新的字符串字面量 - 模板字符串,除了使用反引号 (`) 表示,它们看上去和普通的字符串没有什么区别.在最简单的情况下,他们就是 ...

  4. js模板字符串自定义类名_详解JavaScript ES6中的模板字符串

    这篇文章主要介绍了详解JavaScript ES6中的模板字符串,JS的ES6版本带来诸多简洁化方面的重大改进,需要的朋友可以参考下 在 ES6 中引入了一种新的字符串字面量 - 模板字符串,除了使用 ...

  5. 详解JavaScript变量类型判断及domReady原理 写得很好

    原文:详解JavaScript变量类型判断及domReady原理 我们知道,在开发JavaScript时候,经常要判断JavaScript变量类型,此 JavaScript教程 详细介绍JS变量的判断 ...

  6. 详解JavaScript数组过滤相同元素的5种方法

    详解JavaScript数组过滤相同元素的5种方法:https://www.jb51.net/article/114490.htm 转载于:https://www.cnblogs.com/bydzha ...

  7. 详解JavaScript对象深拷贝

    详解JavaScript对象深拷贝 在几乎所有编程语言中,对象都以引用形式保存给变量.复制给其他变量.JavaScript语言也是如此.因此简单的进行赋值操作进行复制仅仅是对对象数据的引用地址进行一个 ...

  8. 详解Javascript本地存储的方式、区别及应用场景

    详解Javascript本地存储的方式.区别及应用场景 一.方式 javaScript本地缓存的方法我们主要讲述以下四种: cookie sessionStorage localStorage ind ...

  9. javascript 本地对象和内置对象_详解 JavaScript 面向对象

    1. 概述 JavaScript面向对象比较难理解的点是类的继承.不管是es5写法还是es6写法,JavaScript继承的本质是原型链.具体可看我的上一篇文章: 田浩:详解原型.原型链.构造函.实例 ...

最新文章

  1. scrolling=no 无法根据坐标获取元素_汽车制造行业如何选择三坐标测量仪?
  2. 【图论】图,实现图(三种方式),二分图 详解
  3. 交通注意:叉车和自行车
  4. Eclipse的使用总结
  5. MyBatis-Plus工具快速入门使用
  6. 一位老程序员的心得分享,WEB前端菜鸟,感觉很迷茫,该怎么做?
  7. 再探结构体字节对齐问题
  8. 华为交换机查看发光收光
  9. 企业部署WAPI无线网络的技术详析
  10. 【增速】人工智能之计算机视觉工业领域落地一览
  11. android 模拟器 电脑配置,手机安卓模拟器多开对电脑配置要求与占用浅谈
  12. 请求支付宝渠道报错:40006,Insufficient Permissions,ISV权限不足
  13. Adobe Photoshop 2021 22.4.3 中英文+Neural Filters 含神经滤镜 下载
  14. dest是什么意思(dest是什么意思车上的)
  15. 浏览器 本地html 图片不显示,浏览网页图片无法显示怎么回事 网页图片显示不出来的解决方法...
  16. ps,pr,3Dmax软件使用经验
  17. 如何轻松搭建Java安卓开发环境
  18. vue项目引入vue-i18n,实现中英文切换
  19. 大文件分片上传前后端实现
  20. poj 1583 Choose Your Words Carefully

热门文章

  1. struct timespec 和 struct timeval
  2. 【191211产品工作】政务外网、专网、内网、互联网的区别
  3. 震惊!!某知名企业竟然使用这种方法装系统。
  4. html5 泡泡堂,HTML5 拖放 - 彩色泡泡糖的个人空间 - OSCHINA - 中文开源技术交流社区...
  5. linux 数据库导入命令
  6. python混合高斯分布_python 高斯混合模型
  7. 【择校】哈尔滨工业大学——电子与信息工程学院803考研
  8. 更改文件名spring boot项目起不来
  9. 梯度消失和梯度爆炸_梯度消失、爆炸的原因及解决办法
  10. jQuery jq事件绑定