什么是作用域?
作用域是指程序源代码中存储变量的区域。
程序需要一套设计良好的规则来存储变量,并且以后可以方便的找到这些变量,这套规则被称为作用域。
JS是词法作用域
作用域的规则主要有两种:词法作用域、动态作用域,js采用的是词法作用域,也就是说变量的查找规则是书写代码时就已经确定的。动态作用域是变量查找规则由函数执行时的位置决定的。
词法作用域是你找变量时,从函数定义(书写)的地方,向外层代码块找。动态作用域是,你从函数执行处,往外层代码块找。
JS引擎执行逻辑
JS引擎处理代码时,是一块一块的处理,而不是逐行处理。而且是分为两个阶段去处理,先编译、再执行。
编译:
在编译阶段,会为该代码块中的变量声明分配内存,并命名。例如在处理var a=2;这句代码时,编译阶段会在该作用域声明一个变量,并命名为a,但此时a的值为undefiend。
编译阶段会对函数形参、arguments对象、变量声明、函数声明进行分配内存的处理。变量声明不会覆盖之前的同名声明,但是函数声明会覆盖之前的同名声明。普通变量的初始值是undefiend,函数参数有传值时初始值为传值,无传值时为undefiend,函数声明的初始值是该函数(函数在此时被创建)。
执行:
例如处理var a=2;时,js引擎会在作用域中找到a,然后执行a = 2这条命令,给a赋值。
执行上下文
执行上下文,也称执行环境,实际上也是一个对象。执行上下文栈,用来管理执行上下文的栈(也称函数调用栈、执行环境栈)。
执行上下文分为三种:全局执行上下文、函数执行上下文、Eval执行上下文(在执行阶段,Eval可以将传入的字符串转换为代码去执行,而这个传入的字符串可以是一个变量)。
每个函数都有自己的执行环境,当程序调用一个函数时,该函数的执行环境被压入环境栈中,当函数执行完毕时,该函数的执行环境会出栈,并被销毁。栈底是全局执行环境。
执行上下文三剑客(三个重要属性)
函数被调用的时候,会创建对应的函数上下文,并推入执行上下文栈中。执行上下文创建时,会生成三个重要的属性:变量对象、作用域链、this指向。
变量对象:
函数中定义的变量、参数、函数、arguments对象都存储在这个变量中,初始值只包含arguments对象。
我们无法使用js代码访问变量对象,但js引擎可以在后台使用它。函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。其实变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已。
全局上下文中的变量对象就是全局对象,以浏览器环境来说,就是 window 对象。
注:官方说法是每一个执行环境都有一个与之关联的变量对象。那么如何关联?个人理解既然执行环境本身也是对象,关联无非是用属性存储一个对变量对象引用的指针。这才会有闭包情况下,执行环境被销毁,但关联的变量对象可能依旧被其他作用域链引用,依旧存在于内存中,不会被回收。
作用域链:
执行环境中存储着该环境的作用域链,作用域链本质上是一个指向变量对象的指针列表,它只引用但不包含实际变量对象。函数在被创建时,会根据词法作用域,将作用域链保存在内部的[[Scope]]属性中,当函数被调用时,执行环境入栈后,会将该函数的变量对象放到作用域链的最前面,形成最终的作用域链存储到执行环境中。
this指向:
函数只有在调用的时候,才知道内部的this到底指向的谁,因此this的指向实际是在执行上下文中保存着。具体内容在this指向的文章中有说明。
总结
执行环境的创建和入栈过程,实际就是我们上面所说的编译阶段,然后进入函数执行阶段,然后是执行环境出栈、销毁,回收相关内存。一般来讲函数执行完毕,执行环境出栈,该环境会被销毁,关联的变量也会被销毁。但是闭包情况下又有所不同,准确的说是函数的执行环境及作用域链被销毁,但变量对象仍存在于内存中(被其他作用域链引用)。