乐趣区

关于前端:深入理解JavaScript立即执行函数IIFE

一句话解释

  1. 立刻执行函数是什么?

    立刻执行函数就是申明一个匿名函数,并马上调用这个匿名函数

  2. 立刻执行函数有什么用处

    创立一个独立的作用域,这个作用域外面的变量,里面拜访不到(即防止 ” 变量净化 ”)

咱们先问本人一个问题:立刻执行函数是闭包吗?如果你不能马上答复这个问题,那么无妨往下看看

什么是立刻执行函数

来自 MDN 的答复是

IIFE(立刻调用函数表达式)是一个在定义时就会立刻执行的 JavaScript 函数 )。

这样就能造成一个 块级作用域 成果

(function () {// 块级作用域})();

这在没有块级作用域的 ES3 时代,是相当广泛的做法

以前有个有名的面试题,如下所示:

for (var i = 0; i < 5; i++) {setTimeout(function () {console.log(i);
    }, 1000 * i);
}

后果是什么,5,5,5,5,5,而且是在每 1 秒打印一个 5

问,有什么办法让它的后果是 1,2,3,4,5

咱们剖析一下为什么会在一开始的时候打印 5,这是因为 setTimeout 是异步,要塞进异步队列中,所以一开始先循环,循环完了再执行 setTimeout(func, wait)。

所以执行程序是

for (var i = 0; i < 5; i++) {// 赋值 setTimeout(function() {console.log(i) }, 1000 * i)
    // i 1,2,3,4,5
}
// setTimeout 提早执行,var i 被对立赋值为 5
setTimeout(function () {console.log(5);
}, 1000 * 1);
setTimeout(function () {console.log(5);
}, 1000 * 2);
setTimeout(function () {console.log(5);
}, 1000 * 3);
setTimeout(function () {console.log(5);
}, 1000 * 4);
setTimeout(function () {console.log(5);
}, 1000 * 5);

又因为,for() {} 不会造成块级作用域,所以会拿最初的值也就是 5 来给每一个 func 中的 console.log(i) 赋值,最初导致了这样的打印后果

剖析完后,咱们要思考一下,怎么保住 setTimeout 中的变量 i,通常的方法是通过作用域来爱护,例如用块级作用域来爱护 i,办法是用 let 代替 var。

for (let i = 0; i < 5; i++) {
    // 将 var 改成 let 即可
    setTimeout(function () {console.log(i);
    }, 1000 * i);
}

或者用函数作用域来爱护,因为函数作用域内的变量,函数外不能拜访

for (var i = 0; i < 5; i++) {(function (j) {setTimeout(function () {console.log(j);
        }, 1000 * j);
    })(i);
}

用 let 的办法的伪代码相似于立刻执行函数。代码了解为:

每传入一个变量 i,并立刻执行 setTimeout,执行结束一次后,for 循环中的 i 变为 1,再执行 setTimeout,这样就达到了成果

其原面试题为什么会呈现这种后果,实质是 JavaScript 中的 for 循环不能爱护 i 被扭转,即 for 循环不能造成块级作用域。

通过这题咱们能清晰的认知到立刻执行函数的用途: 定义时就会立刻执行的函数

IIFE 的延展状态

咱们常见的 IIFE 是这样的:

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

然而不乏看到这样的代码

(function (window) {console.log(window);
})(window);

刚开始咱们会很懵逼,这是能够传值?

先看看一般函数怎么运行的

var foo = function (name) {console.log(name);
};
foo('johan');

咱们能够在任何中央调用 foo 函数,

之所以要发明“IIFE”,是因为它们是立刻调用的函数表达式,这意味着它们会在运行时立刻被调用,且咱们不会再去调用它,它只运行一次,如下所示:

var foo = (function (name) {console.log(name);
})('johan');

甚至,咱们能够不必赋值给 foo,因为咱们并不会应用 foo

(function (name) {console.log(name);
})('johan');

以上例子很好了解吧,本人定义一个匿名函数并且本人传入参数调用

为什么要有 IIFE

起因很简略,为了让一块代码执行且不被其余库影响。在 ES6 的模块呈现之前,咱们写 JavaScript 不是在 HTML 的 script 标签中书写,就是在 javascript 文件中写代码再通过 script 标签引入。当你写的(全局)办法和他人(第三方库也好,共事的代码也好)雷同时,就会有办法笼罩的 bug 存在

所以用 IIFE,保障了每一个 IIFE 中的代码变量不会在全局作用域下被拜访,也就起到了变量爱护的作用

实用场景

UMD 打包

(function (root, factory) {if (typeof define === 'function' && define.amd) {define(factory);
    } else if (typeof exports === 'object') {module.exports = factory;} else {root.MYMODULE = factory();
    }
})(this, function () {//...});

实质就是把 AMD 和 CommonJS 联合在一起

源码中的立刻执行函数

jQuery 中的:

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

underscore 中的:

(function(global, factory) {...}(this, (function () {...})));

这些大佬库中都用到了立刻执行函数来爱护库中变量

立刻执行函数是闭包吗

回到一开始的问题,IIFE 是闭包吗?

必定不是,IIFE 是立刻执行函数,执行完就被垃圾回收了,怎么会是闭包呢?

什么是闭包?闭包是要利用作用域机制把控公有变量

两者为什么会被人搞混?

因为 IIFE 能起到隔离变量的作用,为模块化没进去前而做的 hack 变量爱护机制。而闭包恰好也能起到隔离变量的成果。所以这两者会被人搞混

那有立刻执行函数实现闭包的场景吗?

var Module = (function () {
    var private = '公有变量';
    var foo = function () {console.log(private);
    };
    return {foo: foo,};
})();

Module.foo(); // 公有变量
Module.private; // undefined

立刻执行函数不是闭包,然而它能够做出闭包成果

三题见真章

第一题

(function () {if (typeof name === 'undefined') {console.log('Goodbye' + name);
    } else {
        var name = 'Jack';
        console.log('Hello' + name);
    }
})();

<details>

<summary>
    答案
</summary>
Goodbye undefined
<p>
    解题思路:</p>    
<p>
    咱们都晓得一个函数定义并立刻执行就是立刻执行函数,既然是函数,就造成了作用域,在这个题目中,函数内有变量晋升,即 var name 被提到函数顶部,且默认为 undefined,所以 typeof name === 'undefined' 时,console.log('Goodbye undefined')
</p>

</details>

第二题

var _fn = function () {console.log(1);
};

(function () {var _fn = function () {console.log(2);
    };

    var fn1 = function () {this._fn.apply(this);
    };

    var obj = {_fn: function () {console.log(3);
        },
        fn2: fn1.bind({_fn: function () {console.log(4);
            },
        }),
        fn3: fn1,
    };

    var fn4 = obj.fn3;
    var fn5 = obj.fn2;

    fn1();
    obj.fn2();
    obj.fn3();
    fn4();
    this.fn5();})();

<details>

<summary>
    答案
</summary>
<p>1</p>
<p>4</p>
<p>3</p>
<p>1</p>
<p> 报错 this.fn5 not function</p>

</details>

第三题

var liList = ul.getElementsByTagName('li');
for (var i = 0; i < 6; i++) {liList[i].onClick = function () {alert(i); // 为什么 alert 进去的总是 6,而不是 0,1,2,3,4,5
    };
}

<details>

<summary>
    答案
</summary>
<p> 为什么 alert 的值总是 6,因为 i 是贯通整个作用域的,而不是给每个 li 调配一个 i

解决方案有很多,例如用 let 代替 var。或者是用 IIFES</p>
</details>

参考资料

  • 揭秘 IIFE 语法

系列文章

  • 深刻了解 JavaScript——开篇
  • 深刻了解 JavaScript——JavaScript 是什么
  • 深刻了解 JavaScript——JavaScript 由什么组成
  • 深刻了解 JavaScript——所有皆对象
  • 深刻了解 JavaScript——Object(对象)
  • 深刻了解 JavaScript——new 做了什么
  • 深刻了解 JavaScript——Object.create
  • 深刻了解 JavaScript——拷贝的机密
  • 深刻了解 JavaScript——原型
  • 深刻了解 JavaScript——继承
  • 深刻了解 JavaScript——JavaScript 中的始皇
  • 深刻了解 JavaScript——instanceof——找祖籍
  • 深刻了解 JavaScript——Function
  • 深刻了解 JavaScript——作用域
  • 深刻了解 JavaScript——this 关键字
  • 深刻了解 JavaScript——call、apply、bind 三大将
退出移动版