说到 Javascript 中的做用域,一般一同出现的还有一个执行上下文(execution context)的概念,之前我在网上搜索相关的内容老是理不清这二者的关系。彷佛函数,做用域,执行上下文这三者天生就是纠缠在一块儿的。为了得到一手的资料我翻看了 ES6 规范,把他们究竟是什么梳理了一下:javascript
首先咱们来讲下做用域,简单来讲做用域就是一个区域,包含了其中变量,常量,函数等等定义信息和赋值信息,以及这个区域内代码书写的结构信息。做用域能够嵌套。咱们一般知道 js 中函数的定义能够产生做用域,下面咱们用具体代码来示例下:java
全局做用域(global scope)里面定义了两个变量,一个函数。walk 函数生成的做用域里面定义了一个变量,两个函数。innerFunc 和 anotherInnerFunc 这两个函数生成的做用域里面分别定义了一个变量。在规范中做用域更官方的叫法是词法环境(Lexical Environments)。什么意思?就是做用域包含哪些内容取决于你代码怎么写,你把定义 go 变量写在了 walk 函数里面,那么 go 变量就属于 walk 函数做用域。函数
做用域其实由两部分组成:this
记录做用域内变量信息(咱们假设变量,常量,函数等统称为变量)和代码结构信息的东西,称之为 Environment Record。lua
一个引用 __outer__
,这个引用指向当前做用域的父做用域。拿上面代码为例。innerFunc 的函数做用域有一个引用指向 walk 函数做用域,walk 函数做用域有一个引用指向全局做用域。全局做用域的 __outer__
为 null。spa
规范中定义了查找一个变量的过程:先查看当前做用域里面的 Environment Record 是否有此变量的信息,若是找到了,则返回当前做用域内的这个变量。若是没有查找到,则顺着 __outer__
到父做用域里面的 Environment Record 查找,以此递归。因此咱们一般所说的函数内同名变量遮蔽全局变量就是这么回事。不过若是你在变量查找的时候指定某个做用域中的 Environment Record,那么也是能够的,譬如:window.name
【其实 window 对象就是全局做用域的 Environment Record 对象,可是普通函数做用域的 Environment Record 对象是获取不到的】。code
函数声明orm
function f() { var inner = 'inner'; console.log( inner ); } f(); // inner; console.log( inner ); // Uncaught ReferenceError: inner is not defined
catch 语句对象
try { throw new Error( 'customized error' ); } catch( err ) { var iamnoterror = 'not error'; console.log( iamnoterror ); // not error console.log( err ); // Error: customized error } console.log( iamnoterror ); // not error console.log( err ); // Uncaught ReferenceError: e is not defined
这里特别指出的是 catch 语句生成的做用域只会框住参数部分的变量(err),使其不能在外面访问。对于 catch 语句体里面声明的变量并不起做用。咱们看规范里面怎么说:递归
For each element argName of the BoundNames of CatchParameter, do
Perform catchEnv.CreateMutableBinding(argName).
catchEvn 就是 catch 语句生成的做用域,可是这个做用域只保存参数列表中的变量(CreateMutableBinding(argName))。
语句块
if ( true ) { let bv = 'bv'; const B_C = 'BC'; let blockFunc = function() {} function notBlockFunc() {} console.log( bv ); // bv console.log( B_C ); // BC console.log( notBlockFunc ); // function notBlockFunc() {} console.log( blockFunc ); // function () {} } console.log( bv ); // Uncaught ReferenceError: bv is not defined console.log( B_C ); // Uncaught ReferenceError: B_C is not defined console.log( notBlockFunc ); // function notBlockFunc() {} console.log( blockFunc ); // ReferenceError: blockFunc is not defined
语句块 {}
会生成一个新的做用域,可是这个做用域只绑定块级变量,常量等,即 let,const 声明的属于块级做用域,而 var 声明的仍是属于块级做用域的父做用域。
接下来咱们说下执行上下文(execution context),执行上下文是用于跟踪代码的运行状况,其特征以下:
一段代码块对应一个执行上下文,被封装成函数的代码被视做一段代码块,或者“全局做用域”也被视做一段代码块。
当程序运行,进入到某段代码块时,一个新的执行上下文被建立,并被放入一个 stack 中。当程序运行到这段代码块结尾后,对应的执行上下文被弹出 stack。
当程序在某段代码块中运行到某个点须要转到了另外一个代码块时(调用了另外一个函数),那么当前的可执行上下文的状态会被置为挂起,而后生成一个新的可执行上下文放入 stack 的顶部。
stack 最顶部的可执行上下文被称为 running execution context。当顶部的可执行上下文被弹出后,上一个挂起的可执行上下文继续执行。
咱们用代码来示例下(从 outer 调用到 level1 调用,再逐层返回):
执行上下文对象的内部属性:
[[code evaluation]]
:当前代码块执行的状态:prerform,suspend,resume。
[[Function]]
:若是当前执行上下文对应的是一个函数,那么这个属性就保存的这个函数对象。若是对应的是全局环境(能够是一个 script 或者 module),属性值是 null。
[[Real]]
:相似与沙箱的概念?(我尚未看懂,不过不太影响此篇的内容)。
若是程序执行到某个点抛出异常了,那么咱们能够用这个记录执行上下文的 stack 来追踪到底哪里出错了,能够看到整个调用栈,此时内部属性 [[Function]]
就起到做用了:
其实你们看下做用域和执行上下文各自的职责,你会发现他们几乎是没有啥交集的。那么为啥一般二者会被同时提到呢?由于在一个函数被执行时,建立的执行上下文对象除了保存了些代码执行的信息,还会把当前的做用域保存在执行上下文中。因此它们的关系只是存储关系。
结合做用域和执行上下文,咱们再来看下变量查找的过程。其实第一步不是到做用域里面找 Environment Record,而是先从当前的执行上下文中找保存的做用域(对象),而后再是经过做用域链向上查找变量。并且同一个执行上下文保存的做用域(对象)是可变的,当代码在同一个执行上下文中执行的时候,若是碰到有必要生成一个新做用域的时候,这个新的做用域会被添加到做用域链的头部,而后执行上下文就保存的做用域对象就更新成这个新的做用域。等这个新的做用域生命周期完成后,做用域链又会恢复到以前的情况,而后执行上下文保存的做用域也会恢复成以前的。示例:
稍微提下,我看到网上有把执行上下文等同于 this 的文章,其实 this 的值是经过当前执行上下文中保存的做用域(对象)来获取到的,规范以下。
ResolveThisBinding ( )
The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context. ResolveThisBinding performs the following steps:
Let envRec be GetThisEnvironment( ).
Return envRec.GetThisBinding().
我接下来会要总结函数做为普通函数和做为构造函数被调用时的区别,那个时候应该会对 this 有更深刻的解释。