JavaScript 执行环境及作用域

在事件循环的文章中,提到JavaScript的执行是在栈中。(https://www.cnblogs.com/wangtingnoblog/p/js_EventLoop.html)

栈是先进后出的数据结构,操作是在栈顶完成的。(注意,以下代码不考虑声明使用var,即不考虑声明提升的情况)

 (左边的就是栈模型)

原本我的理解是,栈中放着的是帧,帧就是函数。

一、一个栗子

比如

test.js

 1 function a() {
 2   // 操作a1
 3 }
 4 function b() {
 5   // 操作b1 
 6   a();
 7   // 操作b2
 8 }
 9 function c() {
10   // 操作c1 
11   b();
12   // 操作c2
13 }
14 c();

 

按照以前的理解:

  • 在代码的执行指针指到第14行时,c函数被推入栈中,
  • 然后执行指针指到c函数的第一行,进行操作c1,这时栈中就有1个帧
  • 当执行指针指到c函数的第二行时,调用了b函数,这时把b函数推入栈中,
  • 根据先进后出的原则,c函数暂时交出执行权,执行指针指到b函数的第一行,进行操作b1,此时栈中有2个帧,b函数的帧在上面。
  • 当执行指针指到b函数的第二行时,调用了a函数,这时把a函数推入栈中,
  • 根据先进后出的原则,b函数暂时交出执行权,执行指针指到a函数的第一行,进行操作a1,此时栈中有3个帧,a函数的帧在上面。
  • 待操作a1完成后,系统判定a函数执行完成,就把a函数的帧推出栈,执行权返回调用a函数的地方(即b函数的第2行)
  • 执行权返回后,执行指针继续往下,指到b函数的第三行,进行操作b2,
  • 待操作b2完成后,系统判定b函数执行完成,就把b函数的帧推出栈,执行权返回调用b函数的地方(即c函数的第2行)
  • 执行权返回后,执行指针继续往下,指到c函数的第三行,进行操作c2,
  • 待操作c2完成后,系统判定c函数执行完成,就把c函数的帧推出栈,执行权返回调用c函数的地方
  • 此时栈已经被清空了

其实我认为上面的执行流程是没有问题的,但是还是有几点疑问:

  1. 在调用c函数之前,a函数,b函数,c函数的定义是放在哪里的?
  2. 函数中声明的变量是放在哪里的?难道执行完变量声明就把变量抛弃了?
  3. 栈中存放的帧就是函数本身吗?

开始时我没有在乎这些问题,直到想到下面的例子:

 1 let num = 1;
 2 function a() {
 3     let num = 2;
 4     let b = function() {
 5         console.log(num)
 6     }
 7     num = num + 1;
 8     return b
 9 }
10 let b = a();
11 b();

 

动态声明函数

下面我们来考虑上面代码的执行栈的推入推出行为:

按照原来的理解:

  • 执行指针指到第10行时,a函数被推入栈中,a函数执行完成后,返回b函数,这时栈清空
  • 执行指针返回调用a的地方,即第10行,指针下移,调用b函数,b函数进入栈中,这时栈中只有一个帧,就是b函数,
  • 此时运行到console.log(num)时,num是从哪里来的呢?这是我最大的疑问。a函数的帧已经被推出了,在执行b函数时遇到变量难道不是在栈中逐级往下找吗?
  • 当然,往下找时就涉及到我刚才的疑问,(函数中声明的变量是放在哪里的?难道执行完变量声明就把变量抛弃了?)

当然,现实是跑出来结果了,没有报err,console里出来的数据是3。这个问题先放在这里,下面先来说一下执行环境和作用域链

二、执行环境和作用域链

今天我复习了以前的执行环境和作用域的知识

执行环境定义了变量或者函数有权访问的其他数据,每个执行环境对应着一个变量对象,执行环境中定义的所有变量函数都保存在这个变量对象中。

每个函数都有自己的执行环境。当执行流进入已经函数时,函数的环境就会被推入一个环境栈中。而在函数执行完成之后,栈将其环境弹出,把控制器返回给之前的执行环境

上面出自:《JavaScript高级程序设计》

有几个问题需要搞清楚:

  1. 执行环境到底是什么?它是函数执行时的场所,包含着函数的代码和这个函数有权访问的其他数据(全局变量等)。
  2. 变量对象保存着函数中定义的所有变量和内部函数(回答了(函数中声明的变量是放在哪里的?难道执行完变量声明就把变量抛弃了?)这个问题),也就是说,在执行函数时遇到变量声明或者函数说明,会把他们放到变量对象中
  3. 上面的疑问(栈中存放的帧就是函数本身吗?)其实是这样的,栈中存放的帧是执行环境,执行环境被推入栈中后,系统会执行环境中的代码。

我们来总结一下,函数被调用时,函数的执行环境会被推入栈中,在函数执行过程中,遇到变量声明和函数声明会被保存到变量对象中。

 

还有一个概念,作用域链

当代码在一个执行环境中执行时(此时执行环境在栈中),会创建变量对象作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的概念好理解,而且主要是保证有序访问访问。我想知道的是,作用域链想保证的有序访问的数据是从哪里来的,

我们知道,执行环境保存着有权访问的所有变量和函数,

其实我认为,执行环境保存着的是有权访问的所有变量和函数的引用。而且,执行环境保存数据的引用的时机是在函数定义的时候。

下面来回顾一下刚才的例子:

 1 let num = 1;
 2 function a() {
 3     let num = 2;
 4     let b = function() {
 5         console.log(num)
 6     }
 7     num = num + 1;
 8     return b
 9 }
10 let b = a();
11 b();
View Code

 

在声明函数b时,b函数的执行环境就保存了a函数的变量num的引用最外层的变量num的引用

在执行b函数时,用到了变量num,这时系统就去作用域链上找,根据规则,它会找到a函数的变量num的引用,这时num的值已经变成了3,使用console了3.

我们再看另外的例子:

let color = 'red';
function a() {
    console.log(color)
}
color = 'black'
a(); // black

 

定义a函数时,执行环境(我认为执行环境的定义时机是在该函数被定义的时候)也同时被定义了,这个函数有权访问的数据的引用也保存在执行环境中,此时保存的是外层环境的变量color,

等到a函数执行时,遇到变量color,根据作用域链的规则,系统找到变量color的引用,这时它的值是black。

我们再来看一下,如果修改为下面的代码呢

1 let color = 'red';
2 function a() {
3     console.log(color)
4 }
5 setTimeout(() => { color = 'black'}, 10)
6 
7 a(); // red

 

这时,在调用a函数时,引用指向的值还没有经过改变,所以console了red

如果指向的值本身就是对象呢

 1 let name = {
 2     first: 'w',
 3     second: 't'
 4 };
 5 function a() {
 6     console.log(name.first)
 7 }
 8 setTimeout(() => { name.first = 'q'}, 10)
 9 //name.first = 'q'
10 a(); // w
View Code

 

结果和基本数据类型是一致的。

 

三、总结

经过一些不靠谱的观察得到一些不靠谱的结论,各位同学只当是看个热闹吧。

  1. 执行环境在函数定义时被创建
  2. 创建执行环境时会同时把函数能够访问到的数据的引用放在执行环境中。
  3. 函数被调用时,执行环境被推入栈中,此时会创建作用域链(为什么是在执行时创建作用域链而不是在创建执行环境时创建作用域链、我认为是效率考虑,执行环境保证函数能够访问到的数据,它必须在定义时就创建,而作用域链保证有序访问,所以作用域链没有必要在定义时创建,可以在执行时根据执行环境中保存的某个对象进行创建)
  4. 函数在执行中,遇到变量会根据作用域链找到对应的保存在执行环境中的变量的引用,然后根据引用来取得值。
  5. 函数在栈中执行时,其实和函数所在的帧的下面的那些帧没有直接的关系,函数执行时不会从下面的帧中取得数据,我觉得这样可以保证执行环境的独立。
  6. 执行环境保存的其他数据的引用指向的是堆内存(系统判定其他函数可能用到这个变量时就会把它保存在堆内存中)

四、疑问

  其实还是有很多地方不理解,就连上面的总结也是我根据现象理解处理的,不能保证它的正确性。

  疑问还有很多:

  • 执行环境什么时候创建?保存在哪里?什么时候消毁?会被改变吗?
  • 执行环境中保存着变量的引用,应该也标记了函数访问变量的顺序,其实这个和作用域链类型,只不过在函数执行时,才会创建作用域链,作用域链应该是根据标记了函数访问变量顺序的某个对象创建的。
  • 其他,一时想不起来了

五、最后

我本身不是计算机专业出身,对一些程序运行原理性的东西不是很理解,之后会慢慢把这方面补起来(虽然我还不知道怎么去弥补这方面的知识)。

 这篇文章其实是我要拿来向一个前辈请教的,这位前辈是我学习的榜样,我要向他好好学习。

 

参考:《JavaScript高级程序设计》

作成: 2019-02-16 01:39:02

修改: 

  1. 2019-02-16 01:41:27  修改文字错误
  2. 2019-02-16 11:06:14  添加执行环境中保存的数据的引用指向堆内存
posted on 2019-02-16 01:40  西门本不吹雪  阅读(201)  评论(0编辑  收藏  举报