还没搞懂作用域、执行上下文、变量提升?看这篇就够啦

还没搞懂作用域、执行上下文、变量提升?看这篇就够啦

码农世界 2024-06-04 前端 104 次浏览 0个评论

前言

 📫 大家好,我是南木元元,热爱技术和分享,欢迎大家交流,一起学习进步!

 🍅 个人主页:南木元元


目录

作用域(Scope)

全局作用域

函数作用域

块级作用域

作用域链(Scope Chain)

执行上下文

执行上下文的类型

执行上下文栈

创建执行上下文

(1)创建阶段

(2)执行阶段

作用域与执行上下文区别

解释阶段

执行阶段

变量提升

本质原因

小练习

结语


作用域(Scope)

在JavaScript中,作用域(Scope)是指代码中变量、函数和对象的可访问性范围,决定了变量或函数在何处可以被访问或引用。

JavaScript中有3种类型的作用域,包括:

  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6新增)

    全局作用域

    在全局作用域中定义的变量在代码中任何地方都能访问到,常见的就是在最外层函数外面定义一个变量,该变量拥有全局作用域。

    var globalVar = "I am global";
    function test() {
      console.log(globalVar); // "I am global"
    }
    test();
    console.log(globalVar); // "I am global"
    

    除了上述情形外,还有几种情况拥有全局作用域:

    • 所有未定义直接赋值的变量自动声明为全局作用域
    • 所有window对象的属性拥有全局作用域,如窗口名字window.name、页面URL地址window.location等

      全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

      函数作用域

      函数作用域是指变量和函数在函数内部定义时,只在函数内部可见和可访问,函数外部无法访问函数内部的变量和函数(每当调用一个函数时,都会创建一个新的函数作用域)。

      function localFunction() {
        var localVar = "I am local";
        console.log(localVar); // "I am local"
      }
      localFunction();
      console.log(localVar); // ReferenceError: localVar is not defined
      

      块级作用域

      使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中创建(由一对花括号{}包裹的代码片段),使用var声明的变量不会有块级作用域。

      if (true) {
        let blockVar = "I am in block";
        console.log(blockVar); // "I am in block"
      }
      console.log(blockVar); // ReferenceError: blockVar is not defined
      

      作用域链(Scope Chain)

      某个变量在当前作用域中找不到时,就会往它的上⼀级寻找,如果找到全局作用域还没找

      到,就放弃寻找(返回 undefined),这种层级关系就是作用域链。

      作用域链的作用就是保证对变量和函数的有序访问。

      var globalVar = "I am global";
      function outerFunction() {
        var outerVar = "I am outer";
        
        function innerFunction() {
          var innerVar = "I am inner";
          console.log(globalVar); // "I am global"
          console.log(outerVar); // "I am outer"
          console.log(innerVar); // "I am inner"
        }
        innerFunction();
      }
      outerFunction();
      

      在上述代码中,innerFunction可以访问globalVar和outerVar,因为它们在外部作用域中。

      需要注意的是,js采用的是静态作用域(词法作用域),函数的作用域在函数定义时就确定了(这里不再详细展开,推荐阅读JavaScript深入之词法作用域和动态作用域)

      执行上下文

      当JavaScript代码执行一段可执行代码时,会创建对应的执行上下文,执行上下文就是代码在执行过程中的环境信息。

      执行上下文的类型

      1.全局执行上下文:这是默认的执行上下文,当JavaScript代码开始运行时首先创建。全局执行上下文中的代码在全局作用域中执行,并且只有一个全局执行上下文。

      2.函数执行上下文:每当一个函数被调用时,会创建一个新的执行上下文。每个函数都有自己的执行上下文,函数执行上下文在函数调用时被创建,并在函数执行完毕后被销毁。

      3.eval函数执行上下文:eval函数内部代码会创建一个独立的执行上下文,但不推荐使用eval。

      执行上下文栈

      JavaScript引擎使用执行上下文栈来管理执行上下文。

      当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。

      示例:

      function f(a) {
        console.log(a);
      }
      function func(a) {
        f(a);
      }
      func(1);
      

      假设用ESP指针来保存当前的执行状态,在系统栈中会产生如下的过程:

      1. 调用func, 将 func 函数的上下文压栈,ESP指向栈顶。

      2. 执行func,又调用f函数,将 f 函数的上下文压栈,ESP 指针上移。

      3. 执行完 f 函数,将ESP 下移,f函数对应的栈顶空间被回收。

      4. 执行完 func,ESP 下移,func对应的空间被回收。

       图示如下:

      创建执行上下文

      创建执行上下文有两个阶段:创建阶段和执行阶段。

      (1)创建阶段

      在 JavaScript 代码执行前,执行上下文将经历创建阶段。主要包括:

      • 创建变量对象(Variable Object, VO)

        在ES5中称为变量对象,在ES6及以后称为词法环境组件,这个对象包含了执行环境中所有变量和函数。

        对全局执行上下文,变量对象是全局对象;对函数执行上下文,变量对象包含函数的参数、局部变量和函数声明。

        提升函数和变量声明:将函数声明提升到当前作用域的顶部,并存储在变量对象中;将变量声明提升到当前作用域的顶部,但不会初始化(初始化在执行阶段进行)。

        • 建立作用域链

          创建作用域链,包含当前执行上下文的变量对象和其父级执行上下文的变量对象,直到全局对象。

          作用域链本质上是一个指向变量对象的指针列表,作用域链的前端始终都是当前执行上下文的变量对象,全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

          • 确定this绑定

            在全局执行上下文中,this指向全局对象;在函数执行上下文中,this取决于函数的调用方式。

            (2)执行阶段

            完成对变量的分配,在变量声明阶段被提升的变量,现在会被赋值,最后执行代码。

            作用域与执行上下文

            JavaScript属于解释型语言,其执行分为解释和执行两个阶段。

            解释阶段

            • 词法分析:即分词,它的工作就是将一行行的代码分解成一个个token
            • 语法分析:将上一步生成的token数据,根据一定的语法规则转化为AST
            • 作用域规则确定

              执行阶段

              • 创建执行上下文
              • 执行函数代码
              • 垃圾回收

                JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。

                作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。

                变量提升

                变量提升是指使用var声明的变量以及函数声明会被提升到作用域顶部, 变量提升的结果,就是可以在变量初始化之前访问该变量(返回undefined),在函数声明前可以调用该函数。

                console.log(a); // undefined
                var a = 5;
                console.log(a); // 5
                

                在上述代码中,尽管console.log(a)在var a = 5之前调用,但代码不会报错,因为在执行代码之前,变量已经被提升。

                同样,函数声明也会被提升:

                foo(); // "Hello, world!"
                function foo() {
                  console.log("Hello, world!");
                }
                

                上述代码中,即使foo在函数声明之前调用,代码依然可以正常执行。

                本质原因

                变量提升的本质原因:js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。

                简单来说就是在执行JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

                注意点:

                1.let和const声明的变量不会被提升。如果在声明前使用它们,会报错。

                2.函数声明会被提升,函数表达式不会被提升。

                小练习

                说出下面代码的执行结果。

                var foo = function () {
                    console.log("foo1")
                }
                foo()
                var foo = function () {
                    console.log("foo2")
                }
                foo()
                function foo() {
                    console.log("foo1")
                }
                foo()
                function foo() {
                    console.log("foo2")
                }
                foo()
                

                这是一道关于变量提升的题,具体就不展开了,可以看这道面试题真的很变态吗?

                结语

                🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论,支持一下博主~ 

                 

转载请注明来自码农世界,本文标题:《还没搞懂作用域、执行上下文、变量提升?看这篇就够啦》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,104人围观)参与讨论

还没有评论,来说两句吧...

Top