讲解「闭包」

作者: 果汁凉茶丶 | 来源:发表于2020-06-07 15:26 被阅读0次

    # 定义

    闭包 是指有权访问另一个函数作用域中的变量的函数。注意别混淆匿名函数和闭包的概念。
    创建闭包 需要达到两个条件,如果不满足第二条,也只能称作是匿名函数
    (1)在一个函数内部创建另一个函数
    (2)内部函数访问外部函数的变量

    function createCompareFn(attr) {
      return function(obj1, obj2) {
        return obj1[attr] >= obj2[attr]
      }
    }
    

      上例中,内部函数(一个匿名函数) 访问了包含函数(即外部函数)的变量attr,即使这个内部函数被返回且在其他地方被调用了,它仍然可以访问变量attr。之所以能够访问,是因为内部函数的作用域链中含有包含函数的作用域。

    # 作用域链

      当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,包含函数的活动对象处于第二位,包含函数的包含函数的活动对象处于第三位……,直到找到全局环境为止。这些活动对象使用链表来连接,形成作用域链
      在函数执行过程中,为了读取和写入变量的值,需要在作用域链中查找变量。看一个简单的函数声明及调用来解释:

    function compare(val1, val2) {
      return val1 >= val2
    }
    var result = compare(5, 10)
    

      上述代码定义了compare函数,然后又在全局作用域中调用。当调用compare()时,会创建一个包含argument, val1, val2的活动对象。而全局环境的变量对象处在作用域链的第二位

    A
      后台的每个执行环境都有一个标识变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()这样的局部环境的变量对象,只有在函数执行过程中菜户存在。在创建compare()函数时,会创建一个包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中,当执行compare()函数时,回味函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行的作用域链,以此类推形成作用域链。如图所示,作用域链本身只是一个指向变量对象的指针列表,它只引用但不实际包含对象
      无论什么时候在函数中访问一个白能量时,就会从作用域链中搜索具有相应名字的变量。通常当函数执行完成后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)

    # 闭包的作用域链

      由于闭包存在函数内部定义函数,内部定义的函数将包含函数的活动对象到它的作用域链中。假设有闭包如下:

    function createCompareFn(attr) {
      return function(obj1, obj2) {
        return obj1[attr] >= obj2[attr]
      }
    }
    var compare = createCompareGn('name')
    var result = compare({ name: 'Nic' }, { name: 'Goe' })
    

      则它的作用域链关系为


    B

    由于闭包会携带它包含函数的作用域链,因此会比其他函数占用更多内存,过度使用闭包容易导致占用内存过多,需谨慎。

    # 闭包与变量

      由于作用域链本身只是一个指向变量对象的指针列表,它只引用并不真正存储它们。而这种配置机制引出了一个副作用,即闭包只能取到包含函数中任何变量的最终值
      因为闭包作用域链与包含函数的活动对象之间只是引用关系,当包含函数中由于某些运算导致它活动对象中的属性发生更新时,该更新会被带到闭包作用域中,当闭包再访问变量时,取到的就是被更新后的变量的值。经典例子如下:

    function createFunctions() {
      var result = new Array()
      for (var i = 0; i < 10; i++) {
        result[i] = function() {
          return i
        }
      }
      return result // 10 个 10
    }
    

      该例中,在闭包中返回包含函数的变量i并压入给结果数组。表象上看应该得到1~10的数组。打印发现是10个10。原因是:每个函数的作用域链中,都保存着createFunctions()函数的活动对象,引用的都是同一个变量i;当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用者保存变量i的同一个变量对象。所以在每个函数内部i的值都是10。作用域引用关系如下:

    C

      我们可以通过创建另一个匿名函数强制让闭包的行为符合预期

    function createFunction() {
      var result = new Array()
      for (var i = 0; i < 10; i++) {
        result[i] = function(num) {
          return function() {
            return num
          }
        }(i)
      }
    return result // 1~10
    }
    

      经过如上改造,我们没有闭包直接赋值给数组,取而代之的是定义了一个匿名函数,并将立即执行该函数的结果赋给数组。匿名函数有个参数num,存在于匿名函数的作用域链中。程序执行for循环调用每个匿名函数时,由于函数参数是按值传递的,在其内部我们创建了一个直接访问num的闭包。这样一来,result数组中存储的值就是每次执行的num的一个副本了。

    # 闭包的this变量

      我们知道,this对象是在运行时基于函数的执行环境绑定的。在全局函数中,this等于window。而当函数被某个对象的方法调用时,this等于那个对象。
      由于匿名函数的执行环境具有全局性,因此其this对象通常指向window(除使用call()apply()来改变函数执行环境外)。看以下例子:

    var name = 'the window'
    var object = {
      name: 'my object',
      getNameFn: function() {
        return function() {
          return this.name
        }
      }
    }
    console.log(object.getNameFn()())  // the window
    

      本例中,getNameFn是对象中的一个方法属性。object.getNameFn()返回一个函数,object.getNameFn()()立即执行该函数得到一个字符串。
      我们知道,每个函数在被调用时都会自动取得两个特殊变量:thisarguments。内部函数在搜索这两个变量时,只会搜索到它自己的活动对象为止(它们自身能获取到不必去包含函数活动对象中获取)。在本段代码执行时,程序发现需要返回逻辑想要返回的this.name,于是搜索匿名函数自身的作用域,取到自己的this对象,该对象因匿名函数执行环境全局性的特征指向了window,从而输出了"the window"

      注意本例的称呼是匿名函数而不是闭包!原因就是匿名函数内部有自身的this变量,它无需也无法获取到外部objectthis,没有达到内部函数访问外部函数的这么一个行为,因此不称呼为闭包。以下改造后就符合了闭包的特征

      如果我们想获取到object中的name,只需如下简单改造即可

    var name = 'the window'
    var object = {
      name: 'my object',
      getNameFn: function() {
        var that = this
        return function() {
          return that.name
        }
      }
    }
    console.log(object.getNameFn()()) // myobject
    

      经过如上改造后,但执行object.getNameFn()()调用内部闭包函数时,需要搜索that,而在自身作用域内并没有找到that,于是顺着作用域链查找包含函数的作用域,得到结果。

    # 闭包与内存泄漏

      由于闭包作用域链包含着包含函数的作用域,因此会比普通函数占用更多的内存,当使用闭包不当,且未得到合适的释放情况下,就容易造成大量内存空间的占用。看一个例子

    function assignHandler() {
      var element = document.getElementById('someElement')
      element.onclick = function() {
        alert(element.id)
      }
    }
    

      以上代码实现了对某个元素进行点击时的点击响应事件。onclick是一个闭包,在这个闭包内循环引用了element.id。因此,只要匿名函数存在,element的引用数至少是1。那么,在垃圾回收机制规则中就无法判定element是一个需要被回收的元素。导致其一直占用在内存空间中。解决办法如下

    function assignHandler() {
      var element = document.getElementById('someElement')
      var id = element.id
      element.onclick = function() {
        alert(id)
      }
    element = null
    }
    

      如此改造有两点:(1)对element.id保存副本目的是在闭包中取消对元素变量的循环引用。(2)由于闭包会引用包含函数的整个活动对象,其中还包含着element,因此包含函数的活动对象中也会保存有一个引用。因此有必要把element变量设置为null

    # 闭包与应用场景

    (1)模仿块级作用域
      在闭包中创建的变量,不受外部变量的影响

    function Counter(count) {
      for (var i = 0; i < count; i ++) {
        alert(i)
      }
      alert(i) // 计数
    }
    

    (2)创建私有变量
      创建一个可以访问私有变量和私有函数的共有方法。该长发也称作特权方法。

    相关文章

      网友评论

        本文标题:讲解「闭包」

        本文链接:https://www.haomeiwen.com/subject/csaxtktx.html