js中的执行上下文--声明提升,暂时性死区等

在JavaScript代码开始执行前,js解析器做好了一系列的准备工作,并且规定了变量、函数的访问路径。(这个为个人的理解,如有理解错误,请大佬们指正!)
JavaScript在代码运行前做了什么?会创建执行上下文,也就是准备好代码运行时的环境,包括变量对象、作用域链、this
代码运行阶段,变量、函数、this的访问,均是从其执行上下文中获取。其中变量的获取分为:执行上下文创建阶段变量初始化的获取,和运行阶段变量赋值后的获取。函数和this在创建阶段已经绑定了引用。

一、执行上下文

在js代码执行前,会创建执行上下文,也就是所谓的代码运行环境。那么该运行环境有什么作用?————实际上,代码运行时,关于变量的访问、函数的调用及this的指向绑定,都和执行上下文有着密切的关系,可以认为,都是从执行上下文中获得它们的值。

1.1运行环境

JavaScript代码有三种运行环境,对应着三种执行上下文

  • global code:全局作用域下的代码。不包括任何function体内的代码
  • Function Code:函数调用执行的代码,不包括其自身内部的函数的代码
  • Eval Code:eval()执行的代码

1.2EC三个属性

执行上下文是一个对象,它具有三个属性:scope chain,variable object,this

  • 变量对象(variable object):vars、function declarations,arguments...
  • 作用域链(scope chain):variable object + all parent scopes
  • this:context object
1.2.1变量对象variable object

​变量对象是与执行上下文相关的数据作用域。存储了在上下文定义的变量和函数声明。

  • variable declaration,初始化为undefined
  • Function declaration,保存函数名称并持有函数体的引用
  • 函数的形参,初始化为undefined
  • 函数执行上下文中,没有使用var声明的变量为全局变量,所以不在变量对象中
  • 函数表达式也不包含在变量对象中
  • let,const声明的变量初始化为uninitialed
1.2.2作用域链scope chain

​全局执行上下文没有外部的作用域,因此定义其作用域链为自身的变量对象。

​函数执行上下文中的作用域链:实际上,当函数定义的时候,函数所有的外层变量对象(即集合)都会保存在函数的内部属性[[scope]]中,当创建函数执行上下文时,首先创建了该对象的属性--作用域链,并把[[scope]]复制到scope chain中,但这并不是完整的作用域链。接着便是变量对象variable object的创建,创建过程见下文——JavaScript代码的执行前后做了什么--执行上下文创建的完整过程。创建完成后,把变量对象复制到scope chain的顶端,形成完整的作用域链。

1.2.3this

this在创建执行上下文时进行绑定

1.2.3.1全局代码中的this

this的绑定指向始终为window对象

console.log(this)//window,非严格模式下
//严格模式下为undefined
1.2.3.2函数代码的this
  • 默认绑定:函数调用时,若无前缀,则this绑定为window

    function test() {
        console.log(this)
    }
    test()//window
    
    function f1(){
        function f2(){
            console.log(this)
        }
        f2()
    }
    f1()//window
    
  • 隐式绑定:函数调用时,前面存在调用它的对象,则this会隐式绑定到这个对象上

    function f1(){
        console.log(this.name,this)
    }
    f1()//window
    let obj = {
        name:'hello',
        fn:f1
    }
    obj.fn()//
    
  • 显式绑定:call,apply,bind改变this

  • new绑定:

    new一个函数,实际进行了的操作:

    • 创建一个空对象,将它的引用赋给this,继承函数的原型
    • 通过this将属性和方法添加到这个对象
    • 若没有手动返回其他的对象,则最后返回this指向的对象(即实例)
    • 以构造器的prototype属性为原型创建一个对象
    • 将这个对象和调用参数传给构造器执行,apply。。
    • 如果构造器没有手动返回对象,则返回第一步的对象

    这个实例中的方法中(即对象的方法内的代码中)的this指向所创建的实例

  • 箭头函数中的this

    ​ 箭头函数没有自己的this,this指向取决于外层作用域中的this,一旦箭头函数的this绑定成功,也无法被再次修改,但可以修改外层函数的this指向达到间接修改箭头函数this的目的。
    题外话:定时器中的回调函数,如果是匿名函数,那么this指向window对象,可以改用箭头函数,那么this则默认指向上层作用域中的this,

二.JavaScript代码的执行前后做了什么----EC的创建过程

当一段JavaScript代码执行的时候,JavaScript解释器会创建执行上下文,包含了两个阶段:

  • 创建阶段(函数被调用,但在开始执行函数内代码之前)
    • 创建scope chain:复制函数属性[[scope]]scope chain,在变量对象创建完后,将其添加到scope chain的前端,形成完整的作用域链
    • 创建VO/AO
      • 根据函数的参数,创建并初始化arguments object
      • 扫描函数内部代码,查找函数声明
        • 对于所有找到的函数声明,将函数名和函数引用存入到VO/AO
        • 如果VO/AO中已有同名的函数,那么就进行覆盖
      • 扫描函数内部代码,查找变量声明
        • 对于所有找到的var变量声明,都存入到VO/AO中,并初始化为undefined
        • let,const变量声明,初始化为uninitialed
        • 如果变量名称和已经声明的形式参数或函数相同,那么变量声明将不起作用,保留后者,也就是说后者不会受前者声明的干扰
    • 设置this的值
  • 激活/代码执行阶段
    • 设置变量的值、函数的引用,解释/执行代码

三、结合执行上下文,解释变量及函数的访问规则--声明提升

js代码开始执行时,浏览器便会创建全局执行上下文(详见EC的创建过程),每有一次函数调用,则创建一次函数执行上下文。
进行变量、函数及this值的访问时,都会在当前执行上下文中的作用域链进行访问。

声明提升:

console.log(var1);//undefined
var var1 = 1;
console.log(var1);//1
console.log(fn);
function fn(){
      console.log('1111');
}

解析:访问时,都是从执行上下文中的作用域链进行取值,创建执行上下文时,var声明的变量被初始化为undefined,因此在变量被赋值前进行访问,打印了undefined,赋值后访问,打印1;
函数的声明提升同理,只不过创建执行上下文时,函数的标识符初始化为函数体的引用,故打印的为函数体。

let,const的暂时性死区:声明前不能进行访问

console.log(a);
let a = 1;
let b;
console.log(b)

解析:创建执行上下文时,被初始化为uninitialed,访问uninitialed值会抛出错误,而赋值后,则可以访问let,const声明的变量。(代码运行到let声明语句时,若没有进行赋值操作,则默认赋值为undefined,const声明变量时必须初始化)

原文地址:https://www.cnblogs.com/joeynkay/p/13527069.html