# JS-闭包

🐴

# 闭包定义

维基百科中闭包是这样定义:

闭包

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

我们通过上面可以知道引用了自由变量的函数就形成了闭包,所以自由变量和函数构成了闭包,函数很容易理解,那么自由变量是什么

自由变量其实就是函数内部使用的函数外部变量

例如:

var a = 1;
function fun(){
  var b = a + 2;
  console.log(b)
}

上面代码中变量a就是自由变量,所以自由变量a和函数fun就构成了闭包,很好理解。多以对于闭包的概念我们还可以这样理解:

闭包(Tom大叔)

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

  2. 从实践角度:以下函数才算是闭包:

  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量

# JS中的闭包

我们从一个简单的例子开始探索JavaScript中的闭包, 我们来看下面的例子:

function fun(){
    var a = 1;
    return function (){
        var b = a + 2;
        console.log(b)
    }
}
var test = fun()
console.dir(test)

text() // 3

上面例子中我们通过JavaScript之执行上下文JavaScript之变量对象JavaScript之作用域和作用域链文章可以分析出它执行过程中执行上下文的变化是这样的:

// 伪代码

// 进入全局代码环境时
ECStack = [
   globalContext  // 全局执行上下文
]
// fun 函数执行时
ECStack.push(<fun> funContext)

//此时执行上下文栈ECStack 
ECStack = [
    <fun> funContext, // fun执行上下文
    globalContext,  // 全局执行上下文
]

// 函数fun执行完成后
ECStack.pop(<fun> funContext)

// 此时ECStack 
ECStack = [
     globalContext,  // 全局执行上下文
]

我们可以看到在函数fun执行完成后,fun的执行上下文就被销毁了,并且fun的执行上下文中的变量对象也都会被销毁,变量对象中存储着变量a也会被销毁,既然变量a已经被销毁了,但是我在执行text()函数时为啥访问到了变量a,并将变量a进行了计算赋值给了变量b

正是为了解决这样的问题,JavaScript中引入了闭包,使得函数fun执行完后变量a虽然被销毁,但我依然可以访问,为什么可以访问,因为变量a已经存在了闭包中。

在我们代码中打印的test函数console.dir(test),我们会看到下面test的作用域[[Scope]]属性

我们可以看到函数fun执行完后返回的函数中存在两个作用域,第一个作用域就是闭包作用域,第一个作用域就是全局作用域,并且在闭包中存储着变量a,所以当test执行时,会先查找自身活动对象中的变量,如果没有找到变量a会通过作用域链去查找作用域中的变量a,所以在闭包中会查找到变量a

所以JavaScript中的闭包为我们解决了即便是自由变量销毁后,也可以从函数内部中访问到,因为自由变量已经存在了闭包中。

如果我们把上面的代码改变一下:

function fun(){
    var a = 1;
    return function (){
        var b =  2;
        console.log(b)
    }
}
var test = fun()
console.dir(test)

text() // 3

在打印console.dir(test)后,因为test函数和变量a没有形成闭包,所以[[Scope]]属性中就不存在闭包作用域了,只存在全局作用域了。

# 常见问题

在说完闭包后,不可避免的我们会说说关于闭包的一些练习题。

var liAry = []
for(var i = 0,len = 4; i<len; i++){
    liAry[i] = function(){
        console.log(i)
    }
}
liAry[0]()  // 我们想要的结果为0
liAry[1]()  // 我们想要的结果为1
liAry[2]()  // 我们想要的结果为2

上面的结果应该很多人都知道,最终都会打印出4。并不是我们想要的结果,首先for循环是没有作用域的,所以for循环中使用var声明的变量i,是存储在全局对象中的,在for循环完成后,i的值最终将是4

此外从实践角度中分析liAry数组中的函数,并没有形成闭包,所以他们在创建时作用域就只用全局作用域(可以打印console.dir(liAry[0]) 看看[[scope]]

scope = [
  VO(globalContext),
]

所以在liAry数组中的函数执行时,他们访问的变量i都是全局中的变量i,所以最终都是4。并且当我们改变全局中i的值时,liAry数组中的函数都会受影响

var liAry = []
for(var i = 0,len = 4; i<len; i++){
    liAry[i] = function(){
        console.log(i)
    }
}

i = 10
liAry[0]()  // 10
liAry[1]()  // 10
liAry[2]()  // 10

为了打印出我们想要的索引值,这里我们可以使用闭包:

var liAry = []
for(var i = 0,len = 4; i<len; i++){
    liAry[i] = (function(i){ // 假设这个匿名函数叫a
        return function(){ // 假设这个匿名函数叫b
           console.log(i)
        }
    })(i)
}
liAry[0]()  // 0
liAry[1]()  // 1
liAry[2]()  // 2

这时就是我们想的的结果了,这是因为每次for循环时匿名函数a都会创建一个新的执行上下文环境,并将i存储在变量对象中,匿名函数b在每次创建时,因为使用了匿名函数a中的i,所以会产生闭包,并存在自己的作用域中。

此时数组liAry中的每个函数的作用域中都有各自的闭包作用域(可以打印console.dir(liAry[0]) 看看[[scope]]),所以当liAry中的函数执行时,都会先查找各自作用域中的闭包内的i, 最终会输出我们想要的值。

解决上面的问题,闭包不是唯一的方法,我们可以使用ES6中的letlet会产生块级作用域)

var liAry = []
for(let i = 0,len = 4; i<len; i++){
    liAry[i] = function(){
        console.log(i)
    }
}
liAry[0]()  // 0
liAry[1]()  // 1
liAry[2]()  // 2

或者通过下面方法实现:

var liAry = []
for(let i = 0,len = 4; i<len; i++){
    (liAry[i] = function(){ 
        console.log(arguments.callee.k)
    }).k = i
}
liAry[0]()  // 0
liAry[1]()  // 1
liAry[2]()  // 2

关于闭包的知识目前先汇总到这里。

最近更新时间: 7/2/2021, 11:27:27 AM