执行上下文
是指当前执行环境中的变量、函数声明、参数(arguments),作用域链,this 等信息。
首先,我们来看看有执行上下文是如何分类的。
执行上下文
的类型有三种:
-
全局执行上下文:仅有一个。当 js 代码被加载完后进入预编译期,即进入了全局环境,创建全局执行上下文,
this
指向全局对象 -
函数执行上下文:存在多个。每当函数被调用时候才会被创建,每次调用函数都会创建一个新的
执行上下文
-
evel 函数执行上下文:运行在
eval
函数中的代码
了解完执行上下文
的类型后,我们知道了执行上下文
存在多个。那么它是如何被保存的呢?
执行上下文栈(Execution context stack,ECS)
也叫做调用栈
,用于存储代码执行过程中创建的所有执行上下文
。
当 javascript 开始解释执行时,其首先遇到的就是全局环境
,所以将全局执行上下文
压入栈中。
然后继续执行,在执行一个函数时,会继续创建一个新的函数执行上下文
,并压入栈中。当该函数执行完毕,再从栈顶中弹出。
执行期间,如果函数中还有函数需要执行,会继续创建新的函数执行上下文
并逐个压入栈中。
具体流程如下图:
了解完了执行上下文栈
是如何处理执行上下文
的,接下来,我们来了解下执行上下文
的属性
一个执行上下文
有三个重要的属性:
-
变量对象(Variable Object)
-
作用域链(Scope chain)
-
this
每创建一个执行上下文
都需要如下的三个步骤:
-
创建变量对象
-
建立作用域链
-
确定当前执行上下文的 this 指向
下面,我们对上述提到的三个属性进行解释:
通俗的说,变量对象
是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
全局执行上下文的变量对象
就是全局对象(在浏览器中是window对象
),在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。
函数执行上下文的变量对象
包括:
-
函数的所有形参。包括参数对象的属性和属性值。(注意:
变量对象
中实参为undefined
) -
函数声明。由名称和对应值组成的函数对象(function-object)作为其
变量对象
的一个属性,如果变量对象中已经存在相同名称的属性,则完全替换。(注意:函数声明优先级高于变量声明。可以用下面代码进行测试,输出的是 foo 函数,而不是 undefined)
console.log(foo); // 输出 function foo
var foo = 'foo1';
function foo() {}
- 变量声明。由名称和对应值(注意此时的值是
undefined
)组成的一个变量对象被创建。
在函数上下文中,我们用活动对象(activation object, AO)
来表示变量对象。
活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
通过下面代码更方便理解:
function foo(a) {
var b = 2;
function c() {}
var d = function () {};
b = 3;
}
foo(1);
对于上面代码,进入函数执行上下文时,对应的变量对象是:
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"
}
作用域链是指:当查找变量时,会先从当前变量对象中查找,如果没有查找到,则查找 “父级变量对象”。以此循环,一直找到全局变量对象,即全局对象为止。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
。
作用域链的第一项永远是当前作用域(当前上下文的变量对象或活动对象);最后一项永远是全局作用域(全局执行上下文的活动对象);
下面,我们通过一个例子来说明
var name = 'kerwin';
function foo() {
console.log(name); // 输出kerwin
}
foo();
在 foo 函数作用域中,没有属性为 name 的变量对象。那么就从它的上一层代码中查找,这时候找到了全局对象声明了name
。则将全局对象中的 name 进行返回 。这就是作用域的一种表达方式。它的实现是在函数创建时,内部有一个[[scope]]
属性会将所有父级变量对象保存到其中。也就存储了上层的所有层级链。
词法作用域和动态作用域
众所周知,js 采用的是词法(静态)作用域
,其作用域在定义函数的时候就确定了。(补充:相反的是动态作用域
,其在函数执行时候才确定作用域。如:bash)
下面,我们通过一个例子来说明词法作用域
和动态作用域
的区别
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
如果是词法作用域
,在执行到console.log(value)
时,由于当前 foo 函数内部不存在value
,则在函数声明位置所在的作用域中查找,返回结果为 1。
如果是动态作用域
,同样在 foo 函数没找到value
,向函数执行所在的作用域中查找,也就是 bar 函数中,返回结果为 2。
全局执行上下文:this
指向全局对象,浏览器中指向window
,node 环境中指向global
函数执行上下文:指向函数的调用者。(也可以通过 call/apply/bind 方法改变 this 指针)