作用域、执行上下文、作用域链相关知识(JavaScript)

115 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情

作用域(scope)

作用域(scope),它规定了如何去查找变量的规则。通俗点就是说当前执行代码对变量的访问权限

比如我当前定义了这三个值 a b c

var a = 1
var b = function(){}
function c() {}

image.png

声明

变量只有在声明之后才能在作用域中查找到它

function f(a){
  console.log(a + b) // 报错!抛出 ReferenceError b is not defined
  b = a
}
f(2)
function f(a){
  console.log(a + b) // 2 + undefined = NaN
  var b = a
  console.log(a + b) // 4
}
f(2)

上面代码实际等同于


function f(a){
  var b
  console.log(a + b) // 2 + undefined = NaN
  b = a
  console.log(a + b) // 4
}
f(2)

如何声明

  • var:在编译器解析时,会将 var a = 1 视为 var a a = 1 两段执行,存在变量提升
  • let:重复声明报错,属块级作用域,存在变量提升,但会暂时性死区
  • const:重复声明报错,属块级作用域,存在变量提升,但会暂时性死区。内容只读,修改报错,但引用类型保存的是指针
  • function:函数声明,直接提升到最上面,提升优先级最高,且已完成赋值

image.png

静态作用域(词法作用域)

静态作用域(词法作用域),采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见;在这段区域以外该变量不可见。如图 scope1scope2 是互相隔离的,作用域链沿定义的位置往外延伸

image.png

词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。

全局作用域

全局作用域:声明在任何函数之外的顶层作用域的变量就是全局变量,变量拥有全局作用域

var a = 0 // 最外层声明,全局变量
function b(){
  c = 1 // 不使用 var 声明也会被当作全局变量
}
b()
function d(){
  console.log(a, c) // 可以访问全局作用域的变量
}
d()

函数作用域(局部作用域)

全局作用域:声明在函数内的顶变量,拥有函数作用域,外部环境无法访问到函数内部的变量(模块化的原理

function a(){
  var b = 1
  console.log(b) // 1
}
a()
console.log(b) // 报错

块作用域(局部作用域)

块作用域ES6 开始,使用 letconst 声明的变量拥有块级作用域,作用域范围在 {} 之间

{
  let a = 1
  const b = 2
  console.log(a, b) // 1 2 属于块级作用域内
}
console.log(a, b) // 报错
{
  var a = 1
  var b = 2
}
console.log(a, b) // 1 2

为什么需要块级作用域

  1. 解决声明提前
    console.log(a) // undefined
    var a = 1
    console.log(b) // 报错
    let b = 2
    
  2. 解决 {}var 声明被视为全局变量
    {
      var c = 1
    }
    console.log(c) // 1
    
    {
      let d = 1
    }
    console.log(c) // 报错
    

看实际的例子

var val = 'global'
function fn (){
  var val = 'fn'
  console.log(val)
  function _fn(){
    var val = '_fn'
    console.log(val)
    return function inner(){
      var _val = 'inner'
      console.log(val)
    }
  }
  return _fn()
}
console.log(val)
fn()()

image.png

image.png

image.png

function fn(){
  let a = 'a'
  console.log(a)
  if(a){
    let b = 'b'
    console.log(b)
  }
}
fn()

image.png

image.png

image.png

修改词法作用域的方式 evalwith (最好不要用!)

  1. eval
function fn(fnStr){
  eval(fnStr)
}
fn('a = "a"')
console.log(a) // a
  1. with
var a = {
  b: 'b'
}
with(a){
  b = 'change'
  c = 'c' // 非严格模式查找键值不存在,会创建一个全局变量!
}
console.log(a.b) // 'change'
console.log(c) // c

动态作用域

动态作用域,采用变量叫动态变量。程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。

作用域链沿着调用栈往外延伸,通过逐层检查函数的调用链,并打印第一次遇到的值。

如果是动态作用域

var local = 'in global'
function A(){
    var local = 'in A'
    function C(){
        var local = 'in C'
        B()
    }
    B() // in A!
    C() // in C!
    B() // in A!
}
function B(){
    console.log(local)
}
B() // in global!
A()

实际上执行是这样的

var local = 'in global'
function A(){
    var local = 'in A'
    function C(){
        var local = 'in C'
        B()
    }
    B() // in global!
    C() // in global!
    B() // in global!
}
function B(){
    console.log(local)
}
B() // in global!
A()

无论你在哪个位置调用 B 都只会向上查找到 in global

image.png

编译

var a = 1
  1. 词法分析:将字符打断成为有意义的片段(token)
    • 比如上面的声明会被打断成如下 token 辅助工具
    • var a = 1 => var a = 1
    [
      {
        "type": "Keyword",
        "value": "var"
      },
      {
        "type": "Identifier",
        "value": "a"
      },
      {
        "type": "Punctuator",
        "value": "="
      },
      {
        "type": "Numeric",
        "value": "1"
      }
    ]
    
  2. 解析:将每个 token 数组转换成一个嵌套元素的树,也就是抽象语法树AST(Abstract Syntax Tree)
    {
      "type": "Program",
      "body": [
        {
          "type": "VariableDeclaration",
          "declarations": [
            {
              "type": "VariableDeclarator",
              "id": {
                "type": "Identifier",
                "name": "a"
              },
              "init": {
                "type": "Literal",
                "value": 1,
                "raw": "1"
              }
            }
          ],
          "kind": "var"
        }
      ]
    }
    
  3. 代码生成:将抽象语法树转换成可执行代码

执行上下文(Execution Context)

JavaScript 被解析执行时,需要 执行代码的环境 这个环境被称为 执行上下文

分类

  • 全局上下文:全局代码所处的环境,不在函数内的代码均执行与全局上下文中
  • 函数上下文:函数调用时创建的环境
  • eval上下文:运行 eval 函数中代码时创建的环境

生命周期

  1. 创建阶段:此时还未执行代码,只做了准备工作
    • 创建变量对象:arguments,提升函数声明和变量声明
    • 创建作用域链:用于解析变量,从内层开始查找,逐步往外层词法作用域中查找
    • 确定 this
  2. 执行阶段:开始执行代码,完成变量赋值,函数引用等等
  3. 回收阶段:函数调用完毕后,函数,对应的执行上下文出栈,等待垃圾回收器回收
var e = 0
function a(d){
  var b = 1
  function c(){
    console.log(e, b, d)
  }
  c()
}
a(1)
  1. 全局上下文创建阶段

image.png

  1. 全局上下文执行阶段

image.png

  1. 遇到函数调用 a(1) a 函数上下文创建阶段,入栈

image.png

  1. a 函数上下文执行阶段

image.png

  1. c() 函数调用, c 函数上下文创建阶段,入栈

image.png

  1. c 执行完毕,出栈

image.png

  1. a 执行完毕,出栈

image.png

特点

  1. 全局执行上下文在代码开始时创建,有且只有一个,且永远再栈底
  2. 函数被调用时就会创建函数执行上下文,后入栈。(根据调用创建)

变量对象(Variable Object,VO)

变量对象时上下文相关的数据作用域,存储了上下文定义的变量和函数声明

var e = 0
/* 全局执行上下文的变量对象则是 window
window = {
  e: 0
  ...
}
*/
function a(d){
  /* 创建阶段
    VO = {
      arguments: { 0: 1, length: 1 }
      b: undefined
      c: fn()
    }
  */
  var b = 1
  /* 开始执行
    AO = {
      arguments: { 0: 1, length: 1 }
      b: 1
      c: fn()
    }
  */
  function c(){
    /* 开始执行
      AO = {
        arguments: { }
      }
    */
    console.log(e, b, d) // 向外层的作用域查询到变量 e, b,d
  }
  c()
}
a(1)

作用域链(Scope Chain)

多个变量对象构成的链表则为作用域链(Scope Chain),从离它最近的变量对象(VO)开始查找变量,逐级往上

image.png