群里分享的关于我对 JS 闭包的理解

昨天在”你知不到的JS“群里分享了我对闭包新的认知,原文如下:

无论是前端还是 iOS 的同学,刚开始学习闭包的时候,总是会感觉到非常吃力。我试着用我理解的方式来聊一聊闭包这个话题,读完《你不知道的 JS 上》后,我对闭包有了新的认识。旧的认知 带有执行环境的函数 - 闭包

在开始之前,我们先想一下为啥要有闭包,闭包的出现为开发人员解决了哪些问题。

群里分享的关于我对 JS 闭包的理解

上面的代码中,当函数 hello 执行完后,它内部的变量 welcome 内存空间将会被释放。如果不想让 welcome 释放,有什么好的办法吗?把上面的代码稍微改动一下。

群里分享的关于我对 JS 闭包的理解

这两段代码的执行结果是一样的,只不过例2返回一个函数,在函数内部访问了变量 welcome。我们再看一个例子:

群里分享的关于我对 JS 闭包的理解

例 3 中 makeAdder 函数返回一个函数 add,add 引用了变量 temp。当执行 plusOne(2) 和 plusOne(5) 的时候,发现变量 temp 仍然能够被访问到。同理 plusTwo(2) 和 plusTwo(5) 也能够访问变量 temp。就好像 makeAdder 这个函数拥有记忆功能,可以记住执行时的参数 x。

上面的这些现象,其实就是用到了闭包(colsure),函数 welcomeFun 可以访问函数外的变量 welcome,函数 add 可以访问函数外的变量 temp,能够访问的变量都有一个特征,这些变量在另外一个函数中,也就是说存在嵌套函数,内部函数可以访问外部函数词法环境内所有变量。

我们再看个例子:

群里分享的关于我对 JS 闭包的理解

例 4 中的代码会产生闭包吗?答案是否定的。函数 welcomeFun 内部访问 welcome,welcome 属于全局作用域的变量。

群里分享的关于我对 JS 闭包的理解

例 5,会不会产生闭包呢?

具体会不会产生闭包,看具体的实现,以 Chrome 为例:

通过 Chrome 浏览器调试可以看出,内部函数 welcomeFun 访问了外部函数 hello 的变量 welcome,当函数 welcomeFun 执行的时候,会产生一个短暂性的闭包,因为 welcomeFun 函数在 hello 函数内部立即执行了,当 hello 函数调用结束后这个闭包就被释放了。

群里分享的关于我对 JS 闭包的理解

MDN 上也有类似这样的例子:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

群里分享的关于我对 JS 闭包的理解

翻译一下:

init() 创建了一个局部变量 name 和一个函数 displayName(),displayName() 函数定义在了 init() 函数内部,属于内部函数,仅仅在 init() 函数体内有效。有一点需要注意一下 displayName() 函数并没有自己的变量。由于内部函数可以访问外部的变量,也就是说 displayName() 函数可以访问外部函数 hello 的变量 name。

运行这段代码,你会发现调用 alert() 后可以成功地显示定义在displayName() 函数中的 name 变量。这是一个词法作用域的例子,描述了解析器如何访问嵌套函数中的变量。词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

如果按照下面代码的写法,当函数 hello 被调用后,welcomeF 变量是对 welcomeFun 函数的一个引用,welcomeFun 函数中包含了外部函数 hello 的变量 welcome,这时候函数 hello 调用结束,此时会产生一个闭包,当 welcomeF 变量释放的时候,闭包也会被释放。

群里分享的关于我对 JS 闭包的理解

在《你不知道的 JS 上》上对闭包的定义是这样的:

群里分享的关于我对 JS 闭包的理解

闭包是当一个函数即使脱离了词法作用域,仍然能够访问它所在词法作用域。

函数 welcomeFun 显然没有脱离词法作用域就被执行了,严格意义上不符合上面关于闭包的定义。至于最终会不会产生闭包,我给出的结论是:这个会根据不同的解释器的具体实现来看,单纯概念上来讲,不会产生闭包,变量的访问依赖于词法作用域就够了,但为了统一实现,Chrome 会生成一个短暂性的闭包,只不过这个闭包没有返回给外部使用,只在函数内部可以访问,当外部函数执行完成后,这个闭包就会被释放,也就是说在函数 hello 外部不会存在闭包,因为闭包已经被释放了。

我们再来看非常经典的一道面试题:

群里分享的关于我对 JS 闭包的理解

上面的代码执行结果是啥?答案是:每隔1秒输出一个 4。var 定义的变量是函数作用域,或者全局作用域,此处只定义了一个 i,timer 函数中使用了 i 的引用,当 for 循环结束后,timer 会被调用,此时 i 为 4。其实,我们想要的结果是每隔 1 秒分别输出 1、2、3。咋么做?

既然 var 声明的变量是函数作用域的,我们可以把变量的申明放到一个函数内部,这时想到了自执行函数。

群里分享的关于我对 JS 闭包的理解

运行这段代码,你发现结果输出的还是:每隔1秒输出一个 4。因为在自执行函数中使用的还是变量 i,可以改进一下:

群里分享的关于我对 JS 闭包的理解

还可以改进一下:

群里分享的关于我对 JS 闭包的理解

ES6 以后可以通过 let 声明块级作用域的变量。把 var 改成 let,在 for 循环中,每次声明一个独立的变量 i。这样结果就是每隔1秒,分别输出 1、2、3。

群里分享的关于我对 JS 闭包的理解


参考:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

《你不知道的 JS》

群里分享的关于我对 JS 闭包的理解