likes
comments
collection
share

💬闭包是什么?| 从现象上直接理解试试!

作者站长头像
站长
· 阅读数 12

网上相关对闭包的定义:

  • MDN:函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
  • 你不知道的JavaScript:是指有权访问另外一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另外一个函数。
  • Javascript核心技术开发解密:闭包是一种特殊对象,由两部分组成:执行上下文A + 该执行上下文创建的函数B

我对这些定义的理解:

  • 《MDN》的解释更加接近原理,
  • 《你不知道的Javascript》的解释更多讲的是现象,
  • 《Javascript核心技术开发解密》的解释更能说明闭包的真实存在:闭包是一种特殊对象。

那么我们就开始来从现象讲讲吧!

定义

下面是直接了当的定义,你需要掌握它才能理解和识别闭包:

当函数可以记住并访问其声明时所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

下面用一些代码来解释这个定义。

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

这段代码看起来和联套作用域中的示例代码很相似。基于词法作用域的查找规则,函数bar()可以访问外部作用域中的变量a。(这个例子也可以当作是一个很单纯的RHS引用查询)。

这是闭包吗?

不是,RHS引用查询规则只是闭包的一部分。(但却是非常重要的一部分!)

上面的代码片段中并没有清晰的展示出 闭包的定义所说的 “即使函数是在当前词法作用域之外执行”

下面我们来看一段代码,很好地展示了闭包:

function foo() {
  var a = 2;
  function bar() {
    console. log( a );
  }
  return bar:
}
var baz = foo();
baz(); // 2 ---> 朋友,这就是闭包的效果。

我们通过不同的标识符baz引用调用了内部的函数 bar()。bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。


闭包理解的重点

bar需要对作用域foo内的变量或函数做RHS查询,所以阻止了垃圾回收销毁foo作用域,那么我们说bar存在对foo作用域的引用,这个引用就叫闭包

foo()执行后,通常会期待foo() 的整个内部作用域都被销毁,而闭包的“神奇” 之处正是可以阻止这件事情的发生。

bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫作闭包。

代码中闭包的身影

前面的仅仅是死板的示例代码,我们来看看平常写的代码中的闭包

function wait(message) {
    setTimeout( function timer(){
            console.log( message );
    }, 1000 );
}
wait( "Hello, closure!" );

wait()执行完成后内部作用域并不会消失,因为timer函数存在对变量message的引用,所以timer函数保有wait作用域的闭包。

从这个思路上来讲,只要使用了回调函数,实际上就是在使用闭包! 在定时器、事件监听器、Ajax请求.....中,到处都存在闭包的身影!

循环和闭包

我们都知道for循环配合var所声明的变量会出一些小问题

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

预期是会每秒输出一个数字,1、2、3、4、5,但实际上却每秒输出一个6

这段代码到底有什么缺陷导致它的行为同语义所暗示的不一致呢?

缺陷就是,尽管循环中五个函数是在各个迭代中分别定义,但是其实它们都是在同一个全局作用域中,因此实际上只有一个i,所以我们需要在每个迭代中都制造出一个闭包作用域

ES5 的解决方法

ES5中我们的解决方法可以用IIFE,通过声明并立即执行一个函数来创建作用域

for (var i=1; i<=5; i++) {
    (function(){
        setTimeout( function timer(){
                console.log( i );
        }, i*1000 );
    })();
}
// 这样还是不行!

当然我们有更多的词法作用域了,但是!我们并没有对这些词法作用域中的任何东西保有引用,所以这些作用域仅仅会被当成“空壳垃圾”被回收掉,让我们加点实质内容试试

for (var i=1; i<=5; i++) {
    (function(){
        var j = i;
        setTimeout( function timer(){
            console.log( j );
        }, j*1000 );
    })();
}
// 行了!它能正常工作了!!

// 优化代码
for (var i=1; i<=5; i++) {
    (function(j){
        setTimeout( function timer(){
            console.log( j );
        }, j*1000 );
    })( i );
}

这样每次迭代的延迟函数的回调都保有对当前迭代作用域的变量j的引用,这样就制造出了一个个闭包,解决了问题!

ES6 的解决方法

let声明可以用来劫持块作用域,并在这个块作用域声明一个变量

for (var i=1; i<=5; i++) {
    let j = i; // 是的,闭包的块作用域!
    setTimeout( function timer(){
        console.log( j );
    }, j*1000 );
}

而且let更棒的是,直接在for循环头部使用let声明的变量将会形成一个块级作用域,它只在for循环的代码块内部可见,并且每次迭代都会创建一个新的独立的变量。这意味着,使用let声明的变量在循环内部具有不同的作用域和值。对于每次迭代,都会创建一个新的变量实例,而不是像使用var声明的变量那样共享同一个变量。

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

这样块作用域与闭包联手,让JavaScript变得更加快乐了!

转载自:https://juejin.cn/post/7281208218176045056
评论
请登录