js基础知识零基础理解:执行上下文

1,385 阅读11分钟

前言

JavaScript中有很多晦涩难懂的概念,作为小菜鸡真的是百思不得其解,为啥别人都能说出个所以然,我却啥也不知道?

执行上下文(EC)

百度一下,我就知道,感觉 貌似 懂了,大家感兴趣也可以看下。

1、执行上下文是指函数调用时在执行栈中产生的当前函数(或全局对象window)的执行环境,该环境如隔绝外部世界的容器边界,保管可访问的变量、this对象等。

2、“执行上下文”就是当前 JS 代码被解析和执行时所处环境的抽象概念,JS 中任何代码都是在“执行上下文”中运行的。

3、 当函数在执行的前一刻,会创建一个称为执行期上下文的内部对象,一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕时,执行上下文就会被销毁。

  1. 执行栈是啥玩意?
  2. js解析和执行环境什么鬼,js代码在上下文运行?
  3. 函数执行内部对象是?晕了晕了要被吐槽了,扫盲这样扫?

这里先说下 EC 这个单词缩写,execution content 翻译意思是 执行内容,从字面意思看就是代码的执行内容,

  • 相信大家马上就想到了既然是执行内容,那么在哪里执行呢?

来,我们来说说 执行环境栈 ECS, 什么是ECS? 再翻译一下 ## Execution Context Stack 翻译就是 执行内容栈堆

执行环境栈(ECS)

在此之前,我们来看一张作用域的图

image.png

  • 泡泡1是全局作用域,有标识符foo;
  • 泡泡2是作用域foo,有标识符a,bar,b;
  • 泡泡3是作用域bar,仅有标识符c。

JavaScript 属于解释型语言,JavaScript 的执行分为:解释执行两个阶段,这两个阶段所做的事并不一样:解释阶段:词法分析,语法分析,作用域规则确定。执行阶段:创建执行上下文,执行函数代码,垃圾回收。

看到此处,杰哥突然悟了~!

执行上下文EC(execution contnet) 就是执行环境,执行上下文环境是在对应的作用域中的,和上图的作用域范围是一致的。 总结就是

  • 作用域: 静态的, 编码时就确定了(不是在运行时), 一旦确定就不会变化了
  • 执行上下文: 动态的, 执行代码时动态创建, 当执行结束消失
  • 联系: 执行上下文环境是在对应的作用域中的

我们继续说执行栈,这个图估计很多文章都有,就盗了吧。

image.png

全局上下文总是在最下面,最上面的是当前执行环境内容就是当前上下文。

学文章肯定要会偷懒的,学习哈也是要吃苦的,接下来我们来吃栈进栈出的苦~!

当浏览器首次载入你的脚本,它将默认进入全局执行上下文。如果,你在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并创建一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。

如果你调用当前函数内部的其他函数,相同的事情会在此上演。代码的执行流程进入内部函数,创建一个新的执行上下文并把它压入执行栈的顶部。浏览器总会执行位于栈顶的执行上下文,一旦当前上下文函数执行结束,它将被从栈顶弹出,并将上下文控制权交给当前的栈。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。

这时候你肯定吐槽了,不就是栈进栈出当前的上下文么,懂了懂了,(懂毛线还不是说不出来)我们继续看

(function foo(i) {
    if (i === 3) {
        return;
    }
    else {
        foo(++i);
    }
}(0));

16968d0a5ad12012_tplv-t2oaga2asx-zoom-in-crop-mark_4536_0_0_0.webp

首先是global 然后是一个立即执行函数把i这个形参赋值一个0 执行foo(0),执行之后就栈出销毁了。 接着就是立即执行函数入参i 赋值 1 2 3 。图片也说了是 Executing now , 不是都推入栈后执行3 2 2 1 哈。 妈的盗图有点坑。

再看个例子吧(也是网上抄袭的)

function foo(i) {
    if (i < 0) return;
    console.log('begin:' + i);
    foo(i - 1);
    console.log('end:' + i);
}
foo(2);

// 输出:

// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2

image.png

从上到下,从右边到左边看。 标准的栈进栈出。

关于执行上下文是啥,大家应该懂是啥了,es啊,就是执行内容啊。

执行上下文基础认知就到这里了结束了。


关于执行上下文的扩展内容 (知其所以还要知其所以然)

1.执行上下文的概念

VO(变量对象)/AO(活动对象)

这里为什么要用一个/呢?按照字面理解,AO其实就是被激活的VO,两个其实是一个东西。下面引用知乎上的一段话,帮助理解一下。原文链接

变量对象(Variable object): 是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字对象属性的值对应它们的值但这个对象是规范上或者说是引擎实现上的不可在JS环境中访问到活动对象。

激活对象(Activation object): 有了变量对象存每个上下文中的东西,但是它什么时候能被访问到呢?就是每进入一个执行上下文时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了。

这里VO 和 AO 其实是一个东西 因为ES3就提过了,所以ES5没有解释是啥,所以我们这些ES5开始看的人哪里会知道是啥?反正就是一个对象,是存放东西的,然后只有在激活的时候才能访问,先记住这些。

image.png

2.执行期上下文的生命周期

一个执行期上下文的生命周期可以分为两个阶段:

  • 创建阶段:在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,确定this指向。

  • 执行阶段:当开始执行代码的时候,会在这个阶段完成变量的赋值,函数的引用,以及执行其他的代码。

    执行期上下文的生命周期.png

image.png

详细了解执行上下文极为重要,因为其中涉及到了变量对象,作用域链,this等极为重要的概念,它关系到我们能不能真正理解JavaScript,下面我们分别了解几个概念。

(一)变量对象

1.变量对象的创建过程

(1)建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
(2)检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
(3)检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined,如果变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

变量对象的创建过程.png

接下来一起来看一个案例:

**

function bar(){
console.log('function')
}
var bar='why';
console.log(bar);//why
//在这个案例中,当变量bar遇到函数声明的bar时会直接跳过变量bar,不会将函数bar覆盖
//但是我们看到最后的输出结果为变量bar的值
//这是因为上面的三条规则只适用于变量对象的创建过程,而bar的值是在代码执行的时候被修改的

再来看另一个案例:

**

console.log(foo);//[Function: foo] node执行结果
function foo(){
  console.log('function');
}
var foo='foo';
console.log(foo);//foo
//本例的执行顺序为:
//首先将所有的函数声明放入变量对象中
//其次将所有的变量声明放入变量对象中
//但是因为foo已经存在同名函数,所以会跳过foo的赋值
//在代码执行的阶段将foo的值赋值给变量foo,所以最后的打印结果为foo
2.变量对象与活动对象

处于函数调用栈栈顶的执行上下文中的变量对象,即为活动对象。
来看另外一个案例:

**

function test(){
  console.log(a);//undefined
  console.log(foo());//2
  var a=1;
  function foo(){
    return 2
  }
}
test();

↑以上代码中,全局作用域中运行test()时,test()的执行上下文开始创建。我们用如下的形式来表示:

**

//创建过程
testEC={
  //VO为Variables Object的缩写,即变量对象
  VO:{
    arguments:{...},
    foo:0xa00,//指向foo函数的内存地址
    a:undefined,
    this:Window
  },
 scopeChain:{}
}

未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行的操作。

**

// 执行阶段
VO->AO//Active Object
AO={
  arguments:{...},
  foo:0xa00,
  a:1,
  this:Window
}

因此,上面例子的执行顺序如下:

**

function test(){
  function foo(){
    return 2
  }
  var a;
  console.log(a);//undefined
  console.log(foo());//2
  a=1
}
test();
3.全局上下文的变量对象

全局上下文就是最外层的上下文,在浏览器环境中,全局上下文指window对象,因此在浏览器环境中,通过在全局使用var定义的变量和'function'声明的函数都会成为window对象的属性和方法,上下文在其所有的代码执行完毕后会被销毁,包括定义在它上面的变量和函数等(对于浏览器来说,关闭网页或者退出浏览器都会销毁全局上下文)

3.执行上下文栈

执行上下文可以理解为当前代码的执行环境,javascript中的运行环境大概包括三种情况:

  • 全局环境:javascript代码运行起来会首先进入该环境。
  • 函数环境:当函数被调用执行时,会进入当前函数中执行代码。
  • eval
    在代码开始执行的时候,首先会产生一个全局执行上下文环境,调用函数时,会产生函数执行上下文环境,函数调用完成后,它的执行上下文环境以及其中的数据都会被销毁,重新回到全局执行环境,网页关闭后全局执行环境也会被销毁,其实这是压栈出栈的过程,全局执行上下文环境永远在栈底,而当前正在执行的函数上下文在栈顶

**

var a=1;//1.进入全局执行上下文环境
function outter(){
  var b=2;
  function inner(){
    var c=3;
    console.log(a+b+c);
  }
  inner();//3.进入inner函数上下文环境
}
outter();//2.进入outter函数上下文环境

↑以上代码的执行会经历以下过程:
1.当代码开始执行时就创建全局执行上下文环境,全局上下文入栈
2.全局上下文入栈后,其中的代码开始执行,进行赋值,函数调用等操作,执行到outter()时,激活函数outter,函数outter创建自己的执行上下文环境,outter函数上下文入栈。
3.outter函数上下文入栈后,其中的代码开始执行,进行赋值,函数调用等操作,执行到inner()函数时,激活函数inner创建自己的执行上下文环境,inner函数上下文入栈。
4.inner函数上下文入栈后,其中的代码开始执行,进行赋值,函数调用,打印等操作,由于里面没有可以生成其他上下文的需求,所有代码执行完毕后,inner函数上下文出栈。
5.inner函数上下文出栈后,又回到outter函数执行上下文栈,接着执行outter函数中后面剩下的代码,由于后面没有可以生成其它执行上下文的需求,所有代码执行完毕后,outter函数上下文出栈。
6.outter函数上下文出栈后,又回到了全局执行上下文环境,直到浏览器窗口关闭,全局上下文出栈。

image.png

最后我们可以得到一些结论:

  • 全局上下文在代码开始执行时就创建,只有唯一的一个,永远在栈底,浏览器窗口关闭时出栈

  • 函数被调用的时候创建自己的上下文环境

  • 只有栈顶的上下文处于活动状态,执行其中的代码