你必须知道的-JS闭包

367 阅读4分钟

JavaScript中闭包无处不在,你要做的就是识别并且拥抱它!

关于闭包,不同的人有不同的解释和认识,以下列举几个普遍的表达方式,以便让更多的人理解什么是闭包?

  • 内部函数A可以引用外部函数B的参数和局部变量,当函数B返回函数A时,相关参数和变量都保存在返回的函数中,这种程序结构称为“闭包(Closure)”
  • 当函数可以记住并访问所在词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行.
  • 闭包是指有权访问另一个函数作用域中的变量的函数

接下来开始切入正题, 鉴于以上几个观点都分别涉及到了"作用域", 首先来介绍一下这个.作用域有两种工作模型.

  1. 词法作用域: 定义在词法阶段的作用域. 换句话说就是由你在写代码时将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变(大多数情况如此)
  2. 动态作用域: 定义在代码运行时的作用域. 换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套.

主要区别: 词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行是确定的.(this也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用.

注意: JavaScript并不具有动态作用域,它只有词法作用域,但是this的机制某种程度上很像动态作用域.

你其实不需要有意的创建闭包,闭包其实在你的代码中随处可见,很多时候其实你就是没有识别闭包.

来看一段代码

 function foo() {
     var a = 2;
     function bar() {
         console.log(a);
     }
     return bar;
 }
 var ba = foo();
 ba()   // 2 

ba执行时,拿到的是foo 的返回值 bar(),也就是说ba调用了内部函数bar(). bar()保持有foo内部作用域,对该作用域的引用,而这个引用就叫做闭包! 这个函数在定义时以外的地方被调用.闭包使得函数可以继续访问定义时的词法作用域.

再来看两段代码

function foo() {
    var a = 2;
    function baz() {
        console.log(a)  //2
    }
    bar(baz);
}
function bar(fn) {
    fn()
}
var fn;
function foo() {
    var a = 2;
    function baz() {
        console.log(a)
    }
    fn = baz //将baz分配给全局变量fn
}
function bar() {
    fn();  //实际就是调用baz() 这就是闭包
}
foo()
bar()

不论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包

无论通过何种手段将内部函数传递到所在词法作用域之外,它都会持有对原始定义作用域的引用,无论何时执行这个函数都会使用闭包

循环和闭包

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

上面代码大家期望的输出结果应该都是每隔一秒输出1~5,而实际是以每秒一次的频率输出5次6.

为什么会出现这样的问题呢?

因为我们以为循环中的每一个迭代都会"捕获"一个 "i"的副本.但是根据作用域的原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个 "i"

延迟函数的回调会在循环结束时才执行. 即使setTimeout(...,0),所有的回调函数依然是在循环结束后才会被执行.

那么怎么修改可以让它正常工作呢?

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

或者对代码改进一下

for(var i = 1; i <= 5; i++){
  (function (j){
    // 相当于let j = i, 存下了值
    setTimeout(()=> {
    console.log(j)
    }, j*1000)
  })(i)
}

立即执行函数会通过声明并立即执行来创建作用域,setTimeout会将立即执行函数在每次迭代中创建的作用域封闭起来!(封闭在每个迭代内部,每个迭代中都会有一个正确的值供我们访问) 因此,使用闭包可以解决这个问题.

根据立即执行函数创建作用域的解决方案分析,可以想到 let声明.

利用let声明劫持块作用域,并在这个作用域声明一个变量.

for(let i = 1; i <= 5; i++){
  setTimeout(()=> {
    console.log(i)
  }, i*1000)
}

这样子同样可以得到期望的输出结果.

使用闭包的注意点

由于闭包会将变量保存在内存中,因此如果大量使用闭包可能会造成性能问题,如内存泄漏,通常的解决方式是在关闭函数之前将不使用的局部变量删除!