Skip to content

Latest commit

 

History

History
289 lines (219 loc) · 13.1 KB

闭包.md

File metadata and controls

289 lines (219 loc) · 13.1 KB
title date tags
JavaScript篇-闭包
2018-10-08 08:23:45 -0700
闭包

闭包

(面试时候一般扯到垃圾回收机制和作用域链)

闭包看了阮一峰老师的,简单易懂,但是要我解释闭包究竟是什么,我也不知该怎么解释,看了红皮书的《JavaScript 高级编程设计》,这里记一下如果哪里有理解错误的,请指出 ~

概念 : 有权访问另外一个函数作用域中变量的函数

特性 : 函数内嵌套函数,内部函数可引用外层参数和变量,参数和变量不会被垃圾回收机制回收

作用链 : 就是变量和函数可访问范围,变量只能向上访问,访问到 window 对象则被终止

原型链 : 每个对象都会有一个原型proto,只有函数对象才会有 prototype, 当我们访问一个对象的属性时,如果这个对象的内部没有这个属性时,就会去proto中查找这个属性,这个proto又有自己的proto,于是一直查找下去,这就是原型链

简单理解 : 函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。 闭包: 有权访问另一个函数作用域中的变量的函数。

创建闭包的常见方式,就是在一个函数内部创建另一个函数; 当某个函数被调用时,会创建一个执行环境以及相应的作用域链,然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象,但在作用域链中,外部函数的活动对象是种处于第二位,外部函数的外部函数的活动对象处于第三位...一直到作为作用域链终点的全局执行环境。

来看个例子:

function compare(value1, value2) {
  if (value1 < value2) {
    return -1
  } else if (value1 > value2) {
    return 1
  } else {
    return 0
  }
}

var result = compare(5, 10)

下面的图,表示了 compare() 函数执行时的作用域链。首先定义了 compare()函数,然后在全局作用域中调用了它。调用 compare() 函数的时候,会创建一个包含 argumetnsvalue1value2的活动对象。全局执行环境的变量对象(包含 result 和 compare)在 compare()执行环境的作用域链中则处于第二位

全局环境得变量对象始终存在,而像 compare() 函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建 compare() 函数时,会先创建一个预先包括全局变量对象的作用域链,这个作用域链被保存在内部的 [[ Scope ]] 属性中。

当调用 compare() 函数的时候,会为函数创建一个执行环境,然后通过复制函数中的 [[ Scope ]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并推入执行环境作用域链的前端。 (也就是作用域链的前端是 compare 的活动对象)

对于例子中的 compare()函数的执行环境来说,其作用域链中包含两个变量对象: 本地活动对象和全局变量对象。显然,作用域链的本质是一个指向变量对象的指针列表

一般来讲,当函数执行完毕之后,局部活动对象就会被销毁,内存中仅保存着全局作用域,但是闭包不同,它会将活动对象添加到作用域链的前端,也就是说,局部活动对象被销毁,但是它的活动对象仍然留在内存中,这也就是为什么使用闭包可能会导致内存问题。因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存

在一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中,例如下边代码

function createComparosonFunction(propertyName) {
  return function(object1, object2) {
    var value1 = object1[propertyName]
    var value2 = object2[propertyName]
    if (value1 < value2) {
      return -1
    } else if (value1 > value2) {
      return 1
    } else {
      return 0
    }
  }
}

var compare = createComparosonFunction('name')

var result = compare({ name: 'PDK' }, { name: '彭道宽' })

在匿名函数从 createComparosonFunction() 被返回时,它的作用域被初始化为包含 createComparosonFunction() 函数的活动对象和全局变量对象,这样,匿名函数就可以访问在 createComparosonFunction() 中定义的所有变量。

最重要的是,createComparosonFunction() 执行完之后,它的活动对象不会被销毁,为什么呢?因为匿名函数的作用域链仍然在引用它的活动对象。换句换说,当 createComparosonFunction()函数执行完毕之后,局部活动对象就会被销毁,但是因为闭包的原因,它的作用域链被添加到了作用域链的前端,导致 createComparosonFunction()的活动对象会留在内存中,知道匿名函数被释放,createComparosonFunction()的活动对象才会被销毁。比如:

// 创建函数
var compareName = createComparosonFunction('name')

//调用函数
var result = compareName({ name: 'PDK' }, { name: '彭道宽' })

// 解除对匿名函数的引用   (以便释放内存)
compareName = null

设置 compareName 为 null,是为了解除对函数的引用,等于通知垃圾回收机制将其回收,随着匿名函数的作用域链被销毁,其他作用域 (除了全局作用域)也都可以安全地销毁了

注意: 作用域链的这种配置机制,引出了一个副作用,即闭包只能取得包含函数中任何变量的最后一个值

强调: 任何变量的最后一个值

function createFunctions() {
  var result = new Array()

  for (var i = 0; i < 10; i++) {
    result[i] = function() {
      return i
    }
  }

  return result
}

从表面上看,似乎每个函数都应该有自己的索引值, 即位置 0 的函数返回 0,1 的函数返回 1, 但实际上,每个函数都返回 10,因为每个函数的作用域链中都保存着 createFunctions() 函数的活动对象,所以它们引用的都是同一个变量 i,当 createFunctions()函数被返回,变量 i 的值是 10,由于作用域链的副作用,每个函数都引用着保存变量i的同一个对象

  解决方式,创建另一个匿名函数

  function createFunctions() {
    var result = new Array()

    for (var i = 0; i < 10; i++) {
      result[i] = function (num) {
        return function () {
          return num
        }
      }(i)
    }
    return result
  }

在上述代码中,没有立即将闭包赋给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数 num,也就是最终的函数要返回的值。在调用每个匿名函数时,我 们传入了变量 i。由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num。而在这个 匿名函数内部,又创建并返回了一个访问 num 的闭包。这样一来,result 数组中的每个函数都有自己 num 变量的一个副本,因此就可以返回各自不同的数值了

闭包与 this 对象

this 对象是在运行时基于函数的执 行环境绑定的: 在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window,(在使用 call 和 apply 改变函数执行环境下,this 会指向其他对象)。但有时候,由于编写闭包的方式不同,这一点可能不会那么明显

  var name = "The Window"
  var object = {
    name : "My Object",
    getNameFunc : function () {
      console.log('@@@@', this)  // 执行 object
      return function () {
        console.log(this)       // 指向 window
        return this.name
      }
    }
  }
  console.log(object.getNameFunc()()) "The Window"(在非严格模式下)

  // 把外部作用域中的this对象保存在一个闭包能访问得到的变量里,这样就能让闭包访问该对象了

  var name = "The Window"
  var object = {
    name : "My Object",
    getNameFunc : function () {
      console.log('@@@@', this)  // 执行 object
      let _this = this
      return function () {
        console.log(this)       // 指向 window
        console.log(_this)      // 指向 object
        return _this.name
      }
    }
  }
  console.log(object.getNameFunc()()) "My Object"

为什么匿名函数没 有取得其包含作用域(或外部作用域)的 this 对象呢 ?

每个函数在被调用时都会自动取得两个特殊变量:thisarguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

(怎么理解这句话?),个人的理解: 在执行过程中,每个函数都会有一个执行环境,在 getNameFunc()函数里的执行环境 this 指向的是 object,而在闭包中,闭包又有自己的执行环境,而这里的this与它外部函数getNameFunc()的this是不相等的,可能在某种情况下,它们都指向 window,但是并不能说它们相等,而上述代码里,在定义匿名函数前,把 this 对象赋值给了 _this 变量,而在定义了闭包之后,闭包可以访问到外部函数的变量,即使在函数返回之后,闭包将活动对象添加到作用域链的前端,_this 仍然引用着 object,所以会打印出 "My Object"

为什么闭包中的 this=window,因为通过闭包可以访问外部函数作用域中的变量。但每个函数在被调用时都会自动取得两个特殊变量: this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其自己活动对象为止(找到了就不用沿着作用域链继续找了)因此永远不可能直接访问外部函数中的这两个变量。匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。

匿名函数与闭包

什么是匿名函数 ?一般用到匿名函数都是立即执行的,通常叫做自执行匿名函数或者自调用匿名函数。常用来构建沙箱模式,作用是: 开辟封闭的变量作用域环境。我们来看几个例子

;(function() {
  console.log('我是匿名方式1')
})()(
  //我是匿名方式1

  (function() {
    console.log('我是匿名方式2')
  })()
)(
  //我是匿名方式2

  function(i, j, k) {
    console.log(i + j + k)
  }
)(1, 3, 5) // 9

实际上,立即执行的匿名函数并不是函数,因为已经执行过了,所以它是一个结果,这个结果是对当前这个匿名函数执行结果的一个引用(函数执行默认return undefined)。这个结果可以是一个字符串、数字或者 null/false/true,也可以是对象、数组或者一个函数(对象和数组都可以包含函数),当返回的结果包含函数时,这个立即执行的匿名函数所返回的结果就是典型的闭包了

用匿名函数实现闭包

var func = (function() {
  var a = 10
  return function() {
    console.log(a)
  }
})()

func() // 10
// func 作为立即执行匿名函数执行结果的一个接收,这个执行结果是闭包,func等于这个闭包。
// 执行func就相当于执行了匿名函数内部return的闭包函数
// 这个闭包函数可以访问到匿名函数内部的私有变量a,所以打印出10

所以,我们可以说: 闭包跟函数是否匿名没有直接关系,匿名函数和具名函数都可以创建闭包 !!!

// 经典面试题,循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

// 首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

// 解决方式一: 闭包
for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

// 方式二: 使用 setTimeout 的第三个参数, 第三个参将作为第一个参数函数func的参数传进去。
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer() {
      console.log(i)
    },
    i * 1000,
    i
  )
}

// 方式三:利用let, let他会创建一个块级作用域
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}
function fetch(a) {
  return function test1() {
    return function test2() {
      var a = 5
      return a
      function a() {}
    }
  }
}

let res = fetch(55)(2)(3)
console.log(res) // 5, 如果把var a = 5 去掉,那么return 的是 function a

function fetch1(a) {
  return function() {
    return a
  }
}
console.log(fetch1(100)(2)) // 100

function fetch2(a) {
  return function(a) {
    return a
  }
}
console.log(fetch2(100)(2)) // 2