之前上选修课赶作业的时候有遇到过这个问题。
然后为了赶作业…没有具体的学习。
对于闭包的认知一直保留在"与 JavaScript 的作用域以及内存回收机制有关"。
来填一填坑。
参考、阅读、引用的文档:
阿里前端面经
MDN - closures
stackoverflow - How do JavaScript closures work?
( ´・ᴗ・` ) so 上这个问答都已经是发生在十年前了。
词法作用域
可恶!又想起了令人头秃的编译原理。
大部分语言都是基于词法作用域的,和词法作用域相反的呢,就是动态作用域。
然鹅 JavaScript 虽然是基于词法作用域的,但是 JavaScript 有一个大宝贝 this ,this 的机制又很像动态作用域(就是说 this 的域是在运行的时候确定的)。
不过说起来我怎么不记得词法分析这一步会确定作用域。。。(菜鸟发言
JavaScript 里,当我们要使用变量时,引擎总会从最近的一个 Scope 开始向外层查找。
1 | let demo = "outer"; |
为什么输出是 outer ?
- JavaScript 中的作用域就是词法作用域 。 (那函数作用域呢 ? 块级作用域呢 ? 词法作用域是指的变量的可见性
- 词法作用域是写代码的时候就静态确定下来的 ,所以函数 foo 的上级 scope 就是 global 。
可以从上图里看到 foo() 函数的上级 scope 是全局环境,因此输出的 demo 是 'outer' 而不是 'inner'。
简单来说,词法作用域关注在何处声明,动态作用域关注在何处调用。
闭包
那认知了一下词法作用域就可以开始了解闭包了。
闭包的定义
查阅到以下的定义:
- 闭包是词法作用域的体现。
- 闭包是函数和声明该函数的词法环境的组合。
- 闭包是 JavaScript 函数作用域的副作用产品,是一种特殊的对象。
- 一个持有外部环境变量的函数就是闭包。
组合一下理解应该就差不多了。就是说闭包是一个函数,即可以访问本身自己的局部变量,又能访问定义这个函数时的域里的局部变量。
再举个 MDN 上的例子。下例里的 add5 和 add10 是两个闭包函数,保存了不同的 x 值(定义函数时的域里的局部变量
1 | function makeAdder(x) { |
在 JavaScript 里,函数的作用域里的变量都会在函数执行完后被回收(就是最开始的提到的为什么闭包会和内存回收机制有关系),但是创建了闭包之后函数的作用域会一直保存。
经典的循环闭包问题
1 | <body> |
上面的代码遇到的问题是无论在浏览器里点击哪个 div ,都会在 console 栏里输出 5 。
去年选修课的时候遇到的上面差不多的问题,其实百度一搜一大把一样的问题,所以当时解决方法就跟百度里的一样,用了 IIFE ,但是当时并不知道原理是什么,就只知道"啊,这不就是那个经典的闭包问题嘛!"
1 | <body> |
以及现在如果遇到这种情况也不会去想闭包问题的逃避式解决方法:
1 | <body> |
问题的原因
在 for 循环里定义的点击事件进行的操作里用到了变量 i , 但是函数本身作用域里没有定义 i 这个变量,只能从定义这个函数的作用域里去找,存在 i 这个变量,并且在循环结束之后 i 的值为 5 (如果用 for(var i of [1,2,3,4] 则 i 最后为 4),上面的词法作用域里也提到了,js 的引擎要使用变量时,会向上层域里寻找,所以四个 div 绑定的监听事件里引用到的 i 是同一个变量,就是他们在创建时的域里的那个变量 i 。
解决的原因
解决方案 1:
使用了 IIFE 生成一个匿名函数,并把建立点击事件时的 i 作为参数传入这个匿名函数,所以监听事件尽管本身的域里还是没有这个变量 i ,但是四个点击事件已经有了不同的函数作用域,就是每一次循环生成的匿名函数的作用域,所以也就解决了这个问题
解决方案 2:
hmm 这个倒是很简单。
关键字是块级作用域。
不过需要注意的是,ES6 之前是没有块级作用域的,而 ES6 里出现的 const/let 关键词声明的变量有自己的作用域块。
所以在 for(let i = 0; i < 4; i++) { 循环体 } 里,相当于表达示里申明了五个 let 块级作用域 {let i = 0}{let i = 1}{let i = 2}{let i = 3}{let i = 4},每一个 let 都有自己的作用域,所以在 for 循环的表达式里使用 let i ,每一个 i 都有属于自己的独立的块作用域。
这个解决方法 1 的区别在于,解决方法 1 使每个点击事件的定义时的域不同,而解决方法 2 里点击事件的定义时的域相同,但是每次循环内部都是一个新的 i。
闭包的用处
虽然对新手不太友好,但是也是很有用处的。
获取函数作用域里的定义
就像在认识闭包前需要认识到词法作用域一样,认识到闭包的这个作用前需要先理解一下函数作用域。
JavaScript 里有 global scope 、函数可以创建 function scope 以及 let/const 的 block scope。
再次强调 词法作用域是指的变量的可见性
在函数外部是没有办法获取到函数内部的变量的,即在 function scope 外,没有办法获取到 function scope 上的变量。
但是闭包可以。
原因就是嵌套的函数可以访问其外部声明的变量。
所以只要获取到某个函数 A 里定义的函数 B ,就可以通过函数 B 来获取函数 A 里定义的变量。
模拟私有函数和变量
也是差不多啦
和上面的那个大同小异,就是利用无法获取到函数作用域里的内容一样,使用闭包来访问私有的函数和变量。
举一个 MDN 的例子
1 | var Counter = (function() { |
闭包的坏处
增加 JavaScript 学习难度
影响处理速度和内存消耗
通常来说函数执行完了,其内部定义的变量就会被释放,但是闭包这个异次元通道使得其外部声明的变量会保留在内存里。
MDN 上举的例子是,最好不要在构造函数里定义方法,(就是不要在函数里内嵌函数)
不然会在每次新建一个实例的时候都对方法从新赋值。
hmmm
IE6 的 bug,使用闭包会造成内存泄露(已修复
影响父级函数的变量
上面提到闭包可以模拟 static
但是如果本意不是建立一个私有对象,那么闭包作为父函数的公用方法,有可能会无意识改变父级函数里的变量的值。(情况和全局变量的问题很相似。