立即执行函数与闭包

612 阅读9分钟

首先解决一个问题: js 闭包 i++ 和 ++i运行结果一样

function a(){
     var i=0;
     function b(){
         i++;
        //++i
         alert(i);
     }
     return b;
}
var c = a();
c();//?
c();//?
c();//?
都会会依次弹出1,2,3

到这里就会有人疑问了,为什么会这样,i++ 和 ++i 不是一个先取值后运算,一个是先运算后取值么?

先来看一段代码:

    let a = 0;
    let b = 0;
    console.log("在运行时a的值:",a++);
    console.log("在运实行b的值:",++b);
    console.log("运行结束之后a的值:",a);
    console.log("运行结束后b的值:",b);

结果:

为什么会时这种效果:

首先,运行结束之后毫无疑问a和b的值都会自增1,那为什么在运行的时候a会是0,b会是1呢?

再来看一段代码:

 <script>
    let a = 0;
    let b = 0;
    let sum_a =  6 + (a++);
    let sum_b =  6 + (++b);
    //console.log("在运行时a的值:",a++);
    //console.log("在运实行b的值:",b++);
    console.log("运行结束之后a的值:",a);
    console.log("运行结束后b的值:",b);
    console.log("sum_a的值:",sum_a);
    console.log("sum_b的值:",sum_b);
</script>

结果:

相信看到这里你应该会有所领悟了,这里的先自增和后自增其实讲的是在运算中的计算步骤

sum_a = 6+(a++);可以改为

sum_a = 6 + a;

a = a + 1;

而sum_b = 6+(++b);可以改为

b = b + 1;

sum_b = 6 + b;

就是因为这样才会有第一段代码中打印值得不同,因为a和b都在计算中(也就是还在运算表达式中),a++要等待运算表达式结束之后再进行自增,而++b一来就先进行自增操作了。

想要解决这个问题,可以直接在alert里面运行:

function a() {
var i = 0;
function b()
{
alert(++i);
//alert(i++);
}
return b;
}
var c = a();
c();
//运行结果
alert(++i); //1
alert(i++);//0

立即执行函数(IIFE)

通常我们声明一个函数有以下几种方式:

// 声明函数f1
function f1() {
    console.log("f1");
}
// 通过()来调用此函数
f1();


//一个匿名函数的函数表达式,被赋值给变量f2:
var f2 = function() {
    console.log("f2");
}
//通过()来调用此函数
f2();


//一个命名为f3的函数的函数表达式(这里的函数名可以随意命名,可以不必和变量f3重名),被赋值给变量f3:
var f3 = function f3() {
    console.log("f3");
}
//通过()来调用此函数
f3();

但如果你看过一些自定义控件的话你会发现他们大多数都是沿用这种写法:

(function() {
    ```
   // 这里开始写功能需求
 })();  

这是我们常说的立即执行函数 (IIFE),顾名思义,也就是说这个函数是立即执行函数体的,不需要你额外去主动的去调用,一般情况下我们只对匿名函数使用IIFE,这么做有两个目的:

一是不必为函数命名,避免了污染全局变量
二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

如果看到这两句话无法理解,那么先从IIFE的运行原理说起。 因为IIFE通常用于匿名函数,这里就用简单的匿名函数作为栗子:

var f = function(){
    console.log("f");
}
f();

我们发现这里f只是这个匿名函数的一个引用变量,那么既然f()能够调用这个函数,我把f替换成函数本身可以么:

function(){
   console.log("f");    
}();

运行之后得到如下结果:

Uncaught SyntaxError: Unexpected token 

产生这个错误的原因是,Javascript引擎看到function关键字之后,认为后面跟的是函数声明语句,不应该以圆括号结尾。解决方法就是让引擎知道,圆括号前面的部分不是函数定义语句,而是一个表达式,可以对此进行运算,这里区分一下函数声明和函数表达式:

1、函数声明(即我们通常使用function x(){}来声明一个函数)
function myFunction () { /* logic here */ }
2、函数表达式(类似以这种的形式)
var myFunction = function () { /* logic here */ };
var myObj = {
    myFunction: function () { /* logic here */ }
};

小学我们就学过用()括起来的表达式会先执行,就像下面这样:

1+(2+3) //这里先运行小括号里面的内容没有意见撒

其实在javascript中小括号也有相似的作用,Javascript引擎看到function关键字会认为是函数声明语句,那么如果Javascript引擎优先看到小括号会怎么样:

//用小括号把函数包裹起来
(function(){
   console.log("f");    
})();

函数成功执行了:

f //控制台输出

立即执行函数通常有下面两种写法:

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

这种情况下Javascript引擎就会认为这是一个表达式,而不是函数声明,当然要让Javascript引擎认为这是一个表达式的方法还有很多:

!function(){}();
+function(){}();
-function(){}();
~function(){}();
new function(){ /* code */ }
new function(){ /* code */ }() // 只有传递参数时,才需要最后那个圆括号。

回到前面的问题,为什么说IIFE这种形式避免了污染全局变量,如果你见过别人写的jquery插件,里面通常会有类似这样的代码:

(function($){
    ```
   //插件实现代码
})(jQuery);

这里的jquery其实是该匿名函数的参数,联想一下我们调用匿名函数时候是用f()那么匿名带参数的就是f(args)对吧,这里把jquery作为参数传入该函数,那么在函数内部使用形参$的时候就不会影响到外部环境,因为有些插件也会用到$这个限定符,你在这个函数内部可以随意折腾。

例题1:

// 创建一个立即调用的匿名函数表达式
// 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只存在于闭包)

例题2:

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
  }
}

为什么 alert 的总是 6 呢,因为 i 是贯穿整个作用域的,而不是给每个 li 分配了一个 i,如下:

那么怎么解决这个问题呢?用立即执行函数给每个 li 创造一个独立作用域即可(当然还有其他办法):

var liList = ul.getElementsByTagName('li')
for(var i=0; i<6; i++){
  !function(ii){
    liList[ii].onclick = function(){
      alert(ii) // 0、1、2、3、4、5
    }
  }(i)
}

在立即执行函数执行的时候,i 的值被赋值给 ii,此后 ii 的值一直不变。

i 的值从 0 变化到 5,对应 6 个立即执行函数,这 6 个立即执行函数里面的 ii 「分别」是 0、1、2、3、4、5。

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <style id="jsbin-css">
        ul {
            list-style: none;
            padding: 0;
        }
        li {
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
    </ul>
    <script>
        var ul = document.getElementsByTagName('ul')[0]
        var liList = ul.getElementsByTagName('li')
        for (var i = 0; i < 6; i++) {
            !function (i) {
                liList[i].onclick = function () {
                    alert(i) // 为什么 alert 出来的总是 6,而不是 1、2、3、4、5
                }
            }(i)

        }
    </script>
</body>
</html>

以上,就是立即执行函数的基本概念。

闭包

面试官:什么是闭包?闭包你了解吗?

应聘者:闭包就是能够读取其他函数内部变量的函数。「函数」和「函数内部能访问到的变量」的总和,就是一个闭包。

面试官:通俗一点呢?

应聘者:通俗的讲就是外部作用域保留着其局部变量供其内部作用域未来某个时刻访问,比如函数a的内部函数b,能够访问函数a内的变量时,就创建了一个闭包。

面试官:是这样,没错,那你知道什么情况下会用到闭包吗?

应聘者:定时器,事件监听器,Ajax请求,或者其他异步或同步任务中,只要使用回调函数,实际上就是闭包。

面试官:那你简单写一个闭包吧

应聘者:下面两个都行

<script>
        let count = 500 //全局作用域
        function foo1() {
            let count = 0;//函数全局作用域
            function foo2() {
                count++;//函数内部作用域
                console.log(count);
             //   return count;
            }
            return foo2;//返回函数
        }
        let result = foo1();
        result();//结果为1
        result();//结果为2
    </script>
function a(){
     var i=0;
     function b(){
         alert(++i);
     }
     return b;
}
var c = a();
c();//外部的变量

面试官:你这个写法是正确的,那我衍生一下,你回答一下依次会弹出什么:

function a(){
     var i=0;
     function b(){
         i++;
         alert(i);
     }
     return b;
}
var c = a();
c();//?
c();//?
c();//?

应聘者:应该是会依次弹出1,2,3。

面试官:没错,你回答的很对,你知道其中的原理吗?能否解释一下

应聘者:好的,i是函数a中的一个变量,它的值在函数b中被改变,函数b每执行一次,i的值就在原来的基础上累加 1 。因此,函数a中的i变量会一直保存在内存中。

当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会 “污染” 全局的变量时,就可以用闭包来定义这个模块。

面试官:非常棒,你说的这是它的一个用处,你能说一下闭包的用处有哪些吗?

应聘者:它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

面试官:那我顺便再出个问题考考你吧,请看题:

var num = new Array();
for(var i=0; i<4; i++){
    num[i] = f1(i);
}
function f1(n){
     function f2(){
         alert(n);
     }
     return f2;
}
num[2]();
num[1]();
num[0]();
num[3]();

应聘者:答案为2,1,0,3(这个时候你了解清楚闭包之后,这个答案应该就是随口而出),解释如下:

//创建数组元素
var num = new Array();
for(var i=0; i<4; i++){
    //num[i] = 闭包;//闭包被调用了4次,就会生成4个独立的函数
    //每个函数内部有自己可以访问的个性化(差异)的信息
    num[i] = f1(i);
}
function f1(n){
     function f2(){
         alert(n);
     }
     return f2;
}
num[2]();  //2
num[1]();  //1
num[0]();  //0
num[3]();  //3

面试官:那你知道闭包的优缺点吗?

应聘者:

优点:

① 减少全局变量;

② 减少传递函数的参数量;

③ 封装;

缺点:

(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

面试官:正好你提到了内存泄漏,说说你的解决方法

应聘者:简单的说就是把那些不需要的变量,但是垃圾回收又收不走的的那些赋值为null,然后让垃圾回收走;

闭包总而言之:

(1)在函数外部访问函数内部变量成为可能;

(2)函数内部变量离开其作用域后始终保持在内存中而不被销毁。

第一条是通过调用闭包的内部函数获取到闭包的成员变量: 在闭包中返回该函数,在外部接收该函数并执行就能获取闭包的成员变量。 原因是因为词法作用域,也就是函数的作用域是其声明的作用域而不是执行调用 时的作用域。

第二条是闭包的 内部函数 必须调用闭包的 成员变量, 这样才能让闭包存在于内存中不被销毁。否则两者没有交互就不会长久存在于内存中(所以debug找不到闭包)