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

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

作用域(scope)

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

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

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

image.png

声明

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

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

上面代码实际等同于

1
2
3
4
5
6
7
8

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

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

全局作用域

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

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

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

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

1
2
3
4
5
6
function a(){
var b = 1
console.log(b) // 1
}
a()
console.log(b) // 报错

块作用域(局部作用域)

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

1
2
3
4
5
6
7
8
9
10
11
{
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. 解决声明提前
    1
    2
    3
    4
    console.log(a) // undefined
    var a = 1
    console.log(b) // 报错
    let b = 2
  2. 解决 {}var 声明被视为全局变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    var c = 1
    }
    console.log(c) // 1

    {
    let d = 1
    }
    console.log(c) // 报错

看实际的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
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
    1
    2
    3
    4
    5
    function fn(fnStr){
    eval(fnStr)
    }
    fn('a = "a"')
    console.log(a) // a
  2. with
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = {
    b: 'b'
    }
    with(a){
    b = 'change'
    c = 'c' // 非严格模式查找键值不存在,会创建一个全局变量!
    }
    console.log(a.b) // 'change'
    console.log(c) // c

动态作用域

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

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

如果是动态作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

实际上执行是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

编译

1
var a = 1
  1. 词法分析:将字符打断成为有意义的片段(token)
    • 比如上面的声明会被打断成如下 token 辅助工具
    • var a = 1 => var a = 1
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      [
      {
      "type": "Keyword",
      "value": "var"
      },
      {
      "type": "Identifier",
      "value": "a"
      },
      {
      "type": "Punctuator",
      "value": "="
      },
      {
      "type": "Numeric",
      "value": "1"
      }
      ]
  2. 解析:将每个 token 数组转换成一个嵌套元素的树,也就是抽象语法树AST(Abstract Syntax Tree)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
    "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. 回收阶段:函数调用完毕后,函数,对应的执行上下文出栈,等待垃圾回收器回收
1
2
3
4
5
6
7
8
9
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)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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