理解JavaScript的立即调用函数表达式(IIFE)

首先这是js的一种函数调用写法,叫立即执行函数表达式(IIFE,即immediately-invoked function expression)。顾名思义IIFE可以让你的函数立即得到执行(废话)。

一般来说,IIFE有以下几种用途:

  1. 创建只使用一次的函数,并立即执行它。
  2. 创建闭包,保存状态,隔离作用域。
  3. 作为独立模块存在(例子如jQuery),防止命名冲突,命名空间注入(模块解耦)。

1. 创建只使用一次的函数,并立即执行它

创建只使用一次的函数比较好理解,在需要调用函数的地方使用IIFE,类似内联的效果:

1 (function(){
2     var a = 1, b = 2;
3     console.log(a+b); // 3
4 })();

还可以传入参数:

1 (function(c){
2     var a = 1, b = 2;
3     console.log(a+b+c); // 6
4 })(3);

IIFE比较常见的形式是匿名函数,但是也可以是命名的函数:

1 (function adder(a, b){
2     console.log(a+b); // 7
3 })(3, 4);

在js中应该尽量使用命名函数,因为匿名函数在堆栈跟踪的时候会造成一些不便。

2. 创建闭包,保存状态,隔离作用域

隔离作用域比较复杂一点,在ES6以前,JS没有块级作用域,只有函数作用域,作为一种对块级作用域的模拟就只能用function模拟一个作用域,比如如下代码:

 1 var myBomb = (function(){
 2     var bomb = "Atomic Bomb"
 3     return {
 4         get: function(){
 5             return bomb
 6         },
 7         set: function(val){
 8             bomb = val
 9         },
10     }
11 })()
12
13 console.log(myBomb.get()) // Atomic Bomb
14 myBomb.set("h-bomb")
15 console.log(myBomb.get()) // h-bomb
16
17 console.log(bomb) // ReferenceError: bomb is not defined
18 bomb = "none"
19 console.log(bomb) // none   

可以看到一个比较奇特的现象,按照常理,一个函数执行完毕,在它内部声明的变量都会被销毁,但是这里变量bomb却可以通过myBomb.get和myBomb.set去读写,但是从外部直接去读和写却不行,这是闭包造成的典型效果。

要清楚解释闭包到底是什么,这里有一篇文章学习Javascript闭包(Closure),上面的代码已经用到了闭包。所有闭包都有一个特点,就是可以通过导出方法从函数外部改变函数内部变量的值,因此可以利用这个特点来隔离作用域,模拟一种“私有”的效果。

举一个IIFE保存变量的例子,我们要写入三个文件,先定义了一个内容数组,然后用for循环遍历这个数组写入文件,最后依次用for循环的下标打印出"File i is written.":

 1 var fs = require(‘fs‘);
 2
 3 var fileContents = ["text1", "text2", "text3"];
 4 for (var i = 0; i < fileContents.length; i++) {
 5     fs.writeFile("file"+i+".txt", fileContents[i], function(err){
 6         if (err) {
 7             console.log(err)
 8         }
 9         console.log("File " + i + " is written.")
10     })
11 }    

这段代码结果是:

File 3 is written.
File 3 is written.
File 3 is written.

很明显和我们的意愿相违背,打印了3次"File 3 is written."。
我们希望的是每个文件的下标索引打印一次。

原因在于写文件是个异步操作,在写完文件调用回调函数时,for循环已经遍历完毕,此时i=3。
要解决这个问题,可以使用IIFE:

 1 var fs = require(‘fs‘);
 2
 3 var fileContents = ["text1", "text2", "text3"];
 4 for (var i = 0; i < fileContents.length; i++) {
 5     (function(index){
 6         var fileIndex = index;
 7         fs.writeFile("file"+fileIndex+".txt", fileContents[fileIndex], function(err){
 8             if (err) {
 9                 console.log(err)
10             }
11             console.log("File " + fileIndex + " is written.")
12         })
13     })(i)
14 }

这次结果是正确的(尽管不是按序,这不在我们考虑范围内):

File 1 is written.
File 2 is written.
File 0 is written.

可以看到这里用IIFE做了一个变量捕获,或者说保存。

再回到myBomb那个例子,这其中用到了一个模式,叫Module模式,很多js模块都是这么写,在IIFE中定义一些私有变量或者私有函数,然后在return的时候导出(一般用一个Object导出)需要暴露给外部的方法。另外在IIFE中定义的变量和函数也不会污染全局作用域,它们都通过统一的入口访问。

3. 作为独立模块存在,防止命名冲突,命名空间注入(模块解耦)

可以使用以下代码为ns这个命名空间注入变量和方法:

 1 var ns = ns || {};
 2
 3 (function (ns){
 4     ns.name = ‘Tom‘;
 5     ns.greet = function(){
 6     console.log(‘hello!‘);
 7 }
 8 })(ns);
 9
10 console.log(ns); // { name: ‘Tom‘, greet: [Function] }

还可以扩展到更多的用途:

 1 (function (ns, undefined){
 2     var salary = 5000; // 私有属性
 3     ns.name = ‘Tom‘; // 公有属性
 4     ns.greet = function(){ // 公有方法
 5         console.log(‘hello!‘);
 6     }
 7
 8     ns.externalEcho = function(msg){
 9         console.log(‘external echo: ‘ + msg);
10         insideEcho(msg);
11     }
12
13     function insideEcho(msg){ // 私有方法
14         console.log(‘inside echo: ‘ + msg);
15     }
16 })(window.ns = window.ns || {});
17
18 console.log(ns.name); // Tom
19 ns.greet(); // hello
20 ns.age = 25;
21 console.log(ns.age); // 25
22 console.log(ns.salary); // undefined
23 ns.externalEcho(‘JavaScript‘); // external echo: JavaScript/inside echo: JavaScript
24 insideEcho(‘JavaScript‘); // Uncaught ReferenceError: insideEcho is not defined
25 ns.insideEcho(‘JavaScript‘); // Uncaught TypeError: ns.insideEcho is not a function

在这里,命名空间可以在局部被修改而不重写函数外面的上下文,起到了防止命名冲突的作用。

注(如果不感兴趣可以直接忽略):还需要解释一下上面IIFE中第二个参数undefined。在js中,undefined表示值的空缺,是预定义的全局变量,它并不是关键字:

1 console.log(typeof a); // undefined
2 var a;
3 console.log(a); // undefined

undefined有多重含义,第一种是一个数据类型叫做undefined,另一种是表示undefined这个数据类型中的唯一值undefined。我们在js代码中看到的undefined一般是全局对象的一个属性,该属性的初始值就是undefined,另一种情况是,这个undefined是个局部变量,和普通变量一样,它的值可以是undefined,也可以是别的。

在ECMAScript 3中undefined是可变的,这意味着你可以给undefined赋值,但在ECMAScript 5标准下,无法修改全局的undefined:

1 console.log(window.undefined); // undefined
2 window.undefined = 1;
3 console.log(window.undefined); // undefined

严格模式下则会直接报错:

1 ‘use strict‘
2
3 console.log(window.undefined); // undefined
4 window.undefined = 1;
5 console.log(window.undefined); // Uncaught TypeError: Cannot assign to read only property ‘undefined‘ of object ‘#<Window>‘

因此我们需要保护这个局部的undefined:

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

这时候就算有人给undefined赋值也没有问题:

1 undefined = true;
2 (function (window, document, undefined) {
3     // undefined指向的还是一个本地的undefined变量
4 })(window, document);

不过随着ECMAScript 5的普及(现在几乎没有哪款浏览器不支持ECMAScript 5了),这种担忧基本没有必要了,jQuery也是为了最大程度的兼容性才这么做。

以上例子说明我们可以把命名空间作为参数传给IIFE,以对其进行扩展和装饰:

 1 (function (ns, undefined){
 2     var salary = 5000; // 私有属性
 3     ns.name = ‘Tom‘; // 公有属性
 4     ns.greet = function(){ // 公有方法
 5         console.log(‘hello!‘);
 6     }
 7
 8     ns.externalEcho = function(msg){
 9         console.log(‘external echo: ‘ + msg);
10         insideEcho(msg);
11     }
12
13     function insideEcho(msg){
14         console.log(‘inside echo: ‘ + msg);
15     }
16 })(window.ns = window.ns || {});
17
18 (function (ns, undefined){
19     ns.talk = function(){
20         console.log(ns.name + ‘ says hello.‘);
21         console.log(ns.name + ‘ says goodbye.‘);
22         // 注意这里不能调用私有函数insideEcho,否则会报错,因为talk和insideEcho不在同一个闭包中
23     }
24 })(window.ns = window.ns || {});
25
26 ns.talk(); // Tom says hello. Tom says goodbye.

命名空间注入

命名空间注入是IIFE作为命名空间的装饰器和扩展器的一个变体,使其更具有通用性。作用是可以在一个IIFE(这里可以把它理解成一个函数包装器)内部为一个特定的命名空间注入变量/属性和方法,并且在内部使用this指向该命名空间:

 1 var app = app || {};
 2 app.view = {};
 3
 4 (function (){
 5     var name = ‘main‘;
 6     this.getName = function(){
 7         return name;
 8     }
 9     this.setName = function(newName){
10         name = newName;
11     }
12     this.tabs = {};
13 }).apply(app.view);
14
15
16 (function (){
17     var selectedIndex = 0;
18     this.getSelectedIndex = function(){
19         return selectedIndex;
20     }
21     this.setSelectedIndex = function(index){
22         selectedIndex = index;
23     }
24 }).apply(app.view.tabs);
25
26 console.log(app.view.getName()); // main
27 console.log(app.view.tabs.getSelectedIndex()); // 0
28 app.view.tabs.setSelectedIndex(1);
29 console.log(app.view.tabs.getSelectedIndex()); // 1

我们还可以写一个模块构造器来批量生产模块:

 1 var ns1 = ns1 || {}, ns2 = ns2 || {};
 2
 3 var creator = function(val){
 4     var val = val || 0;
 5     this.getVal = function(){
 6         return val;
 7     }
 8     this.increase = function(){
 9         val += 1;
10     }
11     this.reduce = function(){
12         val -= 1;
13     }
14     this.reset = function(){
15         val = 0;
16     }
17 }
18
19 creator.call(ns1);
20 creator.call(ns2, 100);
21 console.log(ns1.getVal()); // 0
22 ns1.increase();
23 console.log(ns1.getVal()); // 1
24 console.log(ns2.getVal()); // 100

对某个私有变量,用API的形式对其进行读写,这其实就是OOP的一些思想在js的应用了。

时间: 2024-10-15 13:31:12

理解JavaScript的立即调用函数表达式(IIFE)的相关文章

【javaScript基础】立即调用函数表达式

在javaScript中,每个函数被调用时,都会创建一个新的执行上下文.因为在一个函数里面定义的变量和函数只能在里面访问,在外面是不行的,上下文提供了一种很容易的方法来创建私有性. //makeCounter函数返回另外一个匿名函数,这个匿名函数能够访问到"私有"变量i, 好像有一点"特权"性. function makeCounter() { // i只能在makeCounter的里面被访问到 var i = 0; return function() { cons

【javaScript基础】马上调用函数表达式

在javaScript中,每一个函数被调用时,都会创建一个新的运行上下文.由于在一个函数里面定义的变量和函数仅仅能在里面訪问.在外面是不行的.上下文提供了一种非常easy的方法来创建私有性. //makeCounter函数返回另外一个匿名函数,这个匿名函数可以訪问到"私有"变量i, 好像有一点"特权"性. function makeCounter() { // i仅仅能在makeCounter的里面被訪问到 var i = 0; return function()

理解javascript中的回调函数(callback)【转】

在JavaScrip中,function是内置的类对象,也就是说它是一种类型的对象,可以和其它String.Array.Number.Object类的对象一样用于内置对象的管理.因为function实际上是一种对象,它可以"存储在变量中,通过参数传递给(别一个)函数(function),在函数内部创建,从函数中返回结果值". 因为function是内置对象,我们可以将它作为参数传递给另一个函数,延迟到函数中执行,甚至执行后将它返回.这是在JavaScript中使用回调函数的精髓.本篇文

函数声明、 函数表达式 与立即调用函数表达式的比较

函数声明 函数声明创建将来代码调用的函数.函数可以在声明之前的位置被调用.代码样例如下: //可以在声明之前的位置被调用 var size=area(3,6); function area(width,height){ return width*height; }; //可以在声明之后的位置被调用 var size2=area(2,4); 函数表达式   将函数放在本该表达式待的位置,这称为函数表达式.在函数表达式中,经常使用匿名函数.代码样例如下 var area=function(width

JavaScript 编码规范 之 函数表达式

! function () { var a = function () // 赋值一个函数表达式 { } , b = function () // b c 效果一样 都是返回一个拥有闭包的函数 { var time = +new Date ; return function () { ; console.log( time ) } }() , c = ( function () // 加上圆括号的原因 1 表示是立即执行 2 拥有闭包元素 { var time = +new Date ; ret

Effective JavaScript Item 13 使用即时调用的函数表达式(IIFE)来创建局部域

本系列作为Effective JavaScript的读书笔记. 所谓的即时调用的函数表达式,这个翻译也许不太准确,它对应的英文原文是Immediately Invoked Function Expression (IIFE).下文也使用IIFE来表达这一概念. 首先看一个程序: function wrapElements(a) { var result = [], i, n; for (i = 0, n = a.length; i < n; i++) { result[i] = function

立即调用的函数表达式---IIFE

有些人则称为“自执行的匿名函数” 在闭包中,我们经常需要使用到匿名函数,我感觉闭包就是一种匿名函数,子集. 但是直接在匿名函数后面调用函数是会出错的.比如: function () { alert("Hello IIFE"); }();// Uncaught SyntaxError: Unexpected token ( 我们期望系统立即调用这个匿名函数,但是系统会以为我们是在进行函数声明,进行函数声明需要有函数名称.而上面则没有,就会报错. 那么我们为它添加函数名咯,还会有一个小问题

浅谈javascript立即调用函数表达式

函数声明.函数表达式.匿名函数: 函数声明:function fnName () {-};使用function关键字声明一个函数,再指定一个函数名,叫函数声明. 函数表达式 var fnName = function () {-};使用function关键字声明一个函数,但未给函数命名,最后将匿名函数赋予一个变量,叫函数表达式,这是最常见的函数表达式语法形式. 匿名函数:function () {}; 使用function关键字声明一个函数,但未给函数命名,所以叫匿名函数,匿名函数属于函数表达式

立即调用函数(IIFE)

定义: IIFE:立即调用的函数表达式,声明函数的同时立即调用这个函数. 语法: IIFE的常用写法:这两种写法的作用相同,只是表现形式不同而已,()只是起了自执行的作用 (function(){......})() 把函数当作表达式解析,然后执行解析后的函数[相当于 var a = function(){}; a(); a得到的是函数] (function(){......}())是把函数表达式和执行当作语句直接执行[相当于 var a = function(){}(); a得到的是结果] 传