JavaScript入门③-函数(2)原理{深入}执行上下文
JavaScript入门系列目录
- JavaScript入门①-基础知识筑基
- JavaScript入门②-函数(1)基础{浅出}
- JavaScript入门③-函数(2)原理{深入}执行上下文
- JavaScript入门④-万物皆对象:Object
- JavaScript入门⑤-欲罢不能的对象、原型与继承
- JavaScript入门⑥-WEB浏览器API
- JavaScript入门⑦-DOM操作大全
- JavaScript入门⑧-事件总结大全
- JavaScript入门⑨-异步编程●异世界之旅
- JavaScript入门⑩-ES6归纳总结
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的全局变量就是根,始终不会被清理,有背景靠山就是不一样!
©️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀