00、头痛的JS闭包、词法作用域?

被JavaScript的闭包、上下文、嵌套函数、this搞得很头痛,这语言设计的,感觉比较混乱,先勉强理解总结一下。

  • 为什么有闭包这么个东西?闭包包的是什么?
  • 什么是词法作用域?
  • 函数是如执行的呢?


01、执行上下文 (execution context)

名称 描述
是什么? 执行上下文 (execution context) 是JavaScript代码被解析和执行时所在环境的抽象概念。每一个函数执行的时候都会创建自己的执行上下文,保存了函数运行时需要的信息,并通过自己的上下文来运行函数。
干什么用的? 当然就是运行函数自身的,实现自我价值。
有那些种类? ① 全局上下文:全局环境最基础的执行上下文,所有不在任何函数内的代码都在这里面。
浏览器中的全局对象就是window,全局作用域下var申明、隐式申明的变量都会成为全局属性变量,全局的this指向window
其中会初始化一些全局对象或全局函数,如代码中的consoleundefinedisNaN
② 函数上下文:每个函数都有自己的上下文,调用函数的时候创建。可以把全局上下文看成是一个顶级根函数上下文。
eval() 调用内部上下文eval的代码会被编译创建自己的执行上下文,不常用也不建议使用。基于这个特点,有些框架会用eval()来实现沙箱Sandbox。
保存了什么信息? 初始化上下文的变量、函数等信息
thisValuethis环境对象引用。
内部(Local)环境:函数本地的所有变量、函数、参数(arguments)。
作用域链:具有访问作用域的其他上下文信息。
谁来用? 执行上下文函数调用栈来统一保存和调度管理。
生命周期 创建(入栈)=> 执行=> 销毁(出栈),函数调用的时候创建,执行完成后销毁。


02、函数调用栈是干啥的?

函数调用栈(Function Call Stack),管理函数调用的一种栈式结构(后进先出 )队列,或称执行栈,存储了当前程序所有执行上下文(正在执行的函数)。最早入栈的理所当然就是程序初始化时创建的全局上下文了,他是VIP会员,会一直在栈底,直到程序退出。

2.1、函数执行流程

函数执行上下文调用流程(也是函数的生命周期):

  • 创建-入栈:创建执行上下文,并压入栈,获得控制权。
  • 执行-干活:执行函数的代码,给变量赋值、查找变量。如有内部函数调用,递归重复函数流程。
  • 出栈-销毁:函数执行完成出栈,释放该执行上下文,其变量也就释放了,全都销毁,控制权回到上一层执行上下文。

function first() {
second(); //调用second()
}
function second() {
}
first();

上面的代码执行过程如下图所示

  1. 程序初始化运行时,首先创建的是全局上下文Global,进入执行栈。
  2. 调用first()函数,创建其下文并入栈。
  3. first()函数内部调用了second()函数,创建second()下文入栈并执行。
  4. second()函数执行完成并出栈,控制权回到first()函数上下文。
  5. first()函数执行完成并出栈,控制权回到全局上下文。

再来一个函数调用栈的示例:

var a = 1;
let b = 1;
function FA(x) {
function FB(y) {
function FC(z) {
console.log(a + b + x + y + z);
}
FC(3);
}
FB(2);
}
FA(1); //8

上面函数在执行FC()时的函数调用堆栈如下图(Edge浏览器断点调试):

执行FC函数代码时,其作用域保留了所有要用到的作用域变量,从自己往上,直到全局对象,闭包就是这么来的!

  • var a = 1;:var申明的变量会作为全局对象window的变量。
  • let b = 1;:全局环境申明的变量,任何函数都可以访问,放在全局脚本环境中,可以看做全局的一部分。

调用堆栈中有FC、FB、FA,因为是嵌套函数,FB、FA并未结束,所以还在堆栈中,函数执行完毕就会被立即释放抛弃。

2.2、堆栈溢出

函数调用栈容量是有限的!—— 递归函数

递归函数就是一个多层+自我嵌套调用的过程,所以执行递归函数时,会不停的入栈,而没有出栈,循环次数太多会超出堆栈容量限制,从而引发报错。比如下面示例中一个简单的加法递归,在Firefox浏览器中递归1500次,就报错了(InternalError: too much recursion),Edge浏览器是11000次超出调用栈容量(Maximum call stack size exceeded)。

怎么解决呢?

  • 避免递归:封装处理逻辑,转换成循环的方式来处理。或用setTimeout(func,0)发送到任务队列单独执行。
  • 拆分执行:合理拆分代码为多个递归函数。
function add(x) {
if (x <= 0)
return 0;
return x + add(x - 1); //递归求和
}
add(1000); //Firefox:1000可以,1500就报错 InternalError: too much recursion
add(10000);//Edge:10000可以执行,11000就报错 Maximum call stack size exceeded

» Firefox 的调用堆栈:


03、什么是词法作用域?

作用域(scope)就是一套规定变量作用范围(权限),并按此去查找变量的规则。包括静态作用域动态作用域,JavaScript中主要是静态作用域(词法作用域)

  • 静态作用域(就是词法作用域):JavaScript是基于词法作用域来创建作用域的,基于代码的词法分析确定变量的作用域、作用域关系(作用域链)。词法环境就是我们写代码的顺序,所以是静态的,就是说函数、变量的作用域是在其申明的时候就已经确定了,在运行阶段不再改变。
  • 动态作用域:基于动态调用的关系确定的,其作用域链是基于运行时的调用栈的。比如this,一般就是基于调用来确定上下文环境的。因此this值可以在调用栈上来找,注意的是this指向一个引用对象,不是函数本身,也不是其词法作用域。

因此,词法作用域主要作用是规定了变量的访问权限,确定了如何去查找变量,基本规则

  • 代码位置决定:变量(包括函数)申明的的地方决定了作用域,跟在哪调用无关。
  • 拥有父级权限:函数(或块)可以访问其外部的数据,如果嵌套多层,则递归拥有父级的作用域权限,直到全局环境。
  • 函数作用域:只有函数可以限定作用域,不能被上级、外部其他函数访问。
  • 同名就近使用:如果有和上级同名的变量,则就近使用,先找到谁就用谁。
  • 逐层向上查找:变量的查找规则就是先内部,然逐级往上,直到全局环境,如果都没找到,则变量undefined

这里的词法作用域,就是前文所说JS变量作用域。而闭包保留了上下文作用域的变量,就是为了实现词法作用域。

那词法作用域是怎么实现的呢?——作用域链、闭包

父级函数FA()执行完成后就出栈销毁了(典型场景就是返回函数)FB()可以到任何地方执行,那内部函数FB()执行的时候到哪里去找父级函数的变量x呢?

  • 函数内部作用域:首先每个函数执行都会创建自己作用域(执行上下文),查找变量时优先本地作用域查找。
  • 闭包:引用的外部(词法上级)函数作用域就形成了一个闭包,用一个Closure_(Closure /ˈkləʊʒə(r)/ 闭包)_对象保存,多个(外部引用)逐级保存到函数上下文的[[Scope]](Scope /skoʊp/ 作用域)集合上,形成作用域链
  • 作用域链的最底层就是指向全局对象的引用,她始终都在,不管你要不要她。
  • 变量查找就在这个作用域链上进行:自己上下文(词法环境,变量环境) => 作用域链逐级查找=> 全局作用域 => undefined

function FA(x) {
function FB(y) {
x+=y;
console.log(x);
}
console.dir(FB);
return FB; //返回FB()函数
}
let fb = FA(1); //FA函数执行完成,出栈销毁了
fb(2); //3 //返回的fb()函数保留了他的父级FA()作用域变量x
fb(2); //5 //闭包中的x:我又变大了
fb(2); //7 //同一个闭包函数重复调用,内部变量被改变

闭包简单理解就是,当前环境中存放在指向父级作用域的引用。如果嵌套子函数完全没有引用父级任何变量,就不会产生闭包。不过全局对象是始终存在其作用域链[[Scope]]上的。

举个例子

var a = 1;
let b = 2;
function FunA(x) {
let x1 = 1;
var x2 = 2;
function FunB(y) {
console.log(a + b + x + x1 + x2 + y);
}
FunB(2);
console.dir(FunB)
}
FunA(1); //9
console.dir(FunA)

上面的代码示例中,FunA()函数嵌套了FunB()函数,如下图FunB()函数的[[Scope]]集合上有三个对象:

  • Closure (FunA) FunA()函数的闭包,包含他的参数x、私有变量x1x2
  • Script:Script Scope 脚本作用域(可以当做全局作用域的一部分),存放全局Script脚本环境内可访问的letconst变量,就是全局作用域内的变量。var变量a被提升为了全局对象window的“属性”了。
  • Global:全局作用域对象,就是window,包含了var申明的变量,以及未申明的变量。

如果把FunB()函数放到外面申明,只在FunA()调用,其作用域链就不一样了。


04、执行上下文是怎么创建的?

执行上下文的创建过程中会创建对应的词法作用域,包括词法环境变量环境

  • 创建词法环境(LexicalEnvironment):

    • 环境记录EnvironmentRecord:记录变量、函数的申明等信息,只存储函数声明和let/const声明的变量。
    • 外层引用outer:对(上级)其他作用域词法环境的引用,至少会包含全局上下文。
  • 创建变量环境(VariableEnvironment):本质上也是词法环境,只不过他只存储var申明的变量,其他都和词法环境差不多。
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}

变量查找:变量查找的时候,是先从词法环境中找,然后再到变量环境。就是优先查找const、let变量,其次才var变量。

换几个角度来总结下,创建执行上下文主要搞定下面三个方面:

① 确定 this 的值(This Binding)

  • 在全局上下文中this指向window
  • 函数执行上下文中,如果它被一个对象引用调用,那么 this 的值被设置为该对象,否则 this 的值被设置为全局对象或 undefined(严格模式下)
  • call(thisArg)、apply(thisArg)、bind(thisArg)会直接指定thisValue值。

② 内部环境:包括词法环境变量环境,就是函数内部的变量、函数等信息,还有参数arguments信息。

③ 作用域链(外部引用):外部的词法作用域存放到函数的[[Scope]]集合里,用来查找上级作用域变量。


05、有什么结论?

  • 变量越近越好:最好都本地化,尽量避免让变量查找链路过长,一层层切换作用域去找也是很累的。
  • 优先const,其次let,尽量(坚决)不用var
  • 注意函数调用堆栈的长度,比如递归。
  • 闭包函数使用完后,手动释放一下,fun = null;,尽早被垃圾回收。
  • 尽量避免成为全局环境的变量,特别是一些临时变量,全局对象始终都在,不会被垃圾回收。
    • 包括全局环境申明的的letconstvar
    • 切记不用未申明变量str='',不管在哪里都会成为全局变量。

远离JavaScript、远离前端......我以为已经学会了,其实可能还没入门。


10、GC内存管理

值类型变量的生命周期随函数,函数执行完就释放了。垃圾回收GC(Garbage Collection)内存管理主要针对引用对象,当检测到对象不再会被使用,就释放其内存。GC是自动运行的,不需干预也无法干预

GC回收一个对象的关键就是——确定他确是一个废物,么有任何地方使用他了,主要采用的方法就是标记清理。

  • 标记清理(mark-and-sweep):标记内存中的所有的可达对象和他所有引用的对象),剩下的就是没人要的,可以删除了。
  • 引用计数:按变量被引用的次数,这个策略已不再使用了,由于该回收垃圾的策略太垃圾从而被抛弃了。

什么是可达性?

  • 根(roots):当前执行环境(window)最直接的变量,包括当前执行函数的局部变量、参数;当前函数调用链上的其他函数的变量、参数;全局变量。
  • 可达性(Reachability):如果一个值(对象)可以从根开始链式访问到他,就是可达的,就说明这个数据对象还有利用价值。

上图中FuncA函数中的局部变量 obj1,其值对象{P}存放在内存堆中,此时的值对象{P}被根变量obj1引用了,是可达的。

  • 如果函数执行完毕,函数就销毁了,变量引用obj1也一起随她而去。值对象{P}就没有被引用了,就不可达了。
  • 如果在函数中显示执行 obj1=null; 同样的值对象{P}没有被引用了,就不可达了。

GC定期执行垃圾回收的两个步骤:

① 标记阶段:找到可达对象并标记,实际的算法会更加精细。

  • 垃圾收集器找到所有的根,并“标记”(记住)它们。
  • 继续遍历并“标记”被根引用的对象。
  • ...继续遍历,直到找到所有可达对象并标记。

② 清除阶段:没有被标记的对象都会被清理删除。

全局变量不会被清理:属于window的全局变量就是根,始终不会被清理,有背景靠山就是不一样!


️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀

JavaScript入门③-函数(2)原理{深入}执行上下文的更多相关文章

  1. JavaScript入门-函数function(二)

    JavaScript入门-函数function(二) 递归函数 什么是递归函数? 递归简单理解就是,在函数体里,调用自己. //我们在求一个10的阶乘的时候,可能会这么做 //写一个循环 var to ...

  2. 深入理解JavaScript系列(11):执行上下文(Execution Contexts)

    简介 从本章开始,我将陆续(翻译.转载.整理)http://dmitrysoshnikov.com/网站关于ECMAScript标标准理解的好文. 本章我们要讲解的是ECMAScript标准里的执行上 ...

  3. 第112天:javascript中函数预解析和执行阶段

    关于javascript中的函数:  1.预解析:把所有的函数定义提前,所有的变量声明提前,变量的赋值不提前  2.执行 :从上到下执行,但有例外(setTimeout,setInterval,aja ...

  4. 05.Javascript——入门函数

    //定义函数的方法1 function abs(x) { if (x >= 0) { return x; } else { return -x; } } 上述abs()函数的定义如下: func ...

  5. JavaScript闭包函数&箭头函数调用与执行

    一.标准的闭包函数 //一.标准的闭包函数 function A() { var i=0; ++i; console.log('i : ' + i); return function b() { re ...

  6. 《浏览器工作原理与实践》<11>this:从JavaScript执行上下文的视角讲清楚this

    在上篇文章中,我们讲了词法作用域.作用域链以及闭包,接下来我们分析一下这段代码: var bar = { myName:"time.geekbang.com", printName ...

  7. JavaScript执行上下文

    变量声明.函数声明为何会提升?js执行时是如何查找变量的?JavaScript中最基本的部分——执行上下文(execution context) 什么是执行上下文? 当JavaScript代码运行,执 ...

  8. 了解JavaScript的执行上下文

    转自http://www.cnblogs.com/yanhaijing/p/3685310.html 什么是执行上下文? 当JavaScript代码运行,执行环境非常重要,有下面几种不同的情况: 全局 ...

  9. 【进阶1-2期】JavaScript深入之执行上下文栈和变量对象(转)

    这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://mp.weixin.qq.com/s/hZIpnkKqdQgQnK1BcrH6Nw 阅读笔记 JS是单线程的语言,执行顺序肯定是顺 ...

  10. JavaScript学习系列之执行上下文与变量对象篇

    一个热爱技术的菜鸟...用点滴的积累铸就明日的达人 正文 在上一篇文章中讲解了JavaScript内存模型,其中有提到执行上下文与变量对象的概念.对于JavaScript开发者来说,理解执行上下文与变 ...

随机推荐

  1. MySQL8.0报错:Access denied; you need (at least one of) the SYSTEM_USER privilege(s) for this operation

    MySQL8.0.16版本中新增了一个system_user帐户类型,当新增用户并赋予权限时 mysql> create user 'proxysql'@'192.168.20.%' ident ...

  2. Elasticsearch:Cluster备份 Snapshot及Restore API

    Elasticsearch提供了replica解决方案,它可以帮我们解决了如果有一个或多个node失败了,那么我们的数据还是可以保证完整的情况,并且搜索还可以继续进行.但是,有一种情况是我们的所有的n ...

  3. 使用docker-compose部署SonarQube

    sonarqube 安装 1.系统配置,避免启动问题 # 系统配置,避免启动问题 echo "vm.max_map_count=262144" >> /etc/sysc ...

  4. 使用 Windows 包管理器 (winget) 安装 .Net

    用户可以在 Windows 10 和 Windows 11 计算机上使用 winget 命令行工具来发现.安装.升级.删除和配置应用程序. 此工具是 Windows 程序包管理器服务的客户端接口. 以 ...

  5. PAT (Basic Level) Practice 1010 一元多项式求导 分数 25

    设计函数求一元多项式的导数.(注:xn(n为整数)的一阶导数为nxn−1.) 输入格式: 以指数递降方式输入多项式非零项系数和指数(绝对值均为不超过 1000 的整数).数字间以空格分隔. 输出格式: ...

  6. Java并发编程 | 从进程、线程到并发问题实例解决

    计划写几篇文章讲述下Java并发编程,帮助一些初学者成体系的理解并发编程并实际使用,而不只是碎片化的了解一些Synchronized.ReentrantLock等技术点.在讲述的过程中,也想融入一些相 ...

  7. POJ1094 Sorting It All Out (floyd传递闭包)

    关系具有传递性,可以用floyd解决. 将关系都看做i<j的形式,令d[i][j]=1,如果d[i][j]=d[j][i]=1,说明矛盾:d[i][j]=d[j][i]=0,说明i与j的关系无法 ...

  8. 撸了一个简易的配置中心,顺带整合到了SpringCloud

    大家好,我是三友~~ 最近突然心血来潮(就是闲的)就想着撸一个简单的配置中心,顺便也照葫芦画瓢给整合到SpringCloud. 本文大纲 配置中心的概述 随着历史的车轮不断的前进,技术不断的进步,单体 ...

  9. IDEA清空控制台以及Java中运行cmd命令实现清屏操作

    IDEA中清空控制台方法 在网上有看到各种的实现方法,比如: Runtime.getRuntime().exec("cls"); 或者: public static void cl ...

  10. HTML躬行记(2)——WebRTC基础实践

    WebRTC (Web Real-Time Communications) 是一项实时通讯技术,在 2011 年由 Google 提出,经过 10 年的发展,W3C 于 2021 年正式发布 WebR ...