本文内容主要涵盖了执行上下文栈、执行上下文、变量对象、函数变量提高等内容。java
众所周知,JavaScript
是单线程编程语言,同一时间只能作一件事情,程序执行顺序由上而下,程序的执行主要依托JavaScript
引擎,JavaScript
引擎也并不是一行一行的分析执行代码,而是一段一段的分析执行。git
JavaScript
引擎执行的代码固然是可执行代码,在JavaScript
中可执行代码有三类:全局代码、函数代码以及Eval
代码。github
JavaScript
程序的执行主要分语法检查和运行两个阶段,语法检查包括词法分析和语法分析,目的是将JavaScript
高级语言程序转成抽象语法树。面试
语法检查完成后,到了执行阶段,执行阶段包括预解析和执行,预解析首先会建立执行上下文(本文重点),将语法检查正确后生成的抽象语法树复制到当前执行上下文中,而后作属性填充,对语法树当中的变量名、函数声明以及函数的形参进行属性填充。最后就是执行。编程
JavaScript
运行原理会在后面的文章输出,不是本文的重点,本文只需知道程序运行的大体是怎样的过程,执行上下文什么时候建立。数组
何为执行上下文栈???浏览器
在JavaScript
解释器运行阶段(预解析)还维护了一个栈,用于管理执行上下文。在执行上下文栈中,全局执行上下文处于栈底,顶部为当前的执行上下文。当顶部的执行完成,就会弹出栈,相似于数据结构中的栈,每当有当前的执行上下文执行完就会从栈顶弹出,这种管理执行上下文的栈叫作执行上下文栈。bash
一个执行上下文能够激活另外一个执行上下文,相似于函数调用另外一个函数,能够一层一层的调用下去。数据结构
激活其它执行上下文的某执行上下文被称为调用者(caller
),被激活的执行上下文被称为被调用者(callee
)。一个执行上下文便可能是调用者也有多是被调用者。dom
当一个caller
激活了一个callee
时,caller
会暂停自身的执行,将控制权交给callee
,此时该callee
被放进执行上下文栈,称为进行中的上下文,当这个callee
上下文结束后,把控制权交还给它的caller
,caller
会在刚才暂停的地方继续执行。在这个caller
结束后,会继续触发其余的上下文。
执行上下文栈在JavaScript
中能够数组模拟:
ECStack = [];
复制代码
当浏览器首次载入脚本,会默认先进入到全局执行上下文,位于执行上下文栈的最底部,此时全局代码会开始初始化,初始化生成相应的对象和函数,在全局上下文执行的过程当中可能会激活一些其余的方法(若是有的话),而后进入它们的执行上下文,并将元素压入执行上下文栈中。能够把全部的程序执行看做一个执行上下文栈,栈的顶部是正在激活的上下文。以下表所示:
EC stack |
---|
Active EC |
... |
EC |
Global EC |
在程序结束以前,ECStack
最底部永远是globalContext
:
ECStack = [
globalContext
];
复制代码
看看下面实例一,是一个怎么的过程:
// 实例一
function bar() {
console.log('bar');
}
function foo() {
bar();
}
foo();
复制代码
当执行一个函数时,会建立一个执行上下文并压入执行上下文栈中,当函数执行完毕,就将该执行上下文弹出执行上下文栈。
// 建立执行上下文栈
ECStack = [];
// foo() 建立该函数执行上下文并压入栈中
ECStack.push(<foo> functionContext);
// foo()中调用了bar(),建立bar()执行上下文并压入栈中
ECStack.push(<bar> functionContext);
// bar()执行完毕弹出
ECStack.pop();
// foo()执行完毕弹出
ECStack.pop();
复制代码
执行上下文在程序运行的预解析阶段建立,预解析也就是代码的真正的执行前,能够说是代码执行前的准备工做,即建立执行上下文。
执行上下文有何用,主要作了三件事:
this
、做用域
和做用域链
也是JavaScript
中很重要的知识点,后面的文章会详细的输出。
何为执行上下文?
执行上下文理解为是执行环境的抽象概念,当JavaScript
代码执行一段可执行代码时,都会建立对应的执行上下文,一个执行上下文能够抽象的理解为object
,都包括三个重要属性:
executionContext: {
variable object:vars, functions, arguments
scope chain: variable object + all parents scopes
thisValue: context object
}
复制代码
全局代码不包含函数内代码,在初始化阶段,执行上下文栈底部有一个全局执行上下文:
ECStack = [
globalContext
];
复制代码
当进入函数代码时,函数执行,建立该函数执行上下文并压入栈中。须要注意的是函数代码不包含内部函数代码。
ECStack = [
<xxx> functionContext
...
<xxx> functionContext
globalContext
];
复制代码
eval(...)
有些陌生,平时也不多用到,eval(...)
函数能够接受一个字符串为参数,并将其中的内容视为好像在书写就存在于程序中这个位置的代码。
换句话说,能够在你写的代码中用程序生成代码并运行,就好像是写在那个位置的同样。
eval('var x = 10');
(function foo() {
eval('var y = 20');
})();
console.log(x); // 10
console.log(y); // 'y is not defined'
复制代码
上面实例执行过程:
ECStack = [
globalContext
];
// eval('var x = 10')进栈
ECStack.push(
evalContext,
callingContext: globalContext
);
// eval出栈
ECStack.pop();
// foo funciton 进栈
ECStack.push(<foo> functionContext);
// eval('var y = 20') 进栈
ECStack.push(
evalContext,
callingContext: <foo> functionContext
);
// eval出栈
ECStack.pop();
// foo 出栈
ECStack.pop();
复制代码
变量对象(variable object
)是与执行上下文相关的数据做用域(scope of data
),是与上下文相关的特殊对象,用与存储被定义在上下文中的变量(variables
)和函数声明(function declarations
)。变量对象是一个抽象的概念,不一样的上下文,它表示使用不一样的对象。
全局变量对象是全局上下文的变量对象。全局变量对象就是全局对象,为啥这么说:
全局对象(
Global object
) 是在进入任何执行上下文以前就已经建立了的对象;这个对象只存在一份,它的属性在程序中任何地方均可以访问,全局对象的生命周期终止于程序退出那一刻。
- 全局对象初始建立阶段将
Math
、String
、Date
、parseInt
做为自身属性,等属性初始化,一样也能够有额外建立的其它对象做为属性(其能够指向到全局对象自身)- 在
DOM
中,全局对象的window
属性就能够引用全局对象自身- 能够经过全局上下文的
this
来访问全局对象,一样也能够递归引用自身- 当访问全局对象的属性时一般会忽略掉前缀,全局对象是不能经过名称直接访问的
global = {
Math: <...>, String: <...>, Date: <...>, ... window: global } console.log(Math.random()); //当访问全局对象的属性时一般会忽略掉前缀;初始建立阶段将Math等做为自身属性 console.log(this.Math.random()); // 经过this来访问全局对象 console.log(this) // window 经过全局上下文的this来访问全局对象 var a = 1; console.log(this.a); // 1 console.log((window.a); // 1 全局对象有 window 属性指向自身 console.log(a); // 1 当访问全局对象的属性时一般会忽略掉前缀 this.window.b = 2; console.log(this.b); // 2 复制代码
上面的全局对象的定义和变量对像的定义对比,能知道全局变量对象就是全局对象,简单的说,由于变量对象用于存储被定义在上下文中的变量和函数声明,全局对象在进入任何执行上下文前就已经建立了,一样存储着在全局范围内定义的变量和函数声明。
须要注意的是全局上下文的变量对象容许经过VO
属性名称来间接访问,缘由就是全局变量对象就是全局对象,在其余上下文中是不能直接VO
对象。
全局变量对象VO
会有下列属性:
FunctionDeclaration
, FD
)var
, VariableDeclaration
)Variable object
) 当进入执行上下文时,VO
包含来下列属性:
undefined
;FunctionDeclaration
, FD
),由名称和对应值组成一个变量对象的属性被建立;若是变量对象已经存在相同属性名称,则彻底替换这个属性。var
, VariableDeclaration
),由名称和对应值(undefined
)组成一个变量对象的属性被建立;若是变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。function foo(a) {
var b = 2;
var c = function() {};
function bar() {
console.log('bar');
}
}
foo(10);
复制代码
当进入带有参数10的foo
函数执行上下文时,VO
:
VO = {
a: 10,
bar: <reference to FunctionDeclaration 'bar'>,
b: undefined,
c: undefined
}
复制代码
在函数声明过程当中,若是变量对象已经存在相同的属性名称,则彻底替换这个属性:
function foo(a) {
console.log(a);
function a() {}
}
foo(10) // function a(){}
复制代码
在变量声明过程当中,若是变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
// 与参数名同名
function foo(a) {
console.log(a);
var a = 20;
}
foo(10) // 10
// 与函数名同名
function bar(){
console.log(a)
var a = 10
function a(){}
}
bar() // 'function a(){}'
复制代码
VO
建立过程当中,函数形参的优先级是高于函数的声明的,结果是函数体内部声明的function a(){}
覆盖了函数形参a
的声明,所以最后输出a
是一个function
。
从上面的实例说明,函数声明比变量声明的优先级高,在定义的过程当中不会被变量覆盖,除非是赋值:
function foo(a){
var a = 10
function a(){}
console.log(a)
}
foo(20) // 10
function bar(a){
var a
function a(){}
console.log(a)
}
bar(20) // 'function a(){}'
复制代码
Activation object
)活动对象想必你们对这个概念都不陌生,可是容易和变量对象混淆。
活动对象就是函数上下文中的变量对象,只是不一样阶段的不一样叫法,在建立函数执行上下文阶段,变量对象被建立,变量对象的属性不能被访问,此时的函数尚未执行,当函数来到执行阶段,变量对象被激活,变成了活动对象,而且里面的属性都能访问到,开始进行执行阶段的操做。
// 执行阶段
VO -> AO
function foo(a) {
var b = 2;
var c = function() {};
function bar() {
console.log('bar');
}
}
foo(10);
VO = {
arguments: {
0: 10,
length: 1
},
a: 10,
bar: <reference to FunctionDeclaration 'bar'>,
b: undefined,
c: undefined
}
AO = {
arguments: {
0: 10,
length: 1
},
a: 10,
bar: <reference to FunctionDeclaration 'bar'>,
b: 2,
c: reference to FunctionExpression "c"
}
复制代码
调用函数时,会为其建立一个Arguments
对象,并自动初始化局部变量arguments
,指代该Arguments
对象。全部做为参数传入的值都会成为Arguments对象的数组元素。
简洁的总结下上面的内容:
Arguments
对象;提高一个很常见的话题,是面试中常常被问到的一部分,函数声明优先级比变量声明高,这句话应该是大部分同窗都会回答,为啥,上面的内容已经很好的作出了解释。看下面实例:
function test() {
console.log(foo); // function foo(){}
console.log(bar); // undefined
var foo = 'Hello';
console.log(foo); // Hello
var bar = function () {
return 'world';
}
function foo() {
return 'hello';
}
}
test();
复制代码
// 建立阶段
VO = {
arguments: {
length: 0
},
foo: <reference to FunctionDeclaration 'foo'>, // 解释了第一个输出是foo引用,函数声明优先变量被建立,同名属性不会被干扰,在函数尚未被调用前已经被建立了,即能输出foo的引用
bar: undefined // 解释了第二个输出是undefined,函数表达式仍是只是一个变量声明,不是函数声明,不会被提高
}
复制代码
// 执行阶段
VO -> OV
OV = {
arguments: {
length: 0
},
foo: 'Hello', // 这里解释了为何第三个输出值为‘Hello’,作了赋值操做
bar: reference to FunctionExpression "bar"
}
复制代码
// 实例真实的执行顺序
function test() {
function foo() {
return 'hello';
}
}
var foo;
var bar;
console.log(foo);
console.log(bar);
foo = 'Hello';
console.log(foo);
bar = function () {
return 'world';
}
}
复制代码
须要注意的是变量提高只存在使用var
关键字声明变量,若是是使用let
声明变量不存在变量提高。
声明变量的做用域限制在其声明位置的上下文中,在上下文被建立的阶段时建立了,若是没有声明的变量老是全局的,而且是在执行阶段将赋值给未声明的变量的值被隐式建立为全局变量,能够经过delete
操做符删除,声明变量不能够。
function foo() {
console.log(a); // Uncaught ReferenceError: a is not defined;a不存在VO中
a = 1;
console.log(a);
}
foo();
function bar() {
a = 1;
console.log(a); // 1 能够在全局变量中找到a的值
}
bar();
c = 10;
console.log(delete c); // true
var d = 10;
console.log(delete d); // false
复制代码
若是清楚上下文相关的内容,提高的问题很好的能解答,在学习中咱们仍是须要了解一些底层的知识,这样有助咱们更好的进步。
文章若有不正确的地方欢迎各位大佬指正,也但愿有幸看到文章的同窗也有收获,一块儿成长!
-----------------------------本文首发于我的公众号---------------------------