gpt4 book ai didi

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

转载 作者:我是一只小鸟 更新时间:2022-12-02 14:31:19 28 4
gpt4 key购买 nike

image.png

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

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

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

image


01、执行上下文 (execution context)

名称 描述
是什么? 执行上下文 (execution context) 是 JavaScript 代码被解析和执行时所在环境的抽象概念。每一个函数 执行 的时候都会创建自己的执行上下文,保存了函数运行时需要的信息,并通过自己的上下文来运行函数。
干什么用的? 当然就是运行函数自身的,实现自我价值。
有那些种类? ① 全局上下文 :全局环境最基础的执行上下文,所有不在任何函数内的代码都在这里面。
🔸 浏览器中的全局对象就是 window ,全局作用域下 var 申明、隐式申明的变量都会成为全局属性变量,全局的 this 指向 window
🔸 其中会初始化一些全局对象或全局函数,如代码中的 console undefined isNaN
② 函数上下文 :每个函数都有自己的上下文,调用函数的时候创建。可以把全局上下文看成是一个顶级根函数上下文。
eval() 调用内部上下文 eval 的代码会被编译创建自己的执行上下文,不常用也不建议使用。基于这个特点,有些框架会用 eval() 来实现沙箱Sandbox。
保存了什么信息? 初始化上下文的变量、函数等信息
🔸 thisValue this 环境对象引用。
🔸 内部(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 recursion
add(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()作用域变量x
fb(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); //9
console.dir(FunA)

                        
                      

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

image.png

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

image

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

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

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的全局变量就是根,始终不会被清理,有背景靠山就是不一样! 。


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

最后此篇关于JavaScript入门③-函数(2)原理{深入}执行上下文的文章就讲到这里了,如果你想了解更多关于JavaScript入门③-函数(2)原理{深入}执行上下文的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com