执行上下文变量提升

执行上下文

  • 执行上下文相当于执行环境,在《Javascipt高级程序设计》中关于执行环境是这么描述的:

    • 执行环境定义了变量有权访问的其他数据,并决定了他们各自的行为
    • 执行环境有一个与它关联的对象(变量对象)
    • 该对象中保存了这个执行环境中所有的变量和函数
  • 其实书中所写可以理解为:每一次代码执行都会产生一个执行环境,该环境称为执行上下文

  • 执行上下文的生命周期

    • 进入执行上下文和执行其中的代码

    • 进入执行上下文的时候,会用很短的时间对其中的代码进行预编译,其分为三项:

      • 生成变量对象(VO–Variable Object),又包括三项

        • 检索当前环境的参数:创建arguments对象,以参数名为属性名,以参数值为属性值添加到该对象中。如果未传递参数值,则以undefined为值。arguments包括三部分:

          • callee当前函数的引用

          • length参数的长度

          • properties-indexesarguments是一个类数组对象,properties可以理解为对象中的每一项,其key就是其位置index的字符串形式,其values就是参数值。可以使用arguments[index]来访问。不过因为是类数组对象,不可以使用数组的那些方法。

          • 上述三条,使用代码来演示:

            1
            2
            3
            4
            5
            6
            7
            8
            9
            function fn1(a,b,c){
            console.log(arguments.callee) //[Function:fn1]
            console.log(arguments.length) // 3
            console.log(arguments) //{ '0': 1, '1': 2, '2': 3 }
            console.log(arguments[1]) // 2
            }

            fn1(1,2,3)
            ```
        • 检索当前环境中的函数声明:必须是以function来声明的函数,不可以是函数表达式。会将其函数名为属性名,将其函数地址的引用作为属性值,添加到变量对象中

        • 检索当前环境中的变量声明:将其变量名为属性名,变量值为属性值添加到变量对象中。这里要注意,如果变量名(未赋值)与前变量名、参数、函数相同,则忽略此变量名;如果变量名(已赋值)与前边两名、参数、函数相同,则覆盖。(当然此问题不会这么简单,请往下看到活动对象,再回头理解。)

          此条代码示例:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          function test(a,b,c){
          console.log(a) //函数体a
          console.log(b) // 20
          function a(){
          console.log(1)
          }
          //var a = 100
          // console.log(a) // 100
          var a;
          console.log(a) // 如果未赋值,输出[Function: a];如果赋值则输出100
          var b = 2
          console.log(b) // 2
          }
          test(10,20,30)
          // 由上述可知:函数a覆盖了形参a,后面变声声明覆盖了函数a;变量声明b覆盖了形参b。
          ```
        • 上述三条产生的变量对象的代码讲解:

          代码示例:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          // 因为上面叙述了关于重名的问题,所以这里只做简单的变量对象讲解
          function fn1(a,b){
          console.log(a)
          var c = 10
          function fn2(){
          console.log("fn2被执行了")
          }
          fn2()
          }
          fn1(2,3)
          VO = {
          arguments:{
          a:2,
          b:3,
          length:2,
          callee:[Function:fn1]
          },
          c:undefined,// 注意这里,变量对象中,声明变量未赋值!!!其实上述所谓的覆盖问题可以理解为在此处忽略,真正的覆盖是在代码执行阶段
          fn2:[function reference]
          }
      • 创建作用域链

        • 看下方详解作用域
      • 确定this的指向

        this的指向,是在函数被调用的时候确定的,也就是执行上下文被创建的时候确定的。

        关于this的指向,主要是三种场景:

        • 全局上下文的this:也就是全局对象(不一定是window哦)

        • 函数中的this:如果被调用的函数,被某一个对象所拥有,那么其内部的this指向该对象;如果该函数被独立调用,那么其内部的this指向undefined(非严格模式下指向window)

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          var a = 1;
          function fn(){
          console.log(this.a)
          }
          var obj = {
          a:2,
          fn:fn
          }
          obj.fn();// 2
          fn();// 1
        • 构造函数的this:要清除构造函数中this的指向,必须先了解通过new操作符调用构造函数时所经历的阶段:

          • 创建一个新对象

          • 将构造函数的this指向这个新对象

          • 执行构造函数内部代码

          • 返回这个新对象

            由此可知,对于构造函数来说,其内部this指向新创建的对象实例

    • 执行阶段

      • 执行阶段变量对象会变成活动对象(AO--Active Object),与变量对象不同的是,其声明的变量会该赋值的赋值,该覆盖的覆盖。所以说,活动对象和变量对象的差别并不大。

        接上述例子:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        AO = {
        arguments:{
        a:2,
        b:3,
        length:2,
        callee:[Function:fn1]
        },
        c:10,// 注意这里,变量对象中,声明变量未赋值!!!其实上述所谓的覆盖问题可以理解为在此处忽略,真正的覆盖是在代码执行阶段
        fn2:[function reference]
        }
  • 执行上下文的分类:

    • 全局执行环境:最外围的执行环境,不同的宿主环境表示执行环境的对象也不一样。在Web浏览器中是window
      • js中一切皆对象,则全局变量和函数均可作为全局对象的属性和方法
    • 函数执行环境:这就牵扯到执行上下文的生命周期
  • 执行上下文栈

    • js代码时按照从上往下执行的,代码最先进入的是全局执行

作用域

执行上下文和作用域很多人搞混,主要是其时时刻刻相伴着,比如程序和进程

  • 作用域可以理解为一块代码,编写代码的时候就确定了。其中包含了你所写的变量、常量、函数等等。

  • 【此信息来源某文,后来搜了,没再搜到类似言论,不过可以拿来当做理解。】作用域包含了两部分:

    • 记录作用域内变量信息和代码结构信息的东西,称为Environment Record
    • 一个引用__outer__,这个引用指向当前作用域的父作用。
  • 作用域与上下文的区别:

    • 作用域是在函数定义的时候就确定,而上下文是在函数调用时
    • 作用域是静态的,只要函数定义好了就存在;上下文是动态的,调用函数时就创建,结束调用就自动释放
    • 执行上下文从属于作用域。全局上下文环境–》全局作用域,函数上下文环境–》函数作用域
  • 作用域链详解:

    • 参考原型链,当需要查找某个变量或函数时,会在当前上下文的变量对象(活动对象)中进行查找,若是没有找到,则会沿着上层上下文的变量对象进行查找,直到全局上下文中的变量对象(全局对象)。
    • 其查找的时候所依赖的关系就是作用域链
    • 每个函数都会有一个[[scope]]内部属性,在函数被声明的时候,该函数[[scope]]属性会保存其上下文的变量对象,形成包含上下文变量对象的层级链。
  • ES6中的块级作用域!

    • 上述可知,js的作用域分为全局作用域和函数作用域。但是没有块级作用域。

    • 块级作用域就是在花括号中的代码。js中的if、for、while等都是

    • es5中没有块级作用域的坏处:

      • 在if或者for循环中声明的变量会泄露成全局变量

        1
        2
        3
        4
        for(var i=0;i<=5;i++){
        console.log("hello");
        }
        console.log(i); //5
      • 内层变量可能会覆盖外层变量

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        var temp = "aaa"
        function f(){
        console.log(temp);
        var temp = "bbb"
        }
        f(); //undefined
        // 这里也已经变量提升,上述相当于:
        var temp = "aaa"
        function f(){
        var temp;
        console.log(temp) //所以输出为undefined
        temp = "bbb"
        }
    • es6中添加块级作用域的好处:

      • 允许块级作用域任意嵌套
- 外层作用域无法读取内层作用域的变量

  
1
2
3
    
{let tmp = "hello world"}
console.log(tmp) // error
- 内层作用域可以定义外层作用域的同名变量
1
2
3
    
let tmp = "hello world"
{let tmp = "hello world"}
- 函数本身的作用域在其所在的块级作用域之内
1
2
3
4
5
6
7
8
9
10
11
12
function f(){
console.log("outside")
}
(function(){
if(fasle){
function f(){
console.log("inside")
}
}
f();
}())
//这段代码如果是在ES5中运行,那么会输出inside,因为在ES5中,函数会提升到作用域的顶部,如果是在ES6中运行,则会输出outside,因为在ES6中函数无法提升,所以访问到的f()是外层的f()。
- 在es5中,因为没有块级作用域,获得广泛运用的是立即执行函数。现在es6增加了块级作用域,那么立即执行函数就不再必要了
1
2
3
4
5
6
7
8
//立即执行函数
(function(){
var temp = "hello world";
}());
//块级作用域
{
var temp = "hello world";
}
- 在严格模式下,函数只能在顶级作用域和函数内声明,,在if代码块盒循环代码块下的生命都会报错

变量及函数提升

无论是变量提升和函数提升都是将变量或者函数提升到作用域的最顶部。不过函数提升比变量提升的优先级要高

接下来将是很长很长的代码。

  • 如果变量名(未赋值)与前变量名、参数、函数相同,则忽略此变量名;如果变量名(已赋值)与前边两名、参数、函数相同,则覆盖。 宝典般的话语啊!!!!

  • 变量提升

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function fn1(){
    console.log(a) // undefined
    var a = 10
    console.log(a) // 10
    }
    fn1();
    // 上面为什么是undefined,因为上述代码实际上是:
    function fn1(){
    var a; // 变量被提升至此,但是只声明,不会赋值。所以输出是undefined,而不是error:a is not defined
    console.log(a) // undefined
    a = 10;
    console.log(a) // 10
    }

    1
    2

    - 函数提升:这里也是只有函数声明方式才有函数提升,函数表达式没有。

    console.log(fn1) // [Function:fn1]
    console.log(fn2) // undefined

    function fn1(){
    console.log(“fn1被调用了”)
    }
    console.log(fn1) // [Function:fn1]

    var fn2 = function (){

    console.log("fn2被调用了")

    }
    // 看上面的输出结果就可以发现,函数的提升和变量提升不同。尤其是fn2,输出的是undefined,而不是error。其实这也就类似于变量fn2提升到了上面,但是其未赋值而已,这也就成了变量提升。
    function fn1(){}
    console.log(fn1) // [Function:fn1]
    console.log(fn2) // undefined
    console.log(fn1) // [Function:fn1]
    var fn2 = function (){}

    // !!!上面是输出变量名,下面是进行函数调用
    fn1() // fn1被调用了
    //fn2() //fn2 is not a function

    function fn1(){
    console.log(“fn1被调用了”)
    }
    fn1() // fn1被调用了

    var fn2 = function (){

    console.log("fn2被调用了")

    }
    // 上述代码相当于如下:
    function fn1(){}
    fn1() fn1() // fn1被调用了
    fn2() //fn2 is not a function
    fn1() // fn1被调用了
    var fn2 = function(){}

    // 综上所述:函数表达式的提升按照变量提升来表现,函数名提升了,但是未赋值。所以才会出现console.log(fn2)会输出undefined,而不是报错;但是调用fn2()会出现fn2 is not a function。

// 增加一个示例:变量名和函数名重名的情况
var xxx = 555;
function fn1(xxx){
console.log(xxx)
var xxx = 888;

function xxx(){
   console.log(xxx)
}
console.log(xxx)

}
fn1(234)
// 上述代码可以理解为:
var xxx = 555;
function fn1(xxx){
function xxx(){ // 函数提升比变量提升优先级高,则先打印function

}
var xxx; // 这里虽然是同名,但是其在变量对象中没有值,所以下面打印了function,而后面赋值以后就会覆盖其相同名的function,所以成了8
console.log(xxx) // function
xxx = 8;
console.log(xxx) //8

}



## 参考

- https://juejin.im/post/5c25bcc8e51d451be35e5b41
- https://www.jianshu.com/p/dffdbfdfd09b
- https://www.cnblogs.com/love-life-insist/p/9063104.html
- https://www.cnblogs.com/sxhlf/p/6726976.html
- https://www.cnblogs.com/kawask/p/6225317.html
- https://www.cnblogs.com/luqin/p/5164132.html
- https://www.cnblogs.com/yezi-dream/p/6168557.html