JavaScript 执行上下文,执行栈,作用域链
文章目录
一.简介
执行上下文也叫执行环境,指的就是当前Javascript代码被解析和执行所在环境的抽象概念,Javascript中运行任何的代码都是在执行上下文中运行.
二.执行上下文的类型
执行上下文,关系到Javascript程序内部的执行机制,执行上下文,有三类
1.全局执行上下文
全局执行上下文是最外围的一个执行环境,可以这么理解,不在任何函数中的代码都位于全局执行上下文中.在此期间,共发生两个过程:
(1) 创建一个全局对象,在浏览器中,这个全局对象就是window对象
(2) 将this指针指向这个全局对象
注意:一个程序中,只能存在一个全局执行上下文
当关闭网页和浏览器时,全局执行环境才会被销毁
1.变量对象VO()
变量对象VO(Variable Object):每个执行上下文都有一个与之关联的变量对象,执行环境中定义的所有变量和函数都保存在这个变量对象中.
在web浏览器中,全局执行上下文的变量对象是window对象
全局对象window上预定义了大量的方法和属性,同时window对象还是var声明的全局变量的载体
2.变量对象详解
变量对象的创建,要依次经历以下几个过程.
变量对象 VO={
}
(1) 建立arguments对象: 检查当前上下文中的参数,建立该对象下的属性与属性值
console.log('start');
function start(x,y){
}
/**
VO=Window{
start:ƒ start(),
x:undefined,
y:undefined
}
**/
(2) 检查当前上下文的函数声明,也就是使用function关键字声明的函数.在变量对象中以函数名建立的一个属性,属性值为指向该函数所在内存地址的引用
(3) 检查当前上下文中变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined
ps:如果变量与函数同名,则在这个阶段,以函数值为准
console.log(fn);//ƒ fn(){}
function fn(){}
var fn=1;
其实这个过程就是JavaScript引擎预解析的过程,通过这个过程可以更好的理解声明提升
!
3.let/const和变量对象的关系
我们都知道,var
声明的变量,会有声明提升机制,但是let/const有没有呢?答案肯定是没有的,我们用过很多次了,但是,虽然没有声明提升机制,但是有没有预解析阶段呢?答案是有的.var
和let/const
在预解析阶段,都会将声明的变量放入变量对象里面,只不过let/const
在预解析时,和var
声明的变量存储的位置不同罢了
2.函数执行上下文
每次调用函数时,都会为该函数创建一个新的执行上下文,每个函数都拥有自己的执行上下文.但是只有在函数被调用的时候,才会被创建,一个程序中可以存在多个函数执行上下文.
1.活动对象
活动对象 AO(Activation Object)当函数调用的时候,会创建一个特殊的对象=>活动对象.也就是说函数的执行上下文里,是把活动对象当做是变量对象的活动(对象是作为局部执行上下文的变量对象来使用的),活动对象包含形参和arguments对象
实际上,变量对象和活动对象的作用是一样的,都是为了记录保存我们的变量的.
3.Eval函数执行上下文
运行在eval函数中的代码也获得了自己的执行上下文.eval方法是在运行时对脚本进行解释执行,而普通的javascript会有一个预处理的过程。所以会有一些性能上的损失;eval也存在一个安全问题,因为它可以执行传给它的任何字符串,所以永远不要传入字符串或者来历不明和不受信任源的参数。
这个Eval用的比较少,不太了解,不做介绍
三.执行栈
执行栈,也叫调用栈,是一种后进先出的数据结构,当一个脚本执行的时候,js引擎会解析这段代码,并存储在代码执行期间创建的所有执行上下文.
当JavaScript引擎首次读取脚本时,会创建一个全局执行上下文并将其push
到当前的执行栈中,每当发生函数调用时,引擎都会为该函数创建一个新的执行上下文并push
到当前执行栈的栈顶.
引擎会运行执行上下文在执行栈顶的函数,根据后进先出
原则,当此函数运行完成后,对应的执行上下文将会从执行栈中Pop
出,也就是删除掉,执行上下文控制权将转到当前执行栈的下一个执行上下文.
通俗的讲:每个函数都有自己的执行上下文,当执行流进入一个函数时,函数的执行上下文就会被加入到调用栈中,而这个函数执行完毕之后,调用栈将这个函数的执行上下文删除,把控制权交给之前的执行上下文.
所以可以理解为,JS
代码执行完毕前在执行栈底部,永远有个全局执行上下文
1.调用栈管理执行上下文
<script>
let = 'Hello World!';
function foo1() {
console.log('foo1 函数开始');
foo2();
console.log('foo1 函数结束');
}
function foo2() {
console.log('foo2函数');
}
foo1();
console.log('全局的上下文环境');
</script>
通过上面这个示例来解读一下浏览器的内部运行机制!既然调用栈是以执行上下文为单位的,那我这边为了更好的理解,以执行栈是一个数组为例子来解析.
(1) 代码执行时会创建一个全局执行上下文,并加入到当前执行栈的数组中.
(2) 当调用foo1
函数时,JavaScript引擎就会为该函数创建一个新的函数执行上下文,并将其推到当前执行栈的顶端.
(3) 在foo1
的函数中调用foo2
的函数时,JavaScript
引擎又为该函数创建了一个新的执行上下文,并将其推到当前执行栈的顶端
(4) 当foo2()
的函数执行完成后,它的执行上下文从当前执行栈中删除,然后将控制权交给之前的执行栈上下文,也就是foo1
的函数执行上下文
(5) 当foo1函数执行完毕,它的执行上下文在执行栈中删除,上下文控制权将交给全局执行上下文
(6) 所有代码全部执行完毕,JavaScript引擎把全局执行上下文从执行栈中移除
//代码执行前创建全局执行上下文
ECStack = [globalContext];
// foo1调用
ECStack.push('foo1 functionContext');
// foo1又调用了foo2,f2执行完毕之前无法console.log('foo1 函数结束');
ECStack.push('foo2 functionContext');
// f002执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文
2.执行上下文是怎么创建的
执行环境(EC) 建立分为两个阶段
创建阶段解释器扫描传递给函数的参数或arguments,本地函数声明和本地变量声明,并创建EC对象
内部执行顺序如下(执行上下文生命周期
):
(1) 查找调用函数的代码
(2) 执行函数代码之前,先创建执行上下文
(3) 进入创建阶段
- 初始化作用域链
- 创建变量对象
- 创建arguments对象,检查上下文初始化参数名称和值并创建引用的复制
- 扫描上下文的函数声明
- 扫描上下文的变量声明
- 求出上下文内部
this
的值
(4) 激活代码执行阶段
- 在当前上下文上解释/运行函数代码,并随着代码一行行执行指派变量的值
(5) 销毁阶段
- 执行完毕,执行上下文出栈,等待回收
3.执行上下文谈this
(1) 在全局执行上下文中,this的指向是全局对象,浏览器中,this指向windo对象
(2) 在函数执行上下文中,this的指向取决于函数的调用方式,如果它被一个对象引用调用,那么this的指向则就是该对象,否则this的值被设置为全局对象或undefined(严格模式)
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
console.log(obj.c);//40
console.log(obj.fn());//10
先抛出一个结论,接下来,我们再来论证:
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定.如果调用者函数,被一个对象所拥有,那么该函数在调用时,内部的this指向该对象.如果函数独立调用,那么该函数的内部的this,则指向window这个全局对象.
在上述代码中,对象obj
中的c
属性使用this.a+20
来进行计算,我们特别要注意的是:单独的{}
不会形成新的作用域,也就是说并不会产生新的执行上下文,因此,这里的this.a
还处于全局执行上下文中,这里的this,应该指向window
再来一个箭头函数的示例
var obj = {
a: 10,
b: {
a: 11,
fn: () => {
console.log(this.a);
console.log(this);
}
}
}
obj.b.fn()
ES6的箭头函数是另类的存在,准确来说,箭头函数中没有this,箭头函数的this指向取决于外层作用域中的this,外层作用域或函数的this指向谁,箭头函数中的this便指向谁.
上述代码,箭头函数的外层作用域就是window,所以这里指向window
四.作用域和作用域链
1.作用域
词法作用域,动态作用域
词法作用域:也叫静态作用域,它的作用域是指词法分析阶段就确定了,不会改变
// 词法作用域
var abc = 1;
function f1() {
console.log(abc);
}
function f2() {
var abc = 2;
f1();
}
f2();//1
动态作用域:是在运行时根据程序的流程信息来动态确定的,而不是在写代码时静态确定的.(比如this指向,除箭头函数外,就是动态作用域)
主要区别:词法作用域是在写代码或者定义时确定的,而动态作用域是在运行时确定的.词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用.
2.作用域链
作用域链和作用域是不同的.作用域是一套规则,而作用域链则是在代码执行过程中,会动态变化的一条索引路径
作用域链:是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问
通过一个示例来理解下作用域链:
var a = 20;
function test() {
var b = a + 10;
function innerTest() {
var c = 10;
return b + c;
}
return innerTest();
}
test();
解读示例:上述代码中,先创建全局执行上下文,然后test()
函数执行上下文,以及innerTest()
函数执行上下文,假设他们的变量对象分别是VO(global)
,VO(test)
,VO(innerTest)
,而innerTest的作用域链,就包含了这三个变量对象,所以innerTest的执行上下文可以这样表示
innerTestEC = {
VO: {...}, // 变量对象
scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
}
上述代码,以一个数组来模拟作用域链,数组的第一项scopeChain[0]
为作用域的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象.
不要误解为当前作用域与上层作用域为包含关系,并不是的.以最前端为起点,最末端为终点的单方向通到,更能贴切的形容.
所以呢,作用域链本质是一个指向变量对象的指针列表,它只引用,但不包含实际对象
总结一下:
通俗点说就是:作用域链的作用是保证执行上下文有权访问的变量和函数是有序的.作用域链的指针只能向上寻找访问,指针访问到window对象时,就会终止
参考链接