
JavaScript的闭包原来是这样一回事
想要明白闭包,可不是一件很简单的事情,接下来,我将会从上下文和作用域开始,一步一步讲清楚闭包的概念,希望对各位读者能够有所启发。
执行上下文和作用域
执行上下文(execution context),简称上下文(context),这是JavaScript中最重要的概念。上下文是什么,假如有以下两个场景:
场景一:
😄 纪晓岚:和大人走的那么着急干嘛?
😖 和珅:肚子痛,我要去方便~
场景二:
😄 纪晓岚:和大人走的那么着急干嘛?
😌 和珅:有个官员想贿赂我两万两,让它给他个方便,我现在要去处理以下。
上述两个场景中,都有相同名字的变量“方便”,但显然两者的内容含义是不一样的,同一个变量,根据这个变量所处的上下文的不同,值也可能不同
全局上下文和函数上下文
JavaScript环境中,最外层的上下文叫做“全局上下文(Global Execution Context)”,不同环境对全局上下文的实现不同,在浏览器中,全局上下文是window。
每一个函数也都有一个函数上下文(Function Execution Context),当函数被调用时,就会创建一个函数上下文,并压入上下文栈(Context Stack),函数执行结束就把它弹出,我们来看一个具体例子:
function compare(value1, value2){
if(value1 < value2){
return -1
} else if(value1 > value2){
return 1
} else{
return 0
}
}
let result = compare(5, 10)
console.log(result) //> -1
假设我们在浏览器中执行以上代码,可以看到,函数compare和变量result处于全局上下文中,当调用函数compare()时,会把它的函数上下文给压入栈中:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgUhEoVU-1635061893402)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20210912155300826.png)]](https://i-blog.csdnimg.cn/blog_migrate/9ed69d953c3eb40a0759d2fd0e96b8e1.png)
当执行完毕之后,就把它弹出栈:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9zuzeWQH-1635061893403)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20210912155317193.png)]](https://i-blog.csdnimg.cn/blog_migrate/23cdf754980de3775de109f7a308e706.png)
我们稍微改造一下代码,调用console.log()函数:
function compare(value1, value2) {
console.log('Starting compare...') // 增加一行输出
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
let result = compare(5, 10)
console.log(result)
//> Starting compare...
//> -1
在compare()函数中调用console.log()函数,那么上下文栈就会:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dw5iCkMV-1635061893405)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20210912160121690.png)]](https://i-blog.csdnimg.cn/blog_migrate/110529ef58ee51c3eebf199a20bbada9.png)
当console.log()函数执行结束,就会把它的上下文从栈中弹出,然后继续执行compare()函数
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TviZ2eMS-1635061893407)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20210912160346222.png)]](https://i-blog.csdnimg.cn/blog_migrate/6e2cdc92009bba9791d8ba2bfaea4fa3.png)
作用域和作用域链
上下文用来确定一个变量的值或者函数的行为(可以访问哪些数据),而作用域是用来确定一个变量/函数的可见性(visibility),也就是,确定这个变量/函数可以被访问的范围。看个例子:
var myName = 'window'
function printMyName() {
console.log(myName)
}
printMyName() //> window
console.log(myName) //> window
我们在浏览器中执行上述代码,首先我们用var定义了一个变量myName,这个变量可以在全局上下文中被访问到,也可以在printMyName()函数上下文中访问到。
我们增加一行代码:
var myName = 'window'
function printMyName() {
var myName = 'function' // 在函数中定义相同名字变量
console.log(myName)
}
printMyName() //> function
console.log(myName) //> window
这一次我们看到printMyName()函数输出的结果不一样了,它访问的是函数里面的myName,而不会去访问全局上下文中的myName,这得益于作用域链:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-miYrsM4G-1635061893408)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20210912170349539.png)]](https://i-blog.csdnimg.cn/blog_migrate/004d13662ae3e4a03d7c2ea8f9b1b892.png)
在printMyName的函数上下文中,它创建了一个作用域链,在printMyName()函数中解析myName时,首先会在当前作用域里去寻找myName是在哪里定义的。如果当前作用域找到存在myName变量定义的话,那么就会使用当前作用域的myName变量。如果找不到的话,则会向上查找(Look-up),即沿着作用域链向上查找。
VO和AO
首先我们回顾这张图
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oW6XmKSj-1635061893410)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20210912170349539.png)]](https://i-blog.csdnimg.cn/blog_migrate/fcc117122060b0ccd4598ac83141bfd0.png)
在上一节中,我们介绍到了作用域链,为了行文方便,简单略过了两件东西,见下图:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XWaMzwY9-1635061893411)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20210915235422834.png)]](https://i-blog.csdnimg.cn/blog_migrate/69014b2d78aae19417352364fcf4766e.png)
这两样东西是作用域链上的,它们其实是两个对象,正式名称叫做变量对象(VO, Variable Object),它存放着当前的所有变量/函数,以及可执行代码,在作用域链最前面的变量对象VO的代码总是会被执行。
在函数中,则把**激活变量(AO, Activation Object)**作为变量对象VO,激活变量不太一样,当进入一个函数执行时,激活变量AO会被创建,并且它首先定义的第一个变量是arguments,接着就是实际参数,再然后就是函数中的其他定义的变量/函数。
我们补充了关于VO和AO的概念:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4C6gr9ZI-1635061893412)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20211003111041208.png)]](https://i-blog.csdnimg.cn/blog_migrate/182fc909029bf50e78c9a2759d83903a.png)
刚才我们提到在作用域链最前面的变量对象VO的代码总是会被执行,也就是图中作用域第0对应的VO,其包含的代码会被执行。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QUimBLJO-1635061893413)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20211003111100897.png)]](https://i-blog.csdnimg.cn/blog_migrate/fab3b89ab03886b3c1a7a6c16c25db7b.png)
这里的arguments是为空的,表示函数没有参数,假如有传参的话:
function printMyName(value1, value2) {
//...
}
printMyName(5, 10)
则其对应的AO为:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QIZU9ZOq-1635061893413)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20211003111002190.png)]](https://i-blog.csdnimg.cn/blog_migrate/e27e77677170eb3a540b517ce5147a40.png)
小结
总结以下刚才函数执行的整个过程:先创建函数上下文,压入上下文栈中,再创建函数上下文对应的作用域链,作用域链包含一整个调用过程中的VO(或AO),一个VO(或AO)包含对应作用域的所有变量/函数(以及可执行代码)。函数执行结束后,函数上下文会被弹出栈,AO、作用域链会被销毁。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcGPpour-1635061893414)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/image-20211003110937587.png)]](https://i-blog.csdnimg.cn/blog_migrate/3dcf177f2a372ee3f7b86bca585e4f0c.png)
全局VO不一定被销毁,可能会被其他函数作用域链引用
闭包的不同
闭包(Closure)的定义很简单,闭包是函数,它引用了其他作用域中的变量。闭包一词还是挺形象的,它将其他作用域的变量“封闭包围”起来。我们来看一个经典的例子:
function createComparisionFunction(propertyName) {
return function (o1, o2) {
let v1 = o1[propertyName]
let v2 = o2[propertyName]
switch (true) {
case v1 < v2:
return -1
case v1 > v2:
return 1
default:
return 0
}
}
}
在这个例子中,createComparisionFunction函数返回了一个闭包,这个闭包引用了createComparisionFunction函数的propertyName变量。当我们执行以下代码时:
let compare = createComparisionFunction('age')
let result = compare({ age: 1 }, { age: 2 })
函数上下文、作用域链表示如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mvs5TUtY-1635061893415)(JavaScript%E7%9A%84%E9%97%AD%E5%8C%85%E5%8E%9F%E6%9D%A5%E6%98%AF%E8%BF%99%E6%A0%B7%E4%B8%80%E5%9B%9E%E4%BA%8B.assets/closure-16332322870551.jpg)]](https://i-blog.csdnimg.cn/blog_migrate/43070a30561f940fc9ae9502cb5ce2d2.png)
可以看到,在闭包中执行上下文的作用域链中,引用了全局VO和createComparisionFunction函数的AO,这样闭包可以访问全局和createComparisionFunction函数的所有变量了。
但是,这样带来了一个副作用(side effect)——createComparisionFunction函数执行结束后,上下文会被销毁,但是它的AO却会留在内存中,因为它被闭包函数所引用,必须得等闭包函数销毁后,AO才会被收回释放,比如:
let compare = createComparisionFunction('age')
let result = compare({ age: 1 }, { age: 2 })
compare = null // 解除引用,闭包函数和引用的AO都会被垃圾回收器回收释放
引用其他作用域的闭包
刚才我们提到,闭包是“引用其他作用域变量”的函数,我们比较常看到的是函数作用域,但其实闭包也可以引用块级作用域的变量:
let foo
{
let name = 'goods'
foo = function(){
console.log(name)
}
}
浏览器优化
接上面的例子,在运行闭包之后,我们发现“包围”起来的变量中有些其实是不需要,比如argument:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RN2m5OTG-1635061893417)(C:\Users\Kyle\AppData\Roaming\Typora\typora-user-images\image-20211024141444331.png)]](https://i-blog.csdnimg.cn/blog_migrate/1ede72d4c7eb97447066f604a6e8cef2.png)
现代浏览器会做一个优化,把明显不需要的变量给踢掉。我们来看一个简单的例子:
function breadProduct(){
let name = 'deliciousBread'
let productDate = new Date()
function bread(){
console.log(name)
}
return bread
}
const mybread = breadProduct()
我们来看一下,当执行结束breadProduct结束,mybread中使用[[Scopes]]去保存作用域链:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v55vZEVU-1635061893418)(C:\Users\Kyle\AppData\Roaming\Typora\typora-user-images\image-20211024145252982.png)]](https://i-blog.csdnimg.cn/blog_migrate/e23414202a228271ff0083db6fc2d4a8.png)
我们主要关注到,一个是breadProduct函数的AO,一个是全局作用域:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KbL6IzxT-1635061893420)(C:\Users\Kyle\AppData\Roaming\Typora\typora-user-images\image-20211024145417895.png)]](https://i-blog.csdnimg.cn/blog_migrate/59c9d65d23975b866b400c9136d9177b.png)
当时,我们看一下breadProduct中,name被显式地引用到,所以它留下来了,但是age没有,就清理掉了。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LABslK5D-1635061893421)(C:\Users\Kyle\AppData\Roaming\Typora\typora-user-images\image-20211024145636858.png)]](https://i-blog.csdnimg.cn/blog_migrate/3433a7872e6bbe0453978cf1b66be350.png)
这样做的目的很简单,就是节省内存空间,当然这是现代浏览器的一种可选的优化手段,而不是JS规范的必要要求。在以前的浏览器中,尤其是老式终端设备的浏览器中,会保留所有的变量。
例外:当使用eval
用于eval会动态执行语句,因此没办法确定哪个变量会不会被应用,因此浏览器会保留所有变量,我们稍微改一下代码:
function breadProduct() {
let name = 'deliciousBread'
let productDate = new Date()
function bread() {
eval()
}
return bread
}
const mybread = breadProduct()
只要有eval(),所有变量都会被保存下来:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfRV0Iq2-1635061893422)(C:\Users\Kyle\AppData\Roaming\Typora\typora-user-images\image-20211024150302050.png)]](https://i-blog.csdnimg.cn/blog_migrate/c9960a65237ff3cd2b4f4b09fbf5e67b.png)
注意:同一个AO
当我们有多个闭包时,我们来看看会发生什么事情:
function breadProduct() {
let name = 'deliciousBread'
let productDate = new Date()
function bread() {
console.log(name)
}
function otherBread(){
console.log(productDate)
}
return bread
}
const mybread = breadProduct()
虽然otherBread()函数没有被返回,并且随着breadProduct()函数执行结束而被清除,但是它显式引用了productDate,所有productDate被保留了下来:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eEzSoENG-1635061893423)(C:\Users\Kyle\AppData\Roaming\Typora\typora-user-images\image-20211024150904636.png)]](https://i-blog.csdnimg.cn/blog_migrate/100d3531f6c41cb93f0793c7d572e1be.png)
this引用
在闭包中,this引用情况有点复杂,闭包一般是匿名函数,匿名函数中不会自动将对象绑定到this中,看一个例子:
window.identify = 'The window'
let object = {
identify: 'My Object',
getIdentify(){
return function(){
return this.identify
}
}
}
console.log(object.getIdentify()()) //> 'The window'
我们可以通过定义一个引用this的变量来解决:
window.identify = 'The window'
let object = {
identify: 'My Object',
getIdentify() {
let that = this
return function () {
return that.identify
}
}
}
object.getIdentify()()
内存泄漏
闭包的最大好处就是它能够保存其他作用域的变量,当然,使用不当也会造成内存泄漏,试看一例:
function assignHandler(){
let element = document.getElementById('someElement')
element.onclick = () => console.log(element.id)
}
element引用了某一个DOM节点,并且这个节点的onclick方法引用了element,这样一来element就没办法被GC清除掉,假设element是一个很大的对象的话,那么很可能造成内存泄漏。我们可以修改一下引用关系:
function assignHandler(){
let element = document.getElementById('someElement')
let id = element.id
element.onclick = () => console.log(id)
element = null
}
思考题
试问以下题目会不会造成内存泄漏:
let big = null
let count = 0
setInterval(function () {
let bigReference = big
function unused() {
if (bigReference) {
}
}
big = {
count: count++,
place: new Array(2e9),
introduce: function () {
console.log('I am big')
}
}
}, 1000)
总结
All in all, 闭包就是引用其他作用域变量的一个函数,要明白它,需要从执行上下文到作用域链到AO。闭包的好处有很多,最大的好处就是它能够保存其他作用域的变量,以便后续使用,典型的引用场景有Module、柯里化等等。当然,闭包也有内存泄漏的风险,因此,谨慎使用。
参考
[1] What is the Difference Between Scope and Context in JavaScript?
[2] Professional JavaScript For Web Developers : Chapter4/Chapter10
[3] JavaScript 的静态作用域链与“动态”闭包链
[4] You don’t know JavaScript Yet: Scope and Closure Chapter 7
本文详细讲解了JavaScript中的执行上下文、作用域、作用域链、变量对象(VO/AO)以及闭包的概念,强调了闭包在引用其他作用域变量时如何影响内存。通过实例分析了闭包可能导致的内存泄漏问题,并探讨了`this`在闭包中的引用特点。最后,提醒开发者在利用闭包的同时要注意内存管理,防止潜在的性能问题。
473

被折叠的 条评论
为什么被折叠?



