Bootstrap

深入理解javascript的执行上下文

1、内存空间

内存空间大家都比较熟,即栈与堆。
JS的基础数据类型有Undefined、Null、Boolean、Number、String,这些都是按值访问,存放在栈内存。
其他的Object为引用类型,如数组Array、function或其他的自定义对象,这些存放在堆内存,其对应的地址引用(指针)放在栈内存。
大家对这些应该比较熟,就不赘述了。

2、JS代码的执行环境(执行上下文,Execution Context,下面简称EC)

JS是单线程的,运行在全局EC,每进入一个function,就做一次入栈操作,向栈顶压入一个属于该function的新的EC。若function中又调用了另一个function,则再执行一次入栈…依次执行完再依次出栈,回到全局EC。全局EC一定是在栈底,在浏览器关闭后出栈。
在这里插入图片描述

EC的构成如下图:
在这里插入图片描述

变量对象VO(Variable Object)保存此EC中涉及到的变量。
作用域链保存着此EC中的VO与其他EC中的VO的关联关系(能否访问到)。
然后是this,在EC被创建时,会确定this的指向。
EC的创建过程可以用以下代码表示:

testEC = {
	VO: {},
	scopeChain: {},
	this: {}
}

3、执行上下文具体分析

先看一段代码:
在这里插入图片描述

第一句报错,a未定义,很正常。第二句、第三句输出都是undefined,说明浏览器在执行console.log(a)时,已经知道了a是undefined,但却不知道a是10(第三句中)。
在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明,而不是赋值。变量赋值是在赋值语句执行的时候进行的。可用下图模拟:
在这里插入图片描述

以上是第一种情况。

下面还有第二种。

先来个简单的。
在这里插入图片描述

有js开发经验的朋友应该都知道,你无论在哪个位置获取this,都是有值的。至于this的取值情况,比较复杂,详情请分析下一篇文章。
与第一种情况不同的是:第一种情况只是对变量进行声明(并没有赋值),而此种情况直接给this赋值。这也是“准备工作”情况要做的事情之一。

下面还有第三种情况。

在第三种情况中,需要注意代码注释中的两个名词——“函数表达式”和“函数声明”。虽然两者都很常用,但是这两者在“准备工作”时,却是两种待遇。
在这里插入图片描述

看以上代码。“函数声明”时我们看到了第二种情况的影子,而“函数表达式”时我们看到了第一种情况的影子。
没错。在“准备工作”中,对待函数表达式就像对待“ var a = 10 ”这样的变量一样,只是声明。而对待函数声明时,却把函数整个赋值了。

好了,“准备工作”介绍完毕。
我们总结一下,在“准备工作”中完成了哪些工作:
变量、函数表达式——变量声明,默认赋值为undefined;
this——赋值;
函数声明——赋值;
这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。


这里插一句题外话:通过以上三种情况,你可能会联想到网上的有些考js语法的题目/面试题。的确,几乎每个js语法题中都有这种题目出现。之前你遇到这种题目是不是靠背诵来解决?背过了,隔几天又忘记了。——任何问题,都要去追根溯源,要知道这个问题是真正出自哪一块知识点,要真正去理解。光靠背诵是没用的。


细心的朋友可能会发现,我们上面所有的例子都是在全局环境下执行的。
其实,javascript在执行一个代码段之前,都会进行这些“准备工作”来生成执行上下文。这个“代码段”其实分三种情况——全局代码,函数体,eval代码。

这里解释一下为什么代码段分为这三种。
所谓“代码段”就是一段文本形式的代码。
首先,全局代码是一种,这个应该没有非议,本来就是手写文本到<script>在这里插入代码片标签里面

<script type="text/javascript>
	//代码段
</script>

其次,eval代码接收的也是一段文本形式的代码。

eval("alert(123)");

最后,函数体是代码段是因为函数在创建时,本质上是 new Function(…) 得来的,其中需要传入一个文本形式的参数作为函数体。
在这里插入图片描述


最后,eval不常用,也不推荐大家用。

如果在函数中,除了以上数据之外,还会有其他数据。先看以下代码:

function fn(x){
  console.log(arguments);         //[10]
  console.log(x);                 //10
}
fn(10);

以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

另外一点不同在于,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。用一个例子说明一下:

var a =10;
function fn(){
  console.log(a);         //a是自由变量   函数创建的时候,就确定了a要取值的作用域
}

function bar(f){
  var a =20;
  f();        //打印“10”,而不是“20”
}
bar(fn);

好了,总结完了函数的附加内容,我们就此要全面总结一下上下文环境的数据内容。
全局代码的上下文环境数据内容为:

普通变量(包括函数表达式),如: var a = 10;声明(默认赋值为undefined)
函数声明,如: function fn() { }赋值
this赋值

如果代码段是函数体,那么在此基础上需要附加:

参数赋值
arguments赋值
自由变量的取值作用域赋值

给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。

讲完了上下文环境,又来了新的问题——在执行js代码时,会有数不清的函数调用次数,会产生许多个上下文环境。这么多上下文环境该如何管理,以及如何销毁而释放内存呢?下一节将通过“执行上下文栈”来解释这个问题。

4、变量对象VO

function test1(arg){
    var a = 1;
    var b = {name: 'afei'};
    function c(){
        console.log(a);
    }
}

上面test1执行时创建的变量对象如下:

VO = {
    arguments: {...},
    a: 1,
    b: <b reference>,
    c: <c reference>
}
;