一文带你掌握JavaScript中的执行上下文和作用域
mick 人气:0执行上下文
我们先来看段代码
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.作用域是静态的,只要函数定义好了就一直存在,且不会再变化。
执行上下文环境是动态的,调用函数时创建,函数调用结束上下文环境就会被释放。
加载全部内容