4

JavaScript入门③-函数(2)原理{深入}执行上下文 - 安木夕

 1 year ago
source link: https://www.cnblogs.com/anding/p/16889786.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
image.png

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

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

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

01、执行上下文 (execution context)

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

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

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

2.1、函数执行流程

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

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

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

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

c71b3089775edcdcd2043eba90a70572_u=544850019,275206126&fm=253&app=138&f=PNG&fmt=auto&q=75_w=1280&h=228.webp

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

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浏览器断点调试):

image.png

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

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

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

image.png

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 recursionadd(10000);//Edge:10000可以执行,11000就报错 Maximum call stack size exceeded

» Firefox 的调用堆栈:

image.png

03、什么是词法作用域?

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

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

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

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

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

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

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

  • ✅ 函数内部作用域:首先每个函数执行都会创建自己作用域(执行上下文),查找变量时优先本地作用域查找。
  • ✅ 闭包:引用的外部(词法上级)函数作用域就形成了一个闭包,用一个Closure_(Closure /ˈkləʊʒə(r)/ 闭包)_对象保存,多个(外部引用)逐级保存到函数上下文的[[Scope]](Scope /skoʊp/ 作用域)集合上,形成作用域链
  • ✅ 作用域链的最底层就是指向全局对象的引用,她始终都在,不管你要不要她。
  • ✅ 变量查找就在这个作用域链上进行:自己上下文(词法环境,变量环境) => 作用域链逐级查找=> 全局作用域 => undefined
image
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()作用域变量xfb(2); //5 //闭包中的x:我又变大了fb(2); //7 //同一个闭包函数重复调用,内部变量被改变
image.png

📢闭包简单理解就是,当前环境中存放在指向父级作用域的引用。如果嵌套子函数完全没有引用父级任何变量,就不会产生闭包。不过全局对象是始终存在其作用域链[[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); //9console.dir(FunA)

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

image.png
  • 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变量。

image

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

① 确定 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、远离前端......我以为已经学会了,其实可能还没入门。

image.png


10、GC内存管理

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

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

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

❓什么是可达性?

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

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

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

image.png

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

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

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

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

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


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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK