关于作用域与执行上下文,总是看,总是忘。忘了看,看了忘。所以决定,梳理记录下自己的理解,方便回忆。
无知版作用域
js有三种作用域,全局作用域、函数作用域、块作用域。 作用域在函数创建的时候就已经确定,而不是在执行的时候。
全局作用域
全局变量暴露在全局作用域,绑定在window上。所有在script中直接用var声明的,或者未用var声明,直接赋值的变量均为全局变量,可通过window.xxx访问,如下:
<script>
var a = 1;
b = 2 ;
function foo() {
c = 3
}
</script>
a、b、c均为全局变量,属于全局作用域,在代码的任何地方都可以访问
函数作用域
在函数内部声明的变量为局部变量,调用函数时创建函数作用域,函数执行完毕,函数作用域销毁。
function foo {
let a = 1
const b = 2
var c = 3
}
a、b、c为局部变量,只在foo(函数作用域)中有效,在全局作用域无法访问
块作用域
用let、const声明的变量在块作用域中有效,块作用域为大括号包裹的代码段
function foo {
let a = true
if(a) {
let b = 1
} else {
const c = 2
}
}
a为局部变量,在foo中任何地方都可以访问。b属于if块作用域,c属于else块作用域,都只能在本块作用域访问。
作用域链
如果变量在当前作用域中找不到,则会去父级作用域(函数定义时所在作用域)查找,一直到全局作用域还找不到,则该变量为undefined。这种一层一层,用链式查找决定哪些数据能被内部函数访问的机制称为作用域链。
精进版作用域
js执行的时候不是一行一行执行,而是一段一段执行。这里的段是由可执行代码的类型划分的,分为全局代码、函数代码、eval代码(这里暂且不考虑eval代码)。而js每执行一段代码,都会创建对应的执行上下文。
执行上下文有三个属性,变量对象、作用域链和this。变量对象存储上下文中定义的变量和函数声明。作用域链是由多个变量对象构成的链表。this此处不讨论。
函数有个内部属性叫[[scope]],在函数创建的时候,会保存所有父变量对象。在函数执行的时候,会创建函数的执行上下文。
全局作用域(变量对象)
执行全局代码时,会创建全局上下文,全局执行上下文的变量对象即为全局变量
函数作用域(变量对象)
执行函数代码时,会创建函数上下文。函数上下文的创建分两个阶段。
第一阶段:进入执行上下文
函数上下文的变量对象为函数形参、函数声明、变量声明。若是函数声明与函数形参重名,则覆盖;若是变量声明与函数形参重名,则不干扰。注意,在此阶段函数内部的变量处于声明阶段,未初始化。
函数上下文的作用域链初始化为函数的内部属性[[scope]],即父变量对象。
第二阶段:执行代码
将当前变量对象添加到作用域链的前端,在执行过程中修改变量对象的变量值
作用域链
由上可知,我们平时所说的作用域实际为执行上下文的作用域链属性。函数创建时在内部属性scope上关联父变量对象;函数执行时,使用scope初始化执行上下文的作用域链属性并将自身变量对象添加至作用域链。由此,形成了函数的完整作用域链。
执行上下文栈
代码执行的时候,会创建多个执行上下文,那么这么多的执行上下文是怎么管理的呢。js引擎使用执行上下文栈存储执行上下文,首先进栈的是全局上下文,之后每执行一个函数,就将该函数的执行上下文压入栈中。函数执行完毕,函数上下文出栈。
参考文章:
JavaScript深入之词法作用域和动态作用域
JavaScript深入之执行上下文栈
JavaScript深入之变量对象
JavaScript深入之作用域链