Skip to content

闭包

闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。

闭包是js的一个难点也是它的一个特色,是我们必须掌握的js高级特性,那么什么是闭包呢?它又有什么用呢?

我们都知道,js的作用域分两种,全局和局部,基于我们所熟悉的作用域链相关知识,我们知道在js作用域环境中访问变量的权利是由内向外的,内部作用域可以获得当前作用域下的变量并且可以获得当前包含当前作用域的外层作用域下的变量,反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的,那么我们想在一个函数内部也有限权访问另一个函数内部的变量该怎么办呢?闭包就是用来解决这一需求的,闭包的本质就是在一个函数内部创建另一个函数。

我们首先知道闭包有3个特性: ①函数嵌套函数(也不全是这样,如下面的第4个例子) ②函数内部可以引用函数外部的参数和变量 ③参数和变量不会被垃圾回收机制回收

1 示例

例1:

一个简单的例子

js
function a() {
    var name = 'Jack'
    return function () {
        return name
    }
}
const b = a()
console.log(b()) // Jack

在这段代码中,a()中的返回值是一个匿名函数,这个函数在a()作用域内部,所以它可以获取a()作用域下变量name的值,将这个值作为返回值赋给全局作用域下的变量b,实现了在全局变量下获取到局部作用域中的变量的值。

例2:

js
function fn() {
  let num = 3
  return function () {
    let n = 0
    console.log(++n)
    console.log(++num)
  }
}
const fn1 = fn()
const fn2 = fn()
// 多次调用同一个fn1,变量num无法被销毁,会被“记住”
fn1() // 1 4
fn1() // 1 5

// 每一个闭包都是引用自己词法作用域内的变量num
fn2() // 1 4

一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了fn1,这时候相当于fn1=function(){var n = 0 … },并且匿名函数内部引用着fn里的变量num,所以变量num无法被销毁,而变量n是每次被调用时新创建的,所以每次fn1执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后就剩下孤零零的num,于是这里就产生了内存消耗的问题

每次调用fn1,通过改变 num 的值,会改变这个闭包的词法环境。fn1 和 fn2 具有独立性,每个闭包都是引用自己词法作用域内的变量 num,因此在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

例3:

定时器与闭包

写一个for循环,让它按顺序打印出当前循环次数

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

按照预期它应该依次输出1 2 3 4 5,而结果它输出了五次5,这是为什么呢?原来由于js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束(即循环在定时任务触发之前早已执行完毕),i的值也已经变成5,所以打印出来五个5,那么我们为了实现预期结果应该怎么改这段代码呢?(ps:如果把for循环里面的var变成let,也能实现预期结果)

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

上面代码可以改写成:

js
for (var i = 0; i < 5; i++) {
  (function (i) {

    // 内部函数中可以使用外部作用域中的变量和参数
    var fn = function () {
      console.log(i)
    }

    setTimeout(fn, 100)
  })(i)
}

引入闭包来保存变量i,将setTimeout放入立即执行函数中,将for循环中的循环值i作为参数传递,100毫秒后同时打印出1 2 3 4 5

那如果我们想实现每隔100毫秒分别依次输出数字,又该怎么改呢?

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

在这段代码中,相当于同时启动3个定时器,i*100是为4个定时器分别设置了不同的时间,同时启动,但是执行时间不同,每个定时器间隔都是100毫秒,实现了每隔100毫秒就执行一次打印的效果。

例4

for循环中Ajax如何获取循环变量i

例1:

错误

js
for (var i=1; i<3; i++) {
    $.ajax({
        type: "GET",
        url: "xxx/xxx",
        data: "num=" + i, // 三次请求参数分别位num0,num1,num3,说明这里可以获取到循环变量i
        success: function(data){
            console.log(i); // 三次打印结果都为3,这不是想要的结果,说明这里不能正常获取循环变量i
        }
    });
}

正确

js
// 使用匿名闭包
for (var i=1; i<3; i++) {
    (function (i) {
        $.ajax({
            type: "GET",
            url: "xxx/xxx",
            data: "num=" + i,
            success: function(data){
                console.log(i); // 分别打印0,1,2
            }
        });
    })(i);
}

// 或者使用let关键字声明i
// for (let i=1; i<3; i++) {
//     $.ajax({
//         type: "GET",
//         url: "xxx/xxx",
//         data: "num=" + i,
//         success: function(data){
//             console.log(i); // 分别打印0,1,2
//         }
//     });
// }

例2:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="plugin/jquery-3.3.1.js"></script>

</head>
<body>
    <ul>
        <li>语文</li>
        <li>数学</li>
        <li>英语</li>
    </ul>
</body>
<script>
    var liList = document.getElementsByTagName('li');

    // 错误
    // for (var i = 0; i < liList.length; i++) {
    //     liList[i].onclick = function () {
    //         console.log(i); // 三次打印结果都为3
    //     }
    // }

    // 正确
    for (var i = 0; i < liList.length; i++) {
        (function (i) {
            liList[i].onclick = function () {
                console.log(i)
            }
        })(i)
    }

    // 正确
    // for (let i = 0; i < liList.length; i++) {
    //     liList[i].onclick = function () {
    //         console.log(i)
    //     }
    // }
</script>
</html>

例5

闭包作为参数传递

js
var num = 15
var fn1 = function (x) {
  if (x > num) {
    console.log(x)
  }
}

!(function (fn) {
  var num = 100
  fn(30)
})(fn1)

在这段代码中,函数fn1作为参数传入立即执行函数中,在执行到fn(30)的时候,30作为参数传入fn1中,这时候if(x>num)中的num取的并不是立即执行函数中的num,而是取创建函数的作用域中的num,这里函数创建的作用域是全局作用域下,所以num取的是全局作用域中的值15,即30>15,打印30。

2 闭包的好处与坏处

好处:

①保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突 ②在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存) ③匿名自执行函数可以减少内存消耗

坏处:

①其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null; ②其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响