爆肝了两三天,终于把闭包这玩意梳理的差不多了

461 阅读7分钟

作用域、作用域链、执行上下文

要想搞懂闭包,必须要先理解作用域、作用域链、执行上下文,这些如果都还没理解,就没必要看闭包了,看了也白看,哈哈!可以看下这篇大佬的文章,写的很详细

深入理解JavaScript作用域和作用域链

总结一下重点:

  1. 作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行
// 块作用域
{
  let a = '父作用域的a'
  {
    let b = '子作用域的b'
    console.log('子获取父的a', a) // 子获取父的a 父作用域的a    //子可以获取父作用域的变量
  }
  console.log(`父获取子的b`, b) // b is not defined     //父不可以获取子作用域的变量
}

// 函数作用域
function father() {
  var a = '父作用域的a'
  function son() {
    var b = '子作用域的b'
    console.log('子获取父的a',a) // 子获取父的a 父作用域的a     //子可以获取父作用域的变量
  }
  son()
  console.log(`父获取子的b`, b) // b is not defined     //父不可以获取子作用域的变量
}
father()
  1. 函数要向上逐层查找变量时,是要到创建这个函数的那个作用域链中查找,而不是调用这个函数的作用域链,这就是所谓的"静态作用域"
    var x = 10
    function fn() {
      console.log(x)
    }
    function show(f) {
      var x = 20
      (function() {
        f() //10,而不是20。fn定义在全局作用域下,所以会找全局作用域下的x
      })()
    }
    show(fn)

注意: 如果定义变量时直接定义,没有用var声明,则此相当于在全局环境的window下定义了此变量。例如这么定义:

    function fn() {
      test = `111`  //这里等同于写 window.test = `111`
      testFn = function () {   //这里等同于写 window.testFn = function () {...}
        return `test`
      }
    }
    fn()
    console.log(test, window.test, window.test === test)  //111 111 true
    console.log(testFn(),  window.testFn(), testFn() === window.testFn())   //test test true
  1. 然后要理解执行上下文环境,在函数每次调用执行时,都会创建一个执行上下文环境。执行完成后,如果其他代码不再跟这个方法内的变量存在引用关系,这个执行环境就会自动被销毁。

    当html运行时,将会创建一个永远不会销毁的全局执行环境,在此环境内定义的所有变量、函数,都不会被自动销毁

  2. 作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。

    一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。

闭包是什么?

  • 引用阮一峰大佬的定义:闭包就是能够读取其他函数内部变量的函数

  • 我的简单概括:在一个函数外部或者它的子函数中,与这个函数内部的变量、方法存在一些引用关系,导致这个函数被调用时所创建的执行上下文环境无法被自动销毁。使得这个函数内的变量能够一直保存在内存中,并可以从此函数外部或者子函数中获取、改变这些变量。我理解的闭包就是这样一种机制,更加贴切好理解,而不是说是一个具体的函数

    说明:

    • 函数外部引用:就是父函数包含子函数,子函数return出去,父函数外部与内部保持引用关系
    • 子函数引用:父函数内存在例如事件监听、setTimeout等的异步操作子函数,引用到了父函数的变量,在子函数执行完成之前,父函数的执行环境也会一直存在
  • 我的详细解释:

    1. 要理解闭包,首先要理解js的特殊作用域机制。在当前作用域(函数作用域和es6的块级作用域)内,如果不存在使用的变量,则会通过js的作用域链逐层向上一层级查找引用,直到顶层window所处的全局作用域。反之,当前作用域却不可以向下查找它的子作用域内的变量。

    2. 然后要理解执行上下文环境,在函数每次调用执行时,都会创建一个执行上下文环境。执行完成后,如果其他代码不再跟这个方法内的变量存在引用关系,这个执行环境就会自动被销毁

    3. 就拿闭包最常见的形式来说,在父函数作用域内,定义了子函数和变量(包括形参也算是父作用域内的变量)。当我们将子函数return出去,当父函数在外部被调用,子函数被赋值给一个变量,父函数外部就跟它内部的子函数存在了引用关系,就会导致该子函数所处的执行上下文环境(即它的父函数的执行时创建的执行上下文环境)不会被销毁,而是一直存在于内存中。另外,子函数中用到的变量(形参),如果当前子函数内不存在,会通过作用域链逐层向上一层级查找引用,并可修改。

    4. 子函数中用到的变量(形参),如果当前子函数内不存在,会通过作用域链逐层向上一层级查找引用。在父函数外部,就可以通过子函数,对这些变量就行读取修改操作

闭包示例:

function father() {   // 父函数 father(),上一层级为window
  var n = 1;          // 父作用域内的变量 n
  function son() {    // 父作用域内的子函数 son()
    n++               // 子函数son()内不存在n,会通过作用域链逐层向上一层级查找,直到最外层的window所在的全局作用域
    console.log(n);
  }
  return son;         
}

var result = father(); // 在父函数father外引用了son,导致子函数son()所处于的执行上下文环境(即father调用时生成的执行上下文环境)一直保存在内存中

result(); // 2        // 当第一次调用result(),操作的是父作用域的n,因父执行上下文环境一直存在于内存中,所以n也一直存在
result(); // 3        // 当第二次调用result(),操作的依然是父作用域的n

闭包用途

这个问题困扰了我很久,就感觉日常写代码压根就没用过闭包啊,好像就没遇上过需要特意去保持一个执行上下文环境不被销毁的场景。为此去看了很多人写的博客

  1. for循环 如下这个烂大街的面试题,for循环里套用setTimeout,输出的i全是4,咋办?
    for (var i = 0; i <= 3; i++) {
      setTimeout(function () {
        console.log( i)
      }, 3000)
    }

首先理解,因为var没有块级作用域,上边的写法就等同于将i定义在全局作用域下。

    var i = 0
    for (i; i <= 3; i++) {
      setTimeout(function () {
        console.log(i)
      }, 3000)
    }

每次循环里的setTimeout引用的都是对全局作用域下的i。setTimeout为异步操作,所以会在for循环结束后再执行,此时i已经变成了4,所以打印出来全是4。

用闭包机制怎么解决这个问题呢?

解决办法就是让setTimeout外套用一个立即执行函数,并将i作为参数传入立即执行函数的作用域内。每次for循环里的立即执行函数执行时,会生成一个单独的执行环境,每个执行环境里的i是独立存在的。这里立即执行函数就相当于父函数,setTimeout就相当于子函数,虽然不是常见的将子函数return出去保持引用的形式,但setTimeout作为异步方法,引用到父作用域的i,在setTimeout执行完之前,这个立即执行函数创建的执行环境也会一直存在。

    for (var i = 0; i <= 3; i++) {
      (function (n) {
        setTimeout(function () {
          console.log(n)
        }, 3000)
      })(i)
    }
  1. 封装私有变量(从别人那抄来的) 我们可以把函数当作一个范围,函数内部的变量就是私有变量,在外部无法引用,但是我们可以通过闭包的特点来访问私有变量。同时,这样做也可以不污染全局变量
var person = function(){
    //变量作用域为函数内部,外部无法访问
    var name = "default";
    return {
        getName : function(){
            return name;
        },
        setName : function(newName){
            name = newName;
        }
    }
}();
console.log(person.name);//直接访问,结果为undefined
console.log(person.getName()); // default
person.setName("abruzzi");
console.log(person.getName()); // abruzzi
  1. 储存变量,只要保持引用,就不会销毁

闭包问题

因为执行上下文环境未被销毁,会导致内存泄漏,可手动将引用变量赋值为null,释放内存

    <body>
      <div desc="div1">块1</div>
      <div desc="div2">块2</div>
    </body>
    <script>
      let divs = document.querySelectorAll("div");
      divs.forEach(function(item) {
        item.addEventListener("click", function() {
          console.log(item.getAttribute("desc"));
        });
      });
    </script>

如上示例,item被引用,所以不会销毁,而我们只需要节点的desc属性,可单独存下desc,让item置空,清除不需要的数据解决内存泄漏问题

    let divs = document.querySelectorAll("div");
    divs.forEach(function(item) {
      let desc = item.getAttribute("desc");
      item.addEventListener("click", function() {
        console.log(desc);
      });
      item = null;
    });

闭包的this问题

this会指向当前函数调用的对象,而非定义位置的对象,如下示例中,fn()是在全局作用域中调用,所以this会指向window

    var person = {
      name: `王KK`,
      getName: function () {
        return function () {
          console.log(this.name)   // undefined
          console.log(this)         // window
        }
      }
    }
    var fn = person.getName()
    fn()

解决办法,箭头函数改变this指向

    var person = {
      name: `王KK`,
      getName: function () {
        return () => {
          console.log(this.name)   // 王KK
          console.log(this)         // {name: "王KK", getName: ƒ}
        }
      }
    }
    var fn = person.getName()
    fn()

最后

闭包的问题,就先到这里,如果还没搞懂,推荐个b站的视频,我觉得讲的还不错,本文中也引用了一些视频里的示例

这次把JS闭包给你讲得明明白白