什么是执行上下文?
执行上下文就是当前代码被解析和执行时所在的环境。
Global code
:默认的执行环境,当代码第一次执行时所在的环境Function code
:当进入到一个函数体时的执行环境Eval code
:当一段代码传入到eval
函数执行时的环境
紫色框包裹的为全局上下文,person
、firstName
、lastName
3个函数上下文,整个代码中只有一个全局上下文,它能够被其他三个上下文访问。
执行上下文栈
浏览器中的JS解析器是单线程的,这意味着一个时间只有一个事情发生(一行代码执行),操作与事件均被压到执行栈里面
当浏览器第一次加载脚本,默认会进入全局执行上下文,如果在全局代码中调用一个函数,那么程序流将进入这个函数体,创建一个函数执行上下文,同时这个上下文将在执行栈顶部
如果在当前函数的内部调用其他函数,那么同样的过程将会发生,程序流进这个函数体,创建一个新的执行上下文,同时上下文被压入栈顶。浏览器总是执行位于栈顶部的上下文,当一个函数执行完成,它的上下文将从栈顶移除,并将程序的控制权返回给下一层的上下文。
(function foo(i) {
if (i === 3) {
return;
} else {
foo(++i);
}
})(0);
执行栈的变化情况
上面的代码调用自己三次,均将变量加一。每次函数foo
的调用,一个新的执行上下文被创建。一旦完成了执行,将从栈顶移除,并返回下一层执行上下文,直到栈内只剩下全局上下文。
执行上下文关键点
- JS的代码执行是单线程
- 执行栈内的代码执行均是同步的
- 有且只有一个全局上下文
- 可以有数量不限的函数上下文
- 每次函数调用均创建一个执行上下文,函数自己调用自己也如此
深入执行上下文
每次函数调用将会创建一个执行上下文。在JS解析器中,每次创建函数上下文将经历两个阶段。
1、创建阶段
- 创建一个作用域链
- 创建函数内部的变量、函数和参数
- 对
this
进行复制
2、代码执行阶段
- 为变量进行赋值,函数引用以及代码执行
executionContextObj = {
'scopeChain': { ... },
'variableObject': { ... },
'this': {}
}
1.scopeChain:包括variableObject
,以及所有外层上下文变量
2.variableObject:主要包括函数的参数、内部变量以及函数的声明
变量对象(Variable Object)
在函数调用后,且在函数内代码执行前,会创建一个执行上下文对象executionContextObj
,这是我们所说的JS解析器的第一阶段:创建阶段。具体来说,解析器创建一个executionContentObj
会先扫描函数传入的参数,以及函数内部的变量声明与函数声明,讲这些参数和声明作为执行上下文对象的属性variableObject
详细过程
1、找到调用函数的函数体
2、在执行函数体之前,创建一个执行上下文对象
3、进入创建阶段
-
初始化作用域链(scopeChain)
-
创建一个变量对象
- 创建参数对象(arguments object),检查参数的上下文,初始化参数的名称和值
- 扫描上下文中的函数声明
- 对于每一个函数声明,将在
variableObject
中创建一个属性,属性名为函数名,并指向改函数对象在内存中的引用 - 如果该函数已存在,
variableObject
中的值将会被重写
- 对于每一个函数声明,将在
- 扫描上下文中的变量声明
- 对于每一个变量声明,均会在
variableObject
中创建一个属性,属性名为变量名,同时将该属性的值初始化为undefined
- 如果该属性已经被声明,那么什么也不做,直接跳过
- 对于每一个变量声明,均会在
- 明确上下文中
this
的值
4.代码执行阶段
- 在上下文中执行代码,分配变量值,并逐行依次执行代码
function foo(i) {
var a = 'hello';
var b = function privateB() {};
function c() {}
}
foo(22);
在调用foo(22)
后,创建阶段的执行上下文对象
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
若上所示,在创建阶段只对变量的声明定义(函数参数除外),并没有为他们分配值。一旦函数阶段创建完成,程序流将进入代码执行阶段,在函数完成执行后,执行上下文对象fooExecutionContext
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
命名提升
命名提升是指:在函数内部的变量声明与函数声明被提到函数域的顶部
(function() {
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
}());
我们通过问答的形式来探讨命名提升的发送过程
1.为什么可以在foo
声明前访问到?
- 在创建阶段,所有变量都已经被创建,因此在函数执行的时候,变量
foo
已经定义在上下文的变量对象中
2.foo
声明了两次,为什么foo
被指向function
,而不是undefined
或者string
?
- 尽管
foo
声明了两次,然而在创建阶段时,函数声明将先于变量被定义到上下文对象中 - 因此,一个指向
function foo()
的引用首先被创建,接着,当解释器扫描到var foo
,已经看到属性名foo
存在了,什么也不会做,直接跳过
3.为什么的bar
的类型为undefined
?
bar
是 一个指向函数表达式的变量,在创建阶段变量的值会被初始化为undefined
原文链接 http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/#first-article