我们的《深入浅出 javascript》系列终于来到了 闭包 这个章节,起初学习 javascript 的时候感觉全网都是讲这个概念的文章,然而我看了不知道有多少篇,依然还是很迷茫。
随着工作时间的增长,好像自然而言就理解了这个概念。其实,要想讲明白闭包,只靠一篇文章是远远不够的,你只有阅读了前面四篇文章,并且对执行上下文,作用域链,词法作用域这些概念完全理解了之后,才能彻底明白闭包到底是怎么一回事,废话不多说,我们直接进入文章(没看过前面系列文章的同学先去补一下哦)。
我们先给概念。那么到底什么是闭包呢?网上充斥了各种闭包的概念,包括 MDN 上面的定义:
读懂这个句子可能都要几分钟。而我个人比较倾向阮一峰老师的定义:
闭包就是能够读取其他函数内部变量的函数
我在此基础上再给大家丰富一下:
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
好,请大家记住这个概念,并在心里默念 10 次。
接下来我们上代码看一个例子:
function foo() {
var myName = "wens"
let test1 = 1
const test2 = 2
var innerBar = {getName:function(){console.log(test1)
return myName
},
setName:function(newName){myName = newName}
}
return innerBar
}
var bar = foo()
bar.setName("leon")
bar.getName()
console.log(bar.getName())
当代码执行到第 14 行 return innerBar 的时候,我们来画一下这个时刻的调用栈,相信前面的文章都学会的同学也一定能画的出来
从上面的代码可以看出,innerBar 是一个对象,包含了 getName 和 setName 的两个方法。这两个方法都是在 foo 函数内部定义的,并且这两个方法内部都使用了 myName 和 test1 两个变量。
根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 myName 和 test1。
所以当 foo 函数执行完成之后,其整个调用栈的状态如下图所示:
从上图可以看出,foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。
而用来“装载”这两个变量的就是闭包,并且除了 setName 和 getName 函数之外,其他任何地方都无法访问。
那这些闭包是如何使用的呢?当执行到 bar.setName(“leon”) 这句代码时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量
从图中可以看出,setName 的执行上下文中没有 myName 变量,foo 函数的闭包中包含了变量 myName,所以调用 setName 时,会修改 foo 闭包中的 myName 变量的值。
同样的流程,当调用 bar.getName 的时候,所访问的变量 myName 也是位于 foo 函数闭包中的。
我们通过 Chrome“开发者工具”来看看闭包的情况,在 bar 函数任意地方打上断点,然后刷新页面,可以看到如下内容:
从图中可以看出来,当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:Local 就是当前的 getName 函数的作用域,Closure(foo) 是指 foo 函数的闭包,最下面的 Global 就是指全局作用域,从“Local–>Closure(foo)–>Global”就是一个完整的作用域链。
所以说,工作中碰到复杂的函数我们要学会通过开发者工具来查看实际代码作用域链的情况。这里我再次强推一个国外小哥哥的网站,可以可视化查看调用栈的进栈和出栈情况,包括闭包的生成 https://ui.dev/javascript-vis…。先看文章再去他那边实操一下,学不会都难,哈哈哈。
好了,今日份的闭包文章就完结了,请同学们好好消化。下一篇文章我们介绍 —— this