一文带你掌握JavaScript中的执行上下文和作用域

执行上下文

我们先来看段代码

var foo = function () {
 console.log("foo1")
}

foo() // foo1

var foo = function () {
 console.log("foo2")
}
foo() // foo2

那这段代码呢?

function foo() {
 console.log("foo1")
}
foo() // foo2

function foo() {
 console.log("foo2")
}
foo()// foo2

是不是有点懵逼了呢?第一段代码比较好理解,但是第二段代码为什么会打印两个"foo2"呢?

这是因为JavaScript引擎并非一行一行分析和执行程序的。当执行一段代码的时候,会有一些准备工作。那JavaScript引擎到底准备了哪些工作?

下面我们来一点点分析

console.log(a) // undefined
var a = 10

这段代码我们在定义a之前打印了a,但是并没有报错,说明在执行console.log(a)的时候,a就已经被声明了,也就是我们常说的变量提升,这就是准备工作。

var a
console.log(a)
a = 10

首先会把a的定义提前声明,而不是赋值。

下面我们看下对于函数声明和函数表达式,JavaScript引擎是如何做准备的。

console.log(add2(1, 2)) // 3
function add2(a, b) {
 return a + b
}

console.log(add1(1, 2)) // 报错:add1 is not a function
var add1 = function (a, b) {
 return a + b
}

我们发现,用函数语句创建的add2,函数名称和函数体都被提前,在声明它之前使用它。而函数表达式只是变量声明提前了,变量赋值仍然在之前的位置。现在回到刚开始那段代码是不是就理解了呢?

所以JavaScript引擎都做好了哪些准备工作呢?

  • 变量、函数表达式——变量提前声明,默认为undefined
  • 函数声明——提前声明并赋值

其实还有一个this也是提前就准备好了,并且也赋值了。

当执行一个函数的时候,就会进行准备工作,这里的“准备工作”,就是“执行上下文”

执行上下文栈

执行上下文栈管理执行上下文。JavaScript代码有两种执行上下文:全局执行上下文和函数执行上下文,还有一个是eval(我们先不考虑)。全局执行上下文只有一个,函数执行上下文是在每次函数执行调用的时候,就会创建一个新的。

每个执行上下文都有三个属性:

  • 变量对象(Variable object, VO)
  • 作用域链(Scope chain
  • this

变量对象

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

不同执行上下文的变量对象不同,下面来看看全局上下文的变量对象和函数上下文的变量对象

全局上下文

  • 全局对象是预定义的对象,作为JavaScript的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义对象、函数和属性
  • 在顶层的JavaScript代码中,可以用关键字this引用全局对象。因为全局对象是作用域链的头,意味着所有非限定性的变量和函数名都会作为该对象的属性来查询
  • 例如,当JavaScript代码引用parseInt()函数时,它引用的是全局对象的parseInt属性。

函数上下文

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在JavaScript环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫activation object,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象是在进入函数上下文时候才被创建,它通过函数的arguments属性初始化。arguments属性值是Arguments对象。

执行过程

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

  • 进入执行上下文
  • 代码执行

进入执行上下文

当调用函数后,进入执行上下文,在执行代码之前,变量对象会包含:

函数的所有形参

  • 由名称和对应的值组成一个变量对象的属性被创建
  • 没有实参,属性值设为undefined

函数声明

  • 由名称和对应值(函数对象)组成一个变量对象的属性被创建
  • 如果变量对象已经存在相同名称的属性,则完全替换这个属性

变量声明

  • 由名称和对应值(undefined)组成一个变量对象的属性被创建
  • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性 比如:
function foo(a) {
 var b = 2
 function c() {}
 var d = function () {}
 b = 3
}

foo(1)

进入执行上下文后,AO的值:

AO={
 arguments: {
 0:1,
 length:1
 },
 a: 1,
 b:undefined,
 c: reference to function c(){},
 d:undefined
}

代码执行

在代码执行阶段,会按照顺序执行代码,根据代码,修改变量对象的属性的值

AO={
 arguments: {
 0:1,
 length:1
 },
 a: 1,
 b: 3,
 c: reference to function c(){},
 d: reference to FunctionExpression "d"
}

小小总结一下变量对象:

  • 全局上下文的变量对象初始化是全局对象
  • 函数上下文的变量对象初始化包括Arguments对象
  • 进入执行上下文时会给变量对象添加形参,函数声明,变量声明等初始的属性值
  • 在代码执行阶段,会再次修改变量对象的属性值。

下面我们看下执行上下文栈是如何工作的

function fun3() {
 console.log("fun3")
}

function fun2() {
 fun3()
}

function fun1() {
 fun2()
}

fun1()

我们用数组模拟执行上下文栈,最先遇到的是全局代码,初始化的时候,会向执行上下文栈中压入全局执行上下文globalContext

Stack=[
 globalContext
]

当执行一个函数时候,就会创建一个执行上下文,并且压入执行上下文栈中,当函数执行完毕后,就会将函数的执行上下文从栈中弹出。上下文所在其所有的代码执行完毕后会被销毁。

// 执行fun1
Stack.push(<fun1>functionContext);

// fun1中调用了fun2
Stack.push(<fun2>functionContext);

//fun2中调用了fun3
Stack.push(<fun3>functionContext);

//fun3执行完毕 弹出
Stack.pop()

//fun2执行完毕 弹出
Stack.pop()

//fun1执行完毕 弹出
Stack.pop()

最后Stack底层永远有个全局执行上下文globalContext。

作用域

作用域是指程序源代码中定义变量的区域。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。JavaScript采用词法作用域,也就是静态作用域。

静态作用域和动态作用域

JavaScript采用的是词法作用域,函数的作用域是在函数定义的时候决定的。词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

作用域链

查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

函数创建

上面提到,函数的作用域在函数定义的时候就已经决定了。这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,可以理解[[scope]]就是所有父变量对象的层级链,但是[[scope]]并不代表完整的作用域链。我们来看个代码:

function foo(){
 function bar(){
 }
}

函数创建时,各自的[[scope]]为

foo.[[scope]] = [
 globalContext.VO
]

bar.[[scope]] = [
 fooContext.AO,
 globalContext.VO
]

当函数激活,进入函数体,创建VO/AO后,就会将活动对象添加到作用链的前端。

总结

执行上下文和作用域的区别:

1.全局作用域除外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时。

全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建的。

函数执行上下文是在调用函数时,执行函数体代码之前创建的。

2.作用域是静态的,只要函数定义好了就一直存在,且不会再变化。

执行上下文环境是动态的,调用函数时创建,函数调用结束上下文环境就会被释放。

作者:mick

%s 个评论

要回复文章请先登录注册