在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。
作用域链具体是什么样呢?
静态作用域链
比如这样一段代码
function func() {
const guang = 'guang';
function func2() {
const ssh = 'ssh';
{
function func3 () {
const suzhe = 'suzhe';
}
}
}
}
其中,有 guang、ssh、suzhe 3个变量,有 func、func2、func3 3个函数,还有一个块,他们之间的作用域链可以用babel查看一下。
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const code = `
function func() {
const guang = 'guang';
function func2() {
const ssh = 'ssh';
{
function func3 () {
const suzhe = 'suzhe';
}
}
}
}
`;
const ast = parser.parse(code);
traverse(ast, {
FunctionDeclaration (path) {
if (path.get('id.name').node === 'func3') {
console.log(path.scope.dump());
}
}})
结果是
用图可视化一下就是这样的
函数和块的作用域内的变量声明会在作用域 (scope) 内创建一个绑定(变量名绑定到具体的值,也就是 binding),然后其余地方可以引用 (refer) 这个 binding,这样就是静态作用域链的变量访问顺序。
为什么叫“静态”呢?
因为这样的嵌套关系是分析代码就可以得出的,不需要运行,按照这种顺序访问变量的链就是静态作用域链,这种链的好处是可以直观的知道变量之间的引用关系。
相对的,还有动态作用域链,也就是作用域的引用关系与嵌套关系无关,与执行顺序有关,会在执行的时候动态创建不同函数、块的作用域的引用关系。缺点就是不直观,没法静态分析。
静态作用域链是可以做静态分析的,比如我们刚刚用 babel 分析的 scope 链就是。所以绝大多数编程语言都是作用域链设计都是选择静态的顺序。
但是,JavaScript 除了静态作用域链外,还有一个特点就是函数可以作为返回值。比如
function func () {
const a = 1;
return function () {
console.log(a);
}
}
const f2 = func();
这就导致了一个问题,本来按照顺序创建调用一层层函数,按顺序创建和销毁作用域挺好的,但是如果内层函数返回了或者通过别的暴露出去了,那么外层函数销毁,内层函数却没有销毁,这时候怎么处理作用域,父作用域销不销毁?(比如这里的 func 调用结束要不要销毁作用域)
不按顺序的函数调用与闭包
比如把上面的代码做下改造,返回内部函数,然后在外面调用:
function func() {
const guang = 'guang';
function func2() {
const ssh = 'ssh';
function func3 () {
const suzhe = 'suzhe';
}
return func3;
}
return func2;
}
const func2 = func();
当调用 func2 的时候 func1 已经执行完了,这时候要不要销毁 ?为了解决这个问题,JavaScript 设计了闭包的机制。
闭包怎么设计? 先不看答案,考虑一下我们解决这个静态作用域链中的父作用域先于子作用域销毁怎么解决。
首先,父作用域要不要销毁?是不是父作用域不销毁就行了?
不行的,父作用域中有很多东西与子函数无关,为啥因为子函数没结束就一直常驻内存。这样肯定有性能问题,所以还是要销毁。但是销毁了父作用域不能影响子函数,所以要再创建个对象,要把子函数内引用(refer)的父作用域的变量打包里来,给子函数打包带走。
怎么让子函数打包带走? 设计个独特的属性,比如 [[Scopes]] ,用这个来放函数打包带走的用到的环境。并且这个属性得是一个栈,因为函数有子函数、子函数可能还有子函数,每次打包都要放在这里一个包,所以就要设计成一个栈结构,就像饭盒有多层一样。
我们所考虑的这个解决方案:销毁父作用域后,把用到的变量包起来,打包给子函数,放到一个属性上。这就是闭包的机制。
我们来试验一下闭包的特性:
这个 func3 需不需要打包一些东西?会不会有闭包?
其实还是有闭包的,闭包最少会包含全局作用域。
但是为啥 guang、ssh、suzhe 都没有 ?suzhe 是因为不是外部的,只有外部变量的时候才会生成,比如我们改动下代码,打印下这 3 个变量。
再次查看 [[Scopes]] (打包带走的闭包环境):
这时候就有俩闭包了,为什么呢?suzhe 哪去了?
首先,我们需要打包的只是环境内没有的,也就是闭包只保存外部引用。然后是在创建函数的时候保存到函数属性上的,创建的函数返回的时候会打包给函数,但是 JS 引擎怎么知道它要用到哪些外部引用呢,需要做 AST 扫描,很多 JS 引擎会做 Lazy Parsing,这时候去 parse 函数,正好也能知道它用到了哪些外部引用,然后把这些外部用打包成 Closure 闭包,加到 [[scopes]] 中。
所以,闭包是返回函数的时候扫描函数内的标识符引用,把用到的本作用域的变量打成 Closure 包,放到 [[Scopes]] 里。
所以上面的函数会在 func3 返回的时候扫描函数内的标识符,把 guang、ssh 扫描出来了,就顺着作用域链条查找这俩变量,过滤出来打包成两个 Closure(因为属于两个作用域,所以生成两个 Closure),再加上最外层 Global,设置给函数 func3 的 [[scopes]] 属性,让它打包带走。
调用 func3 的时候,JS 引擎 会取出 [[Scopes]] 中的打包的 Closure + Global 链,设置成新的作用域链, 这就是函数用到的所有外部环境了,有了外部环境,自然就可以运行了。
这里思考一个问题:调试代码的时候为什么某个变量明明在作用域内能访问到,但就是没有相关信息呢?
这个 traverse,明明能访问到的,为啥就是不显示信息呢?是 debugger 做的太烂了么?
不是的,如果你不知道原因,那是因为你还不理解闭包。因为这个 FunctionDeclaration 的函数是一个回调函数,明显是在另一个函数内调用的,就需要在创建的时候打包带走这个环境内的东西,根据只打包必要的环境的原则(不浪费内存),traverse 没有被引用(refer),自然就不打包了。并不是 debugger 有 bug 了。
所以我们只要访问一下,就能在调试的时候访问到了。
是不是突然知道为啥调试的时候不能看一些变量的信息了,能解释清楚这个现象,就算理解闭包了。
再来思考一个问题:闭包需要扫描函数内的标识符,做静态分析,那 eval 怎么办,他有可能内容是从网络记载的,从磁盘读取的等等,内容是动态的。用静态去分析动态是不可能没 bug 的。怎么办?
没错,eval 确实没法分析外部引用,也就没法打包闭包,这种就特殊处理一下,打包整个作用域就好了。
验证一下:
这个就像上面所说的,会把外部引用的打包成闭包
这个就是 eval 的实现,因为没法静态分析动态内容所以全部打包成闭包了,本来闭包就是为了不保存全部的作用域链的内容,结果 eval 导致全部保存了,所以尽量不要用 eval,会导致闭包保存内容过多。
但是 JS 引擎只处理了直接调用,也就是说直接调用 eval 才会打包整个作用域,如果不直接调用 eval,就没法分析引用,也就没法形成闭包了。
这种特殊情况有的时候还能用来完成一些黑魔法,比如利用不直接调用 eval 不会生成闭包,会在全局上下文执行的特性。
给闭包下个定义
用我们刚刚的试验来给闭包下个定义:
闭包是在函数创建的时候,让函数打包带走的根据函数内的外部引用来过滤作用域链剩下的链。它是在函数创建的时候生成的作用域链的子集,是打包的外部环境。evel 因为没法分析内容,所以直接调用会把整个作用域打包(所以尽量不要用 eval,容易在闭包保存过多的无用变量),而不直接调用则没有闭包。
过滤规则:
1. 全局作用域不会被过滤掉,一定包含。所以在何处调用函数都能访问到。
2. 其余作用域会根据是否内部有变量被当前函数所引用而过滤掉一些。不是每个返回的子函数都会生成闭包。
3. 被引用的作用域也会过滤掉没有被引用的 binding (变量声明)。只把用到的变量打个包。
闭包的缺点
JavaScript 是静态作用域的设计,闭包是为了解决子函数晚于父函数销毁的问题,我们会在父函数销毁时,把子函数引用到的变量达成 Closure 包放到函数的 [[Scopes]] 上,让它计算父函数销毁了也随时随地能访问外部环境。
这样设计确实解决了问题,但是有没有什么缺点呢?
其实问题就在于这个 [[Scopes]] 属性上
我们知道 JavaScript 引擎会把内存分为函数调用栈、全局作用域和堆,其中堆用于放一些动态的对象,调用栈每一个栈帧放一个函数的执行上下文,里面有一个 local 变量环境用于放内部声明的一些变量,如果是对象,会在堆上分配空间,然后把引用保存在栈帧的 local 环境中。全局作用域也是一样,只不过一般用于放静态的一些东西,有时候也叫静态域。
每个栈帧的执行上下文包含函数执行需要访问的所有环境,包括 local 环境、作用域链、this等。
那么如果子函数返回了会发生什么呢?
首先父函数的栈帧会销毁,子函数这个时候其实还没有被调用,所以还是一个堆中的对象,没有对应的栈帧,这时候父函数把作用域链过滤出需要用到的,形成闭包链,设置到子函数的 [[Scopes]] 属性上。
父函数销毁,栈帧对应的内存马上释放,用到的 ssh Obj 会被 gc 回收,而返回的函数会把作用域链过滤出用到的引用形成闭包链放在堆中。这就导致了一个隐患:本来作用域是随着函数调用的结束而销毁的,因为整个栈帧都会被马上销毁。而形成闭包以后,转移到了堆内存。当运行这个子函数的时候,子函数会创建栈帧,如果这个函数一直在运行,那么它在堆内存中的闭包就一直占用着内存,就会使可用内存减少,严重到一定程度就算是内存泄漏了。所以闭包不要乱用,少打包一点东西到堆内存。
我们从静态作用域开始聊起,明确了什么是作用域,通过 babel 静态分析了一下作用域,了解了下静态和动态作用域,然后引入了子函数先于父函数销毁的问题,思考了下方案,然后引入了闭包的概念,分析下闭包生成的流程,保存的位置。我们还用闭包的特性分析了下为什么有时候调试的时候查看不了变量信息,之后分析了下 eval 为什么没法精确生成闭包,什么时候全部打包作用域、什么时候不生成闭包, eval 为什么会导致内存占用过多。之后分析了下带有闭包的函数在内存中的特点,解释了下为啥可能会内存泄漏。
闭包是在返回一个函数的时候,为了把环境保存下载,创建的一个快照,对作用域链做了tree shking,只留下必要的闭包链,保存在堆里,作为对象的 [[scopes]] 属性,让函数不管走到哪,随时随地可访问用到的外部环境。
因为还没执行函数,所以要静态分析标识符引用。静态分析动态这件事情被无数个框架证明做不了,所以返回的函数有eval 只能全部打包或者不生成闭包。类似webpack 的动态import没法分析一样。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/0uT1wm2icR0vMW0lstj9lQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。