全面解析JavaScript中的执行上下文
想要了解作用域,就必须先了解执行上下文。
变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。
全局上下文是最外层的上下文。在浏览器中,全局上下文就是 window 对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁(全局上下文在应用程序退出前才会被销毁,如关闭网页或退出浏览器)。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments。(全局上下文和箭头函数都没有这个变量)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。
代码执行时的标识符(var color,color 就是标识符)解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符,搜索停止,变量确定。(如果在全局上下文都没找到标识符,那通常就会报错)
看一看下面这个例子:
1 | var color = 'bule'; |
对这个例子而言,函数 changeColor() 的作用域链包含两个对象:一个是它自己的变量对象(就是定义 arguments 对象的那个),另一个是全局上下文的变量对象。这个函数内部之所以能够访问变量 color ,就是因为可以在作用域链中找到它。
此外,局部作用域中定义的变量可用于在局部上下文中替换全局变量。
看一看下面的例子:
1 | var color = 'bule'; |
以上代码涉及3个上下文:全局上下文、changeColor() 的局部上下文和 swapColors() 的局部上下文。全局上下文中有一个变量 color 和一个函数 changeColor() 。changeColor() 的局部上下文有一个变量 anotherColor 和一个函数 swapColors() ,但在这里可以访问全局上下文中的变量 color 。swapColors() 的局部上下文中有一个变量 tempColor ,只能在这个上下文访问到。全局上下文和 changeColor()
局部上下文都无法访问到 tempColor 。而在 swapColors() 中则可以访问另外两个上下文中的变量,因为它们都是父上下文。
1 | window |
上图的树形结构表示不同的上下文。内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。上下文之间的连接是线性的、有序的。每个上下文都可以到上一级上下文中区搜索变量和函数,但任何上下文都不能到下一级上下文中区搜索。swapColors() 局部上下文的作用域链中有 3 个对象:swapColors()的变量对象、changeColor() 的变量对象和全局变量对象。swapColors() 的局部上下文首先从自己的变量对象开始搜索变量和函数,搜不到就搜索上一级变量对象。changeColor() 上下文的作用域链中只有 2 个对象:它自己的变量对象和全局上下文对象。因此,它不能访问 swapColors() 的上下文。
注意:函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则。