Libx

我不知道的JavaScript:闭包

字数统计: 1,417阅读时长: 5 min
2018/07/13 Share

上篇梳理了作用域的相关基础知识,要理解闭包,作用域是相当重要的一环。闭包一直以来总是一个会出现些怪异问题的东西,之所以怪异是因为自己还是没有理解透彻,这次就来梳理一下闭包的相关问题。

闭包在JS中的地位可以说是相当重要的了,总是会在各种论坛博客上面看到各种前辈们所写的关于闭包的解析,自己也是似懂非懂,总以为自己懂了,然后偶然间突然想起来要问问自己却又说不出个所以然。所以现在就来系统的阐述一下。

何为闭包

我们从前辈的总结中得到这样一个定义:当函数可以记住并且访问所在的词法作用域时,就产生了闭包,即使函数是在当前作用域之外执行。

先来看一个例子:

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

这是一个闭包吗?

这段代码和之前嵌套作用域所使用的代码很类似,bar访问上级作用域并找到a,但是这和上面的定义似乎不是很完全贴合,有人说这属于一种闭包,有人说这并不算严格的闭包,这里我觉得,我们所关心的是闭包所带来的作用,而非是作为标准委员会来制作标准,所以这里不再争论。

再看下一个例子:

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

这其实就是一个非常典型的闭包了。
来分析一下这段简单的代码的执行过程:
很显然,函数bar能够访问foo的内部作用域,然后foo将bar本身传递了出去,即bar函数作为返回值,中间的调用执行我们不再关注,需要注意的是,函数foo执行完毕后,其作用域是否有销毁?观察运行结果,我们发现是没有的,函数baz(其实也就是bar)在执行的时候并非在其定义时所在的词法作用域,但是他依然保留了foo内部作用域的引用,而这个引用,就叫做闭包。

这个函数在定义时词法作用域以外的地方被调用,闭包使得函数可以继续访问定义的词法作用域。且无论通过何手段将内部函数传递到其所在词法作用域以外,他都会保留对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包/

应用

事实上,在实际写代码的时候我们可以发现闭包在代码中并不是一个陌生的东西,来看一个例子:

function wait(message){
settimeout(function timer(){message},1000)
}
wait("hello")

将一个内部函数timer传递给settimeout,timer具有涵盖wait(…)作用域的闭包,因此还保留有对变量message的引用
在wait执行1000ms后,他的内部作用于并不会消失,timer依然保留着wait作用域的闭包。
更深入的来说,从settimeout的异步运行机制我们可以知道,他会被送入macortask等待执行,settimeout会持有一个对参数的引用,也就是timer,函数会调用这个函数,也就是timer函数,词法作用域保持完整。

事实上,只要使用了回调函数,那么就在使用闭包。

IIFE

一般认为立即执行函数是典型的闭包例子,但是若以之前试图解释的定义来看,这似乎并非是一个无异议的闭包实例
举个🎂;

var a = 2
(function IIFE(){console.log(a)})()

我们其实可以这样理解,首先,函数并非是在所定义的词法作用域之外执行,其次我们可以理解为函数在执行时向上级作用域(这里也是全局作用域)找到了a,只是通过了普通的词法作用域查找而非闭包中找到的。

但是正像之前所说的那样有不同的概念,他确实可以说是创建了闭包。因为他保存了自身的状态。

闭包与循环

function counter (){
for(var i = 0;i<5;i++){
settimeout(()=>{console.log(i)},1000)
}
}

依然是之前就提到过的一个问题,虽然之前已经解释过这里还是再放一下:
现在若要使用闭包解决:

function counter (){
for(var i = 0;i<5;i++){
((i)=> settimeout(()=>{console.log(i),1000))(i)
}
}

闭包与模块设计

在ES6的import export诞生之前,可以说模块的设计可能会直接接触最最典型的闭包应用

function Moudle (){
var something = "something";
var somethingElse = "sometingElse"
function saySomething (){
console.log(something)
}
function saySomethingElse (){
console.log(somethingElse)
}
return {
saySomething:saySomething,
somethingElse:somethingElse
}
}

这是一个在之前可能会写的代码,他很直观地体现了闭包的作用。

当然这只是一个方便理解的demo,实际的使用中,这样是肯定不能用的。

但我们可以以此归纳出:

  1. 为创建内部作用域而调用了一个包装函数
  2. 包装函数返回值必须至少包括一个对内部函数的引用,这样就会创建包含整个包装函数内部作用域的闭包。

    小结

我们现在来简要的总结一下:
可以说:当函数能够记住并访问所在的词法作用域时,即使函数在他的词法作用域之外执行,这事就产生了闭包。


下篇来梳理下this

CATALOG
  1. 1. 何为闭包
  2. 2. 应用
    1. 2.1. IIFE
    2. 2.2. 闭包与循环
    3. 2.3. 闭包与模块设计
  3. 3. 小结