前端小秘密系列之闭包
本篇文章,我们来说说老(bei)生(xie)常(lan)谈(le)的闭包,很多文章包括一些权威书籍中对于闭包的解释不尽相同,每个人的理解也都不一样。并且在其他语言中,也有对闭包的不同实现,让我们来看看
Javascript
中是如何实现闭包的以及有哪些特性。
直接进入主题,上一段简短的代码:
function outer(count) {var temp = new Array(count)function log() {console.log(temp)}log()function inner() {console.log('done')}return inner
}var o = {}for(var i = 0; i < 1000000; i++) {o["f"+i] = outer(i)
}
复制代码
如果你不知道这段代码可能带来的问题,那么这篇文章就值得你读一读。
执行上下文 & 作用域链
我们先把上面的问题放一放,先让我们来看一看下面这段简单的代码:
function outer() {var b = 2function inner() {console.log(a, b)}return inner
}var a = 1
var inner = outer()
inner()
复制代码
在 JS引擎
中,是通过执行上下文栈来管理和执行代码的。上述代码的伪执行过程如下(本节内容主要参考冴羽大大的系列文章):
0、程序开始
ECStack = []
复制代码
1、创建全局上下文globalContext,并将其入栈
ECStack = [globalContext
]
复制代码
2、在执行之前初始化这个全局上下文
globalContext = {VO: {a: undefined,inner: undefined,outer: function outer() {...}},Scope: [globalContext]
}
复制代码
初始化作用域链属性 Scope
为 [globalContext]
,此时代码还没有执行,由于变量提升的缘故, inner
和 a
变量为 undefined
,需要注意的是,这个时候,outer
函数的作用域 [[scope]]
内部属性已确定(静态作用域):
outer.[[scope]] = [globalContext.VO
]
复制代码
3、执行
globalContext
全局上下文
在执行过程中,不断改变 VO
,执行到 a = 1
语句,将 VO
中的 a
置为 1
,执行到 inner = outer()
语句,执行 outer
函数,进入 outer
函数的执行上下文。
4、创建outerContext执行上下文,将其入栈
ECStack = [outerContext,globalContext
]
复制代码
5、初始化
outerContext
执行上下文
outerContext = {VO: {b: undefined,inner: function inner() {...}},Scope: [VO, globalContext.VO]
}
复制代码
初始化作用域链属性 Scope
为 [VO].concat(outer.[[scope]])
即 [VO, globalContext.VO]
。 并在此时,确定 inner
函数的 [[scope]]
属性:
inner.[[scope]] = [outerContext.VO,globalContext.VO
]
复制代码
6、执行
outerContext
上下文
执行语句 b = 2
,将 VO
中的 b
置为 2
,最后返回 inner
。
7、
outerContext
执行完毕,出栈,继续回到globalContext
执行余下的代码
ECStack = [globalContext
]
复制代码
继续执行 inner = outer()
语句的赋值操作,将 outer
函数的返回结果赋给 inner
变量。
执行 inner()
语句,进入 inner
函数的执行上下文。
8、创建
innerContext
执行上下文,将其入栈
ECStack = [innerContext,globalContext
]
复制代码
9、初始化
innerContext
执行上下文
innerContext = {VO: {},Scope: [VO, outerContext.VO, globalContext.VO]
}
复制代码
初始化作用域链属性 Scope
为 [VO].concat(inner.[[scope]])
即 [VO, outerContext.VO, globalContext.VO]
。
10、执行
innerContext
上下文
执行语句 console.log(a, b)
,VO
中没有变量 a
,往上查找到 outerContext.VO
,找到变量 a
,VO
中没有变量 b
,依次往上查找到 globalContext.VO
,找到变量 b
。执行 console.log
函数,这里同样涉及到 变量console
的作用域链查找,console.log
函数的执行上下文切换,不再赘述。
11、
globalContext
执行完毕,出栈,程序结束
ECStack = []
复制代码
在第7步中,outerContext
执行完毕后,虽然其已出栈并在随后被垃圾回收机制回收,但是可以看到 innerContext.Scope
仍有对 outerContext.VO
的引用。当 outerContext
被回收后,outerContext.VO
并不会被回收,如下图:
这就使得我们在执行 inner
函数时仍可以通过其作用域链访问到已执行完毕的 outer
函数中的变量,这就是闭包。
通过执行上下文和作用域链相关知识,我们引出了闭包的概念,让我们继续。
在第5步中,我们说到,初始化 outerContext
的过程中,同时确定了 inner
函数的作用域属性 [[scope]]
为 [outerContext.VO, globalContext.VO]
,这其实是不准确的。
我们稍微改动下加上两句代码:
function outer() {var b = 2var c = new Array(100000).join('*')var d = 3function inner() {console.log(a, b)}return inner
}var a = 1
var inner = outer()
inner()
复制代码
聪明的你会发现,变量 c
和 d
在inner中并不会用到,如果按照如上所述,将 inner
函数的 [[scope]]
属性置为 [outerContext.VO, globalContext.VO]
,那么变量 c
(准确的说应该是变量 c
指向的那块内存,下同)只能一直等到 inner
函数执行完毕后才会被销毁,如果 inner
函数一直不执行的话,new Array(100000).join('*')
所占用的内存一直无法被释放。
那么,你可能会想,我们在确定 inner
函数 [[scope]]
属性的时候,只引用 inner
函数体内用到的变量不就好了吗?实际上,JS引擎
和你一样聪明,就是这么干的,在 Chrome
调试工具下:
可以看到,并没有对变量 c
的引用,我们可以认为 inner
函数 [[scope]]
属性为:
inner.[[scope]] = [Closure(outerContext.VO),globalContext.VO
]
复制代码
这里,我们用 Closure
这样一个函数来表示得到内部函数体中(包括内部函数中的内部函数,一直下去...)引用外部函数变量的集合,即闭包。
共享闭包
让我们继续前进的脚步,把上面的代码再稍微改动下:
function outer() {var b = 2var c = new Array(100000).join('*')var d = 3function log() {console.log(c)}function inner() {console.log(a, b)}log()return inner
}var a = 1
var inner = outer()
inner()
复制代码
这里,我们只是加了一个 log
函数,并将变量 c
打印出来。对于 inner
函数来说,并没有什么改变,果真如此吗?我们看下 Chrome
调试工具下作用域和闭包相关信息。
outer函数执行之前:
outer函数执行完成:
咦,我们可以看到 inner
函数中的闭包中竟然包含了变量 c
!但是 inner
函数中并没有用到 c
啊,你可能隐隐发现了什么,是的,我们在 log
函数中引用了变量 c
,这竟然会影响到 inner
函数的闭包。
在前文中,我们说到确定 inner
函数 [[scope]]
属性时,会通过 Closure
函数得到 inner
函数体内引用到的所有闭包变量集合,那有多个内部函数呢?
其实,JS引擎
会通过 Clousre
函数得到 outer
函数下所有内部函数体中用到的闭包变量集合 Closure(outerContext.VO)
,并且所有的内部函数的 [[scope]]
属性都引用这个共同的闭包,所以:
inner.[[scope]] = [Closure(outerContext.VO),globalContext.VO
]log.[[scope]] = [Closure(outerContext.VO),globalContext.VO
]Closure(outerContext.VO) = { b, c }
复制代码
让我们来看看 log
函数的闭包信息,同样也有变量 b
:
这里,你可能会有疑问,变量 a
哪里去了,其实变量 a
在 globalContext
下。
读到这里,细心的你会发现,这和文章开头给出的代码几乎一毛一样啊,那究竟会带来什么问题呢,我想你应该知道了:内存泄露!
让我们回到文章开头的那段代码,返回的 inner
函数中,一直引用着 temp
变量,在 inner
函数不执行的情况下,temp
变量一直无法被垃圾回收。
我们再稍微改下代码:
function outer(count) {var temp = new Array(count)function log() {console.log(temp)}log()function inner() {var message = 'done'return function innermost() {console.log(message)}}return inner()
}var o = {}for(var i = 0; i < 1000000; i++) {o["f"+i] = outer(i)
}
复制代码
这里,我们在 inner
函数里面又包了一层,那最终返回的 innermost
还有对 temp
变量的引用吗?
按照前面关于执行上下文相关内容的逻辑分析下去,其实是有的。innermost
的 [[scope]]
属性如下:
innermost.[[scope]] = [Closure(innerContext.VO): { message },Closure(outerContext.VO): { temp },globalContext
]
复制代码
当然,你可能会说,只要 inner
函数执行完成后,这些内存就会被回收掉。OK,那我们再来看一个更经典的例子:
var theThing = null;
var replaceThing = function () {var originalThing = theThing;var unused = function () {if (originalThing)console.log("hi");};theThing = {longStr: new Array(1000000).join('*'),someMethod: function () {console.log(someMessage);}};
};
setInterval(replaceThing, 1000);
复制代码
unused
函数引用了 originalThing
,由于共享闭包的特性,theThing.someMethod
函数的闭包中也包含了对 originalThing
的引用,而 originalThing
是上一个 theThing
,也就是说下一个 theThing
引用者上一个 theThing
,形成了一个链。并随着 setInterval
的执行,这个链越来越长,最终导致内存泄露,如下:
如果把间隔时间改小点,分分钟 out of memory
。
这个例子来源于这里,建议大家都点进去读一读(我记得之前有小伙伴翻译了这篇文章的,一时找不到了,有知道中文翻译链接的小伙伴在评论里贴一下哈)。
Real Local Variable
vs Context Variable
Real Local Variable
,直译过来就是真正的局部变量,在这里变量 d
就是 Real Local Variable
,在C++层面,它可以直接分配在栈上,随着 inner
函数执行完毕的出栈操作而被立即回收掉,不需要后面垃圾回收机制的干预。
Context Variable
,上下文环境变量或者称之为闭包变量,在这里变量 b
就是 Context Variable
, 在C++层面,它一定分配在堆上,尽管这里它是一个基本类型。
那变量 c
呢,你可以认为它是一个 Real Local Variable
,只是在栈上存的是指向这个 new Array()
的内存地址,而 new Array()
的实际内容是存在堆上的。
内存分布如下:
通过上面的分析,我们在很多文章中经常看到的 基本类型分布在栈上,引用类型分布在堆上
这句话明显是错误的,对于被闭包引用的变量,不管其是什么类型,肯定是分配在堆上的。
eval
与闭包
前文中已经提到,JS引擎
会分析所有内部函数体中引用了哪些外部函数的变量,但是对于 eval
的直接调用是无法分析的。因为无法预料到 eval
中可能会访问那些变量,所以会把外部函数中的所有变量都囊括进来。
function outer() {var b = 2var c = new Array(100000).join('*')var d = 3function inner() {eval("console.log(1)")}return inner
}var a = 1
var inner = outer()
inner()
复制代码
JS引擎
内心OS是这样的:eval
这家伙什么事情都干的出来,你们(局部变量)统统不准走!
如果,你在层层嵌套的函数下面来一个 eval
,那么 eval
所在函数的所有父级函数中的变量都无法被释放掉,想想就可怕...
那对于 eval
的间接调用呢?
function outer() {var b = 2var c = new Array(100000).join('*')var d = 3function inner() {(0, eval)("console.log(a)") // 输出1}return inner
}var a = 1
var inner = outer()
inner()
复制代码
这时 JS引擎
内心OS又是这样的:eval
是谁,不认识,你们(局部变量)都回家收衣服吧...
其实,对于 eval
和 function
的组合还有各种姿势,比如:
function outer() {var b = 2var c = new Array(100000).join('*')var d = 3return eval("(function() { console.log(a) })")// return (0,eval)("(function() { console.log(a) })")// return (function(){ return eval("(function(){ console.log(a) })") })()// ...// 更多姿势留待各位自己去发掘和尝试,逃...
}var a = 1
var inner = outer()
inner()
复制代码
到这里就写完了,希望各位对闭包有一个新的认识和见解。
最后欢迎各路大佬们啪啪打脸...
转载于:https://juejin.im/post/5c723d90f265da2dc0065bdb
前端小秘密系列之闭包相关推荐
- 前端科普系列(2):Node.js 换个角度看世界,
[前端科普系列]往期精彩内容: 前端科普系列(1):很有趣的一篇前端简史,作者有心了~主要介绍 web 前端发展的历史.大事件. 本文为系列文章(2),主要介绍 Node.js 的前世今生.核心科技以 ...
- 前端工程化系列[06]-Yeoman脚手架核心机制
在前端工程化系列[05] Yeoman脚手架使用入门这边文章中,对Yeoman的使用做了简单的入门介绍,这篇文章我们将接着探讨Yeoman这个脚手架工具内部的核心机制,主要包括以下内容 ❏ Yeoma ...
- gif透明背景动画_前端基础系列之bmp、jpg、png、gif、svg常用图片格式浅谈(二)...
IT客栈 作者:大腰子 bmp.jpg.png.gif.svg常用图片格式 之前为大家介绍了几种WEB前端常用的图片格式,对比了它们的特点,参见<前端基础系列之bmp.jpg.png.gif.s ...
- 前端工程化系列[03]-Grunt构建工具的运转机制
在前端工程化系列[02]-Grunt构建工具的基本使用这篇文章中,已经对Grunt做了简单的介绍,此外,我们还知道了该如何来安装Grunt环境,以及使用一些常见的插件了,这篇文章主要介绍Grunt的核 ...
- php配合jade使用,前端自动化系列(四)之jade预编译html
刚开始写这篇文章的时候: 其实我是拒绝的: 因为在 前端自动化系列(二)之less.scss.sass.stylus css预处理器 中: 我已经表明了我的态度: 我是不喜欢那种靠缩进来体现等级层次感 ...
- 前端安全系列(二):如何防止CSRF攻击?
背景 随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点.在移动互联网时代,前端人员除了传统的 XSS.CSRF 等安全问题之外,又时常遭遇网络劫持 ...
- 好程序员前端教程之JavaScript闭包和匿名函数的关系详解...
好程序员前端教程之JavaScript闭包和匿名函数的关系详解 本文讲的是关于JavaScript闭包和匿名函数两者之间的关系,从匿名函数概念到立即执行函数,最后到闭包.下面一起来看看文章分析,希望你 ...
- 前端安全系列(一):如何防止XSS攻击?
前端安全 随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点.在移动互联网时代,前端人员除了传统的 XSS.CSRF 等安全问题之外,又时常遭遇网络 ...
- 前端求职系列:如何写一份小程序简历(二)
前言 在之前前端求职系列:如何写一份好的简历(一),我们清楚了如何写一份好的简历.那么当我们不满足于纸质简历想实现一个线上的简历如何实现呢?今天给大家带来一个微信小程序简历. 一.项目展示 说明 因为 ...
最新文章
- 最精简写法→去掉任意多个空行
- Spring bean配置继承
- 无服务器安全性:将其置于自动驾驶仪上
- 使用WORD封面自带模板?
- 最全MD5 密码破解 碰撞 网站
- 【目标跟踪】|MOSSE原理及对应代码解释 matlab C
- String类常用方法
- c语言max比较字符串,c语言中能不能用max函数求三个数的最大者呢?
- selenium +geogle chomer批量爬取(百度知道、爱问、360、悟空问答、搜狗)的第一条结果
- 总结今年一些公司的待遇
- ccd摄像机基础知识
- 前世界银行经济学家质疑华为财报
- CNN-卷积神经网络
- 《控制系统设计指南》_George Ellis著_部分章节读书笔记
- 程序员去美国工作:工作在加州的华为
- 如何保留优秀的程序员
- 统计单词出现频次(例:See You Again)
- Linux普通用户无法执行docker命令的解决方法
- 27岁乌克兰数学家在俄自杀,留下给疯狂世界的遗书!
- 2021 年 15 个受欢迎的 CMS 平台(比较)
热门文章
- Python3爬虫入门之beautifulsoup库的使用
- Flutter 入门指北(Part 9)之弹窗和提示(SnackBar、BottomSheet、Dialog)
- python网络编程需要学什么,python网络编程学习笔记(五):socket的一些补充 Python 网络编程需要学习哪些网络相关的知识...
- windows10怎么锁定计算机,别让Windows 10锁住亲友
- mysql找不到sys_解决方法:①MySQL 闪退 ②服务列表里找不到MySQL ③MySQL服务无法启动...
- Docker 安装 MongoDB教程
- HttpClient工具类 HttpClientUtils.java
- [Ext JS 4] Extjs 图表 Legend(图例)的分行与分列显示
- 使用python开发网页游戏_四大游戏编程网站,边玩游戏,边学Python,拒绝枯燥快乐编程...
- 蓝牙怎么区分单模和双模_小院闲聊#01#——蓝牙的发展和不同蓝牙之间的关系...