全面理解闭包

50 阅读4分钟

1.什么是闭包

一个函数如果可以访问它被创建是所处的上下文环境,则这个函数被称为闭包。--JavaScript语言精粹

闭包是指有权访问一个函数作用域中的变量的函数。 -- JavaScript高级程序设计

我们再举个闭包的例子:

function user() {
    var age = 12
    function userAge() {
        age++
        return age
    }
    return userAge
}
const addAge = user()
console.log(addAge())  // 13
console.log(addAge())  // 14
console.log(addAge())  // 15

从上面的代码中我们可以很明显地看到每调用一次 addAge 函数输出的内容就会+1,这就是闭包的效果。

2.闭包产生的原理

产生

从上面的例子很容易看出来,输出是age每次都+1的结果。虽然addAge 函数在使用的时候并不是在user函数之内,但是它仍然能够使用user函数的变量,比如age,于是乎,闭包就产生了。因为addAge是有权访问user作用域的函数,它有着对该作用域的引用,这个引用就是我们说的闭包

基本原理

addAge分明没有在user之内使用那它为什么还能访问user的变量呢?想要理解这点,我们就需要用到我们之前说的作用域和作用域链的知识了。 由我们之前了解的知识我们知道,JavaScript使用作用域是词法作用域,所以函数声明完成之后它的作用域也确定了,addAge这个函数是user赋值给它的,但user已经执行了,所以addAge的值应该是userAge才对(user 函数return的值),如果是userAge,它自然能够无压力访问age这个变量。

深入原理

就是在执行上下文的内容中我们知道,函数在调用完成之后就会弹栈,再之后就会被垃圾回收机制回收。如此一来该函数里面的变量应当会被销毁,无法再被访问到,但是为什么闭包还是能访问到呢? 如果想不通就说明我们在某个地方陷入了误区,首先,我们知道现在的情况是父函数虽然弹栈了,但是不在父函数之内运行的子函数仍然能够访问父函数里面的变量,那么这就说明该变量肯定还存在内存之中没有被销毁。那变量在什么情况下不会被销毁呢?从垃圾回收机制之中我们知道,只要该变量还在被使用,垃圾回收机制就不会销毁它,这就说明age在一直被人调用着,调用的人就是addAge ,所以闭包就这么产生了。

3.闭包的用途场景

既然我们知道了闭包是如何出现了,我们接下来就可以思考它能用来干嘛了。从上面的例子中我们看到,函数user的变量age是定义在函数里面的,如果我们想在外面的函数中读取到这个变量是不可能的,除非函数将这个变量return出去。但是现在我们发现这个问题可以通过闭包轻松解决了,不过需要我们注意的是,闭包尽量不要像上面那样改动这个值(除非真的需求需要),不然到后面出了问题都不好去找。

异步事件(典型例子:setTimeout/setInterval)

* 这里的i必须要用var来定义,不能用let。熟知es6语法的你们应该都懂的
// 不用闭包的情况下
function fun1(){
  for(var i = 0; i < 10; i++){
    setTimeout(function(){
      console.log(i) 
    },1000) 
  }
  console.log(i) // 10
 }
fun1() // 10个10
  
  // 使用闭包解决
function fun2(){
  var j = 1;
  for(var i = 0; i < 10;i ++){
    (function(j){
      setTimeout(function(){
        console.log(j)
      },1000)
    })(i)
  }
  console.log(i) // 10
  }
fun2()  // 0-9分别输出

作为变量

function fn1(){
    var name="hello";
    return function(){
        return name;
    }
}
var fn2 = fn1();
 console.log(fn2()) // hello

模块化 (2的升级版)

//  IIFE(立即执行函数)等价于fn2 = fn1()
var fn2 = (
function fn1() {
    var name = "hello";
    var age = 3;
    var realName = "my name"
    function myName() {
        console.log( name );
    }
    function myAge() {
        console.log(age);
    }
    return {
        realName:realName,
        myName:myName,
        myAge:myAge
    }
})()
console.log(fn2.realName) // my name
console.log(fn2.myName()) // hello
console.log(fn2.myAge()) // 3

4.闭包的缺点

但是学过前端的都应该听过一句话:不要滥用闭包。这是因为虽然闭包有时候用于帮忙解决某些问题会方便很多,但是它也有一个让人感到窒息的缺点:使用闭包会占用大量内存。因为闭包如果不是开发人员手动清除,那么垃圾回收机制就不会回收它,那使用的闭包越多,所占用的内存就越大,会造成网页的性能问题,在IE中甚至会导致内存泄露。一个开发人员如果在开发时不考虑性能问题简直就是耍流氓。