当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。
如果你刚看《高程》,看到这句话估计会似是而非的理解。其实我当时也是,直到最近撸第二遍,也是偶然看到大叔的系列文章,有心研究了下,加上自己的理解,促成了这几篇博文。今天我们先来聊聊执行上下文与活动对象。
执行上下文
执行上下文可能不太容易理解,其实它就是上面写到的执行环境,代码就是在相对应的执行环境中运行的。所以我们可以这样理解,每当调用一个函数,就会相应地为这个函数划出一块地,该函数所有的代码活动都被限制在这块地上。
好,这里我们可以引出两个问题:这块地是什么?只有函数调用才能创建EC吗?
这块地是什么?
当然就是内存了。无论是js还是其他代码,代码的运行所需空间都是由内存来分配的。
内存分堆(heap)和栈(stack),在这里,执行上下文组成了 执行上下文栈 来调控代码的进行,执行上下文栈的顶端就是正在使用的上下文。我们来用图示看一下:
|
|
当程序结束时,全局上下文出栈并销毁。
相信大家也注意到了上面出现了很多次的 全局上下文 ,它可以很好的回答我们的第二个问题:不是! 那么,问题就来了。。。
什么代码可以创建EC?
全局代码
函数代码
Eval代码
全局代码
全局代码就是 <script>
标签中的,或者是通过外部加载的js文件中的代码,它不包含任何函数体内的代码。
全局代码创建的EC就是我们上文中所提的全局上下文,它是程序刚开始,代码尚未执行时就产生的上下文,它永远处在执行上下文栈的底部。
函数代码
function
函数中的代码段就是函数代码,它也不包括内部函数代码。
函数如果递归调用自身,每调用一次都会创建一个新的执行上下文。如果函数体内有 return
,执行到它的时候会立刻退出当前执行上下文。
Eval代码
Eval代码和函数代码类似,执行到它的时候也会创建一个新的执行上下文。但不同的是,它有一个调用上下文(calling context),同样会压入ECStack。eval函数中如果进行变量、函数声明,将会体现到调用上下文中。
不过eval会使阅读性、安全性、性能下降,不建议大家使用。
变量对象
代码执行的时候访问的变量都是从变量对象中取到的。
变量对象(Variable Object)其实是执行上下文的一个属性,执行上下文还有 this
和 作用域
属性。它们介绍起来篇幅很大,所以我把它放到了后面的文章。
变量对象分为两类:
1.全局上下文变量对象
2.函数上下文变量对象
全局上下文中的变量对象
全局上下文变量对象就是全局对象。
全局对象本身包含很多属性如Math、String、Date、parseInt等等,大家不妨用 console.log(this)
在控制台中查看一下,还有,在全局作用域中声明的变量和函数也会成为它的属性。
当访问全局对象的属性时通常会忽略掉前缀,这是因为全局对象是不能通过名称直接访问的。不过我们依然可以通过全局上下文的this来访问全局对象,同样也可以递归引用自身。例如,DOM中的window。
函数上下文中的变量对象
函数上下文中存在的是活动对象,它可以当做变量对象使用,其中包含变量、函数声明、形参、arguments。
我们来配合一个例子看一下:
|
|
对应的变量对象是:
|
|
我们想一个问题,变量对象是初始化时就是这样吗?其实不是这样,执行上下文中的代码分两个阶段,不同阶段值不同,这也是js的很重要的一个特性。
执行上下文的两个阶段
我们透过这段代码在不同时期的变量对象看一下。
|
|
进入执行上下文
这个阶段处于代码执行之前,此时执行上下文的VO已经包含函数声明、形参、变量、arguments。其实这个阶段就是声明提升的阶段。
此时AO对象数据为:
类型 | 赋值与否 |
---|---|
变量 | undefined |
函数声明 | 对函数的引用 |
形参 | 赋值 |
arguments | 赋值 |
建议大家配合 Javascript深入浅出之声明与提升 中的 提升规则 一起看看。
对应变量对象:
|
|
代码执行
此时AO对象数据为:
类型 | 赋值与否 |
---|---|
变量 | 赋值 |
函数声明 | 对函数的引用 |
形参 | 赋值 |
arguments | 赋值 |
在这个阶段中,AO被改成了
|
|
到了这里,相信大家都对函数调用阶段有个很多本质的理解,配合本文也可以更好地理解声明提升,也为后面理解闭包打好基础。