当执行一段代码的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做“执行上下文(execution context)”。
执行上下文会产生三种执行上下文,分别是:
每个执行上下文中都会有三个重要的属性:
接下来我们先来看一段代码:
1 | function fun3(){ |
这段代码是很简单,我们可以很快的就得出打印的内容为fun3。但是引擎没有我们那么智能,它需要知道它是如何进行一个先后顺序来执行的,那么引擎该如何管理创建的那么多执行上下文呢?
JavaScript引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
关于上面的代码,接下来我会画一张图来解释执行上下文栈是怎么执行的:
变量对象:是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
步骤一:初始化
a. 全局上下文,就等价于全局对象window。
全局对象是预定义的对象,作为JavaScript的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。
在最顶层JavaScript代码中,可以用关键字this引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。
例如,当JavaScript代码引用parseInt()函数时,它引用的是全局对象的parseInt属性。全局对象是作用域链的头,还意味着在顶层JavaScript代码中声明的所有变量都将成为全局对象的属性。
b. 函数上下文。
在函数上下文中,我们用活动对象(activation object,AO)来表示变量对象。活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在JavaScript环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活(即只有当执行这个函数的时候才会被激活),所以才叫activation object的,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
活动对象是在进入函数上下文时被创建的,它通过函数的arguments属性初始化(即它初始化的值就是我们的一个实参)。arguments属性值是Arguments对象。
步骤二:进入执行上下文
变量对象会包括:
a. 函数的所有形参(如果是函数上下文)
(1) 由名称和对应值组成的一个变量对象的属性被创建;
(2) 没有实参,属性值设为undefined。
b. 函数声明
(1)由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建;
(2)如果变量对象已经存在相同名称的属性,则完全替换这个属性。
c. 变量声明
(1)由名称和对应值(undefined)组成一个变量对象的属性被创建;
(2)如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
步骤三:代码执行
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。
看完这些之后,我们来做一个小例子进行一个练习,代码和解析如下所示:
1 | function foo(a){ |
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级,即定义函数它的父级,而不是执行函数的一个父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
函数创建
这是因为函数有一个内部属性[[scope]],当函数创建(即函数定义)的时候,就会保存所有父变量对象到其中,你可以理解[[scope]]就是所有父变量对象的层级链,但是注意:[[scope]]并不代表完整的作用域链!!!
函数激活
当函数激活时(即执行函数),进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。这时候执行上下文的作用域链,我们命名为scope:scope = [AO].concat([[scope]])
举一个例子,代码如下所示:
1 | function foo(){ |
接下来我们举一个实例,然后对上面的进行一个总结和练习,代码如下所示:
1 | var scope = "global scope"; |