详解 JavaScript 的 IIFE 语法
在 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) {
// AMD
define(['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 / CommonJS
var 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 语法分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:http://bianchenghao.cn/9797.html