[JavaScript]执行上下文相关的总结

754 阅读5分钟

上次写了作用域相关的知识总结,这篇文章是它的进一步提升。如果对作用域还不是很了解,可以先看一下:JavaScript作用域相关的总结

我翻了挺多文章,感觉他们对上下文这个词的解释都有些抽象,不如干脆就放过他的字面意义,而是记住他是如何产生的,以及作用

一、 执行上下文的产生

在执行到函数的时,会进行准备工作,这里的"准备工作",就叫做"执行上下文"

顾名思义,该过程发生在代码执行阶段(重要!javaScript代码的运行会经过准备(包含变量初始化、函数定义等)执行(逻辑)两个阶段)

当JavaScript执行一段可执行代码时,就会先建立对应的执行上下文

执行上下文的代码会分成两个阶段进行:

  • 进入执行上下文(分析)
  • 代码执行(处理)

在生成的每个执行上下文中,都会有3个重要的属性:

  • 变量对象
  • 作用域链
  • this

二、执行上下文的类别

1. 全局上下文

全局上下文可以说是最开始执行javaScript代码时就会存在的事物。它的变量对象就是全局对象,this指向window,作用域链是最顶层

console.log(this) //window
console.log(Math.random())
console.log(this.Math.random()) //均能生效

var a = 1
console.log(window.a) //1
console.log(this.a) //1

2. 函数上下文

在进入函数时创建

二、变量对象

变量对象存储了在该上下文中定义的变量函数声明

进入执行上下文时(未代码执行),变量对象会进行初始化,包括:

  1. 函数的所有形参(arguments)
  2. 函数声明
  3. 变量声明
function a(n, m) {
    var b = 1
    function c() {
        console.log('c')
    }
    var d = function() {
        console.log('d')
    }
    b = 2
}
a(10, 20)

在进入执行上下文后,此时的变量对象是这样的:

变量对象 = {
    arguments: {
        0: 10,
        1: 20,
        length: 2
    },
    n: 10,
    m: 20,
    b: undefined,
    c: function,
    d: undefined
}

等到了代码执行时,代码会顺序执行,从而更改变量对象的值。当代码执行到最尾时,变量对象是这样的:

变量对象 = {
    arguments: {
        0: 10,
        1: 20,
        length: 2
    },
    n: 10,
    m: 20,
    b: 1 -> 2,
    c: function,
    d: functionExpression
}

二、作用域链

作用域总结中已经提过相关知识。作用域和作用域链的作用是定义对于一个变量的保存范围和查找方向

在代码准备阶段,会创建对应的所有的代码作用域 通过上文说的简单理解,会保存着他们的层级关系,比如说:

var a = 1

function foo() {
    var b = 2
    function bar(c) {
        var d = 3
        console.log(a, b, c, d)
    }
    bar(b * 3)
}

function kk() {
    var e = 4
    console.log(a, e)
}

最终创建出作用域:[全局作用域 [foo作用域 [ bar作用域 ], kk作用域]]。如果从顶层到最底层的话,有

  1. 全局作用域 -> foo作用域 -> bar作用域
  2. 全局作用域 -> kk作用域
  • 在bar函数里面,可以用到foo函数里面定义的变量,再往上可以用到全局里面定义的变量
  • 在foo函数里面,可以用到全局里面定义的变量,但用不了bar函数里面定义的变量(只可以往上不可以往下

至此在执行上下文准备阶段,初始化了作用域和作用域链,并保存到执行上下文的相关属性中,便于之后的使用

在接下来的变量查找使用,均是按照作用域链的方向来查找(第二步执行阶段)

var a = 1 //1.将a从undefined赋值为1

function foo() {
    var b = 2 //3.将b从undefined赋值为2
    function bar(c) {
        //6.将参数c从undefined赋值为6
        var d = 3 //7.将d从undefined赋值为3
        console.log(a, b, c, d)
        //8.1打印a,在本作用域内不能拿到,往上一层到foo作用域,找不到,再往上一层到全局作用域,可以查到,取该值a=1
        //8.2打印b,在本作用域内不能拿到,往上一层到foo作用域,可以查到,取该值b=2
        //8.3打印c,在本作用域内能拿到,取值c=6
        //8.4打印d,在本作用域内能拿到,取值d=3
    }
    // 4.计算b*3 -> b在本作用域内不能拿到,根据作用域链往上查,从foo作用域拿到b=2,计算得b*3=6,代入函数
    bar(b * 3) //5.顺序执行代码
}

foo() //2.顺序执行代码

三、this

JavaScript有一套不同于其他语言的对this的处理机制。要深究的话原理听复杂的,基本上可以总结为这五种不同的情况:

  • 全局范围内 this - 指向全局对象
  • 函数调用 foo() - 指向全局对象
  • 方法调用 bar.foo() - 指向bar对象
  • 调用构造函数 new foo() - 指向新创建的对象
  • 显式设置 apply/call - 指向函数第一个参数

常见误解

Foo.method = function() {
    function test() {
        console.log(this)
    }
    test() // window
}

这里会被认为test函数中的this会指向Foo对象,实际上不是这样子的,而是会指向全局对象。如果想拿到Foo的内部this,可以这样写

Foo.method = function() {
    let that = this
    function test() {
        console.log(that)
    }
    test() // Foo

另一个看起来奇怪的地方是函数别名,也就是将一个方法赋值给一个变量。上例中b 就像一个普通的函数被调用。因此,函数内的 this 不被指向到 a 对象,而是全局对象

let a = {
    b: function() {
        console.log(this.c)
    },
    c: 1
}
let mb = a.b
mb() // undefined (指向window)

四、总结

执行上下文的创建准备会使上面提到的三个属性初始化,在代码执行阶段中扮演着重要的作用,约定着各值

像闭包、高阶函数等使你觉得变量怎么变来变去的事物,都是依赖于该基础之上。懂了它的原理内容,再去看就不是那么难了