结合《你不知道的JavaScript(上)》一书,详解闭包,理清关于闭包的三个问题,彻底认识闭包。
闭包详解
1. 什么是闭包?
闭包的定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
1 | function foo() { |
这是一段非常经典的闭包
代码,根据网上的说法,闭包的产生有三个条件:1. A函数内返回B函数。2.B函数调用了A函数的变量。3.B()。
很神奇有没有? 我们在外部环境访问了内部环境的变量。并且正常执行了foo
的作用域会被销毁掉,垃圾回收机制也会使的释放掉内存空间。显然闭包阻止了这件事情的发生。
2.闭包产生的条件?
由于bar
声明的位置,使其拥有涵盖了foo
的内部作用域,当bar
被调用后,会保持对foo
作用域的引用,访问a
变量。保持对外部作用域的引用,就会产生闭包,而不是非要返回函数。
1 | function waiting(msg) { |
无论是什么其他的骚操作,只要你将内部函数传递到所在的词法作用域
之外,他都会保持对内部作用域的引用,无论在哪里执行内部函数,都会创建闭包。这个例子中setTimeout
保持对waiting
的引用,在执行时也会创建闭包。
3. 闭包的用途?
有时候,我们希望能够重用一个变量,并且其被保护起来不被污染篡改,就可以使用闭包。这种变量一般被称为私有变量或者局部变量。
1 | //一个很经典的题 |
嗯,我相信大多数搜集资料学习的人,都会清楚答案。 至于为什么,值得再复习一下。
每次循环,我们都会挑出一份i
用来输出,但因为setTimeout
会在循环完成后执行,每次的i
都在同一全局作用域下,于是后来居上,覆盖了前面的i
,再由setTimeout
执行时,就全是6了。
怎么使得每次循环输出正确呢? 我们只需要将每次的i
变成一个私有变量,有独立的作用域,让其不在篡改就OK了。
- 使用
IIFE
这里引入来自MDN
的释义。
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
1 | (function () { |
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符
()
里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用 ()
创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
1 | // IIFE方法 |
这里利用IIFE
拥有独立的此法作用域的特性,将变量私有化,这样在setTimeout
执行时就会得到正确输出。没错,好像就是利用闭包将每次的变量缓存起来,放在独立的内存中。
- 使用ES6的
let
1 | // let方法 |
let
会劫持块作用域,“劫持”就是把当前块作用域抢过来变为一个独立的,如果还不明白,请看下面代码。
1 | for(var i = 1; i <= 5; i++) { |
let
遇见{}
发生了奇妙的邂逅,于是他们“私奔”了~ 在每次循环中都被声明了一次,所以每次循环中的{}
内的变量都不干扰。setTimeout
很高兴的执行了。
摇晃一下你的小脑瓜,试想一下,如果我们将这些通过闭包产生的私有变量赋值给一个函数,你会联想到什么?
1 | var foo = (function myModule() { |
是不是在项目中很常见? 我靠,这就是模块机制啊,兄弟!!!把一些绝妙的方法都劫持过来放在自己的小空间里面,想拿来用就拿来用,岂不美哉???当然,现代的模块机制肯定没这么简单,嘻嘻。
4. 强大的闭包
认识了闭包,好好想想过去一天中你所写的代码,里面有闭包吗?这是一种日常且强大的模式。
包管理器或者管理模块机制都会将模块定义和模块引入进行封装。封装到现在,已经变成了import
和export
或者其他的语法糖。