JavaScript入门③-函数(2)原理{深入}执行上下文
00、头痛的JS闭包、词法作用域?
被JavaScript的闭包、上下文、嵌套函数、this搞得很头痛,这语言设计的,感觉比较混乱,先勉强理解总结一下。
- 为什么有闭包这么个东西?闭包包的是什么?
- 什么是词法作用域?
- 函数是如执行的呢?
01、执行上下文 (execution context)
名称 | 描述 |
---|---|
是什么? | 执行上下文 (execution context) 是JavaScript 代码被解析和执行时所在环境的抽象概念。每一个函数执行的时候都会创建自己的执行上下文,保存了函数运行时需要的信息,并通过自己的上下文来运行函数。 |
干什么用的? | 当然就是运行函数自身的,实现自我价值。 |
有那些种类? | ① 全局上下文:全局环境最基础的执行上下文,所有不在任何函数内的代码都在这里面。 浏览器中的全局对象就是 window ,全局作用域下var 申明、隐式申明的变量都会成为全局属性变量,全局的this 指向window 。其中会初始化一些全局对象或全局函数,如代码中的 console 、undefined 、isNaN ② 函数上下文:每个函数都有自己的上下文,调用函数的时候创建。可以把全局上下文看成是一个顶级根函数上下文。 ③ eval() 调用内部上下文:eval 的代码会被编译创建自己的执行上下文,不常用也不建议使用。基于这个特点,有些框架会用eval() 来实现沙箱Sandbox。 |
保存了什么信息? | 初始化上下文的变量、函数等信息 thisValue: this 环境对象引用。内部(Local)环境:函数本地的所有变量、函数、参数(arguments)。 作用域链:具有访问作用域的其他上下文信息。 |
谁来用? | 执行上下文由函数调用栈来统一保存和调度管理。 |
生命周期 | 创建(入栈)=> 执行=> 销毁(出栈),函数调用的时候创建,执行完成后销毁。 |
02、函数调用栈是干啥的?
函数调用栈(Function Call Stack),管理函数调用的一种栈式结构(后进先出 )队列,或称执行栈,存储了当前程序所有执行上下文(正在执行的函数)。最早入栈的理所当然就是程序初始化时创建的全局上下文
了,他是VIP会员,会一直在栈底,直到程序退出。
2.1、函数执行流程
函数执行上下文调用流程(也是函数的生命周期):
- 创建-入栈:创建执行上下文,并压入栈,获得控制权。
- 执行-干活:执行函数的代码,给变量赋值、查找变量。如有内部函数调用,递归重复函数流程。
- 出栈-销毁:函数执行完成出栈,释放该执行上下文,其变量也就释放了,全都销毁,控制权回到上一层执行上下文。
function first() {
second(); //调用second()
}
function second() {
}
first();
上面的代码执行过程如下图所示:
- 程序初始化运行时,首先创建的是全局上下文
Global
,进入执行栈。 - 调用
first()
函数,创建其下文并入栈。 first()
函数内部调用了second()
函数,创建second()
下文入栈并执行。second()
函数执行完成并出栈,控制权回到first()
函数上下文。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
、私有变量x1
、x2
。Script
:Script Scope 脚本作用域(可以当做全局作用域的一部分),存放全局Script脚本环境内可访问的let
、const
变量,就是全局作用域内的变量。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;
,尽早被垃圾回收。 - 尽量避免成为全局环境的变量,特别是一些临时变量,全局对象始终都在,不会被垃圾回收。
- 包括全局环境申明的的
let
、const
、var
- 切记不用未申明变量
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)原理{深入}执行上下文的更多相关文章
- JavaScript入门-函数function(二)
JavaScript入门-函数function(二) 递归函数 什么是递归函数? 递归简单理解就是,在函数体里,调用自己. //我们在求一个10的阶乘的时候,可能会这么做 //写一个循环 var to ...
- 深入理解JavaScript系列(11):执行上下文(Execution Contexts)
简介 从本章开始,我将陆续(翻译.转载.整理)http://dmitrysoshnikov.com/网站关于ECMAScript标标准理解的好文. 本章我们要讲解的是ECMAScript标准里的执行上 ...
- 第112天:javascript中函数预解析和执行阶段
关于javascript中的函数: 1.预解析:把所有的函数定义提前,所有的变量声明提前,变量的赋值不提前 2.执行 :从上到下执行,但有例外(setTimeout,setInterval,aja ...
- 05.Javascript——入门函数
//定义函数的方法1 function abs(x) { if (x >= 0) { return x; } else { return -x; } } 上述abs()函数的定义如下: func ...
- JavaScript闭包函数&箭头函数调用与执行
一.标准的闭包函数 //一.标准的闭包函数 function A() { var i=0; ++i; console.log('i : ' + i); return function b() { re ...
- 《浏览器工作原理与实践》<11>this:从JavaScript执行上下文的视角讲清楚this
在上篇文章中,我们讲了词法作用域.作用域链以及闭包,接下来我们分析一下这段代码: var bar = { myName:"time.geekbang.com", printName ...
- JavaScript执行上下文
变量声明.函数声明为何会提升?js执行时是如何查找变量的?JavaScript中最基本的部分——执行上下文(execution context) 什么是执行上下文? 当JavaScript代码运行,执 ...
- 了解JavaScript的执行上下文
转自http://www.cnblogs.com/yanhaijing/p/3685310.html 什么是执行上下文? 当JavaScript代码运行,执行环境非常重要,有下面几种不同的情况: 全局 ...
- 【进阶1-2期】JavaScript深入之执行上下文栈和变量对象(转)
这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://mp.weixin.qq.com/s/hZIpnkKqdQgQnK1BcrH6Nw 阅读笔记 JS是单线程的语言,执行顺序肯定是顺 ...
- JavaScript学习系列之执行上下文与变量对象篇
一个热爱技术的菜鸟...用点滴的积累铸就明日的达人 正文 在上一篇文章中讲解了JavaScript内存模型,其中有提到执行上下文与变量对象的概念.对于JavaScript开发者来说,理解执行上下文与变 ...
随机推荐
- 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 ...
- Elasticsearch:Cluster备份 Snapshot及Restore API
Elasticsearch提供了replica解决方案,它可以帮我们解决了如果有一个或多个node失败了,那么我们的数据还是可以保证完整的情况,并且搜索还可以继续进行.但是,有一种情况是我们的所有的n ...
- 使用docker-compose部署SonarQube
sonarqube 安装 1.系统配置,避免启动问题 # 系统配置,避免启动问题 echo "vm.max_map_count=262144" >> /etc/sysc ...
- 使用 Windows 包管理器 (winget) 安装 .Net
用户可以在 Windows 10 和 Windows 11 计算机上使用 winget 命令行工具来发现.安装.升级.删除和配置应用程序. 此工具是 Windows 程序包管理器服务的客户端接口. 以 ...
- PAT (Basic Level) Practice 1010 一元多项式求导 分数 25
设计函数求一元多项式的导数.(注:xn(n为整数)的一阶导数为nxn−1.) 输入格式: 以指数递降方式输入多项式非零项系数和指数(绝对值均为不超过 1000 的整数).数字间以空格分隔. 输出格式: ...
- Java并发编程 | 从进程、线程到并发问题实例解决
计划写几篇文章讲述下Java并发编程,帮助一些初学者成体系的理解并发编程并实际使用,而不只是碎片化的了解一些Synchronized.ReentrantLock等技术点.在讲述的过程中,也想融入一些相 ...
- 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的关系无法 ...
- 撸了一个简易的配置中心,顺带整合到了SpringCloud
大家好,我是三友~~ 最近突然心血来潮(就是闲的)就想着撸一个简单的配置中心,顺便也照葫芦画瓢给整合到SpringCloud. 本文大纲 配置中心的概述 随着历史的车轮不断的前进,技术不断的进步,单体 ...
- IDEA清空控制台以及Java中运行cmd命令实现清屏操作
IDEA中清空控制台方法 在网上有看到各种的实现方法,比如: Runtime.getRuntime().exec("cls"); 或者: public static void cl ...
- HTML躬行记(2)——WebRTC基础实践
WebRTC (Web Real-Time Communications) 是一项实时通讯技术,在 2011 年由 Google 提出,经过 10 年的发展,W3C 于 2021 年正式发布 WebR ...