作用域与作用域链

Thursday, December 24, 2020

常见作用域分为 全局作用域 函数作用域

全局作用域

  1. 全局对象下的属性和方法

    window.name
    window.location
    window.top
    ...
    
  2. 最外层声明的变量和方法

    const foo = function() {}
    const str = 'string'
    const arr = [1,2,3]
    function bar() {}
    
  3. 非严格模式下,函数作用域中未定义,直接赋值的变量和方法

    function foo() {
        bar = 20 // window.bar = 20
    }
    

避免使用全局变量

  1. 无意修改,其它场景并不知晓
  2. 极易命名冲突
  3. 全局变量内存无法释放

tips

全局变量的声明有细微差异

  1. var声明,挂载于全局window对象

    var a = 100
    console.log(window.a) // 100
    
  2. let/const声明,挂载于Script对象下(Script对象无法直接访问,于作用域链中)

    const b = 20
    console.log(window.b) // undefined
    

函数作用域

函数声明或函数表达式中,花括号内范围为函数作用域,函数内声明的变量与方法,只能被下层作用域访问

function foo() {
  var a = 20;
  var b = 30;
}
foo();

function bar() {
  return a + b;
}

bar(); // 因为作用域的限制,bar中无法访问到变量a,b,因此执行报错

注意 循环体内没有块级作用域,循环内定义(var)变量,在外部依然可以访问

作用域

作用域的范围信息,在预解析阶段已经确定(未执行前,未产生执行上下文时)

function foo() {
  var a = 1;
  function bar() {
    console.log(a);
      
  console.dir(bar)
}
foo()

块级作用域

利用函数作用域,模拟出块级作用域效果

(
    function() {
        var a = 1
    }
)()
console.log(a) // a is not defind

变量声明

  1. var声明,具备变量提升的特性

    // 同名变量可以重复声明
    var a = 10;
    var a = 20;
    
    // 声明的变量可以任意修改
    var b = 30;
    b = 40;
    b = function() {}
    
    
    // 花括号不会约束 var 变量的作用范围
    {
      var c = 50
    }
    console.log(c) // 50
    
    console.log(a) // 输出 undefined
    var a = 20
    console.log(a) // 输出 20
    ------------------- 相当于
    var a = undeinfd
    console.log(a)
    a = 20
    console.log(a)
    
  2. let声明

    1. 被任何花括号约束,即存在块级作用域
    2. 不能重复声明
    3. 不会提前赋值(无变量提升)(实际存在,但没有赋值undefined)
    4. 存在暂时性死区,不能声明前访问
  3. const声明

    1. 同上
    2. 基础数据类型,无法修改
    3. 引用类型,不能修改引用内存地址
  4. 优先使用const,不适应则使用let

作用域链

每一个函数都有一个 [[Scopes]] 属性,它是由一系列对象组成的数组。每个对象,都对应某一个父级作用域。它们是从对应的父级函数作用域中,收集到的当前函数作用域内会使用到的变量声明、函数声明、函数参数的集合。

function foo() {
  var a = 10
  function bar() {
    console.log(a)
  }
}

预解析首次只回解析最外层函数,检测到函数会被执行时,则对函数进一步进行全量解析,全量解析后再创建该函数的执行上下文,函数内变量/方法在[创建执行上下文]这一步定义,在执行的时候才是进行赋值,创建执行上下文时,会对函数内的函数进行预解析,其余步骤同上

预解析 =》 全量解析 =》 创建执行上下文(定义变量/方法)=》执行(变量/方法赋值)(执行时对函数内的函数进行预解析)

作用域链[[scope]]会进行优化,假如当前函数只使用了父级函数中的一个变量/方法,其作用域链中,该父级函数对应的作用域对象则只有该变量/方法,如果对某一父级函数内变量/方法完全没有使用,作用域链中则会直接去除该作用域 预解析和全量解析都会进行优化

inner.[[Scopes]] = [O(bar), O(foo), O(window)] => inner.[[Scopes]] = [O(bar), O(window)] // O(foo) = {}

假如O(foo)中没有一个对线,那它将会被优化

在当前函数中,要寻找到变量的值是从哪里来的,就首先会从当前执行上下文中查找,如果没有找到,则会去作用域链中查找。这里需要注意的是,作用域链本身就是存在于函数对象中的一个属性 [[Scopes]],因此不是一层一层的往上查找「这里经常理解有误」,该属性是在代码预解析阶段就已经确认好的。

完整作用域链

  • Global 全局对象,不会做任何优化,会包含全局对象中的所有属性与方法
  • **Script 对象 **在全局环境下,由 let 和 const 声明的变量对象
  • **Closure 对象 **我们讨论比较多的闭包对象,嵌套函数生成,仅会保存当前作用域能够访问的变量属性
  • **Local 对象 **以上的几种变量对象,都会存在于函数的 [[Scopes]]属性之中,因为他们都能够在函数解析时确认,而 Local 对象则不行,需要在函数的执行过程中才能确定,并且在执行过程中,该对象中的属性是随时会发生变化的,该对象除了会存储当前函数上下文中所有的变量与函数声明,还会额外记录this的指向

  1. Local当前函数内新声明的变量/方法
  2. Closure,父级函数作用域内的变量/方法 (因此包括自身?)
  3. Script const/let定义的变量
  4. Global 全局window对象