作用域问题
什么是作用域
在计算机程序设计中,作用域(scope,或译作有效范围)是名字(name)与实体(entity)的绑定(binding)保持有效的那部分计算机程序。 – 维基百科 我理解就是:一个变量在某个范围内产生作用。
静态作用域和动态作用域
我们还必须明确两个概念,静态作用域(Static Scope)和动态作用域(Dynamic Scope)。
静态作用域又叫词法作用域,在词法作用域里的变量在定义的时候就决定了,它可以是一个函数或一段代码,该变量在这段代码区域内可见(visibility);在这段区域以外该变量不可见(或无法访问)。
动态作用域,在函数或者代码块执行的时候才被决定,在动态作用域内的变量存在于代码执行的这段时间,执行完毕后便被销毁。
我们用一段经典代码来解释它们(下面的代码虽然是 JavaScript 代码, 但它不仅仅代表 JavaScript!)
var a = 1;function foo() {console.log(a); // 输出什么呢?console.log(b); // 输出什么呢?}function bar() {var a = 2;var b = 3;foo();}bar();
在采用静态作用域的语言下执行以上代码会报错,因为 foo 中的 console.log(b) 语句中的变量 b 是不存在 foo 可访问的作用域内,如果去掉这一句打印的话会打印出 1 。
在采用动态作用域的语言下执行的话会输出 2 和 3。因为动态作用域里面的变量生命周期是在函数执行的时候,函数执行结束后这些变量就会被销毁。
其实大部分语言都采用静态作用域,比如 Java、C/C++、C#、Python、JavaScript …… 注意:JavaScript 中的 this 关键字类似于动态作用域,它是在函数被调用的时候才决定的。
执行上下文栈(Execution context stack)
可执行代码
有三种类型的ECMAScript的代码:全局代码,函数代码和eval代码。每一段代码都在其执行上下文中求值。只有一个全局上下文,可能有许多函数上下文和eval执行上下文的实例。函数的每次调用都进入函数执行上下文并计算函数代码类型。eval函数的每次调用都进入eval执行上下文并计算其代码。
执行上下文可以激活另一个上下文,例如函数调用另一个函数(或全局上下文调用全局函数),等等。从逻辑上讲,这是作为一个堆栈实现的,称为执行上下文栈。
激活另一个上下文的上下文称为调用者。被激活的上下文称为被调用者。同时,被调用方可能是其他一些被调用方的调用方(例如,从全局上下文中调用的函数,然后调用一些内部函数)。
当调用方激活(调用)被调用方时,调用方暂停其执行并将控制流传递给被调用方。被调用者被推入堆栈,并成为一个正在运行的(活动的)执行上下文。在被调用方的上下文结束后,它将控制权返回给调用方,并继续对调用方的上下文进行评估(然后可能激活其他上下文),直到其结束,依此类推。被调用方可以简单地返回或退出异常。抛出但未捕获异常可能退出(从堆栈中弹出)一个或多个上下文。
也就是说,所有ECMAScript程序运行时都显示为执行上下文(EC)堆栈,该堆栈的顶部是一个活动上下文:

当程序开始时,它进入全局执行上下文,这是堆栈的底部和第一个元素。然后全局代码提供一些初始化,创建所需的对象和函数。在全局上下文执行期间,它的代码可能会激活其他一些(已经创建的)函数,这些函数将进入它们的执行上下文,将新元素推入堆栈,等等。初始化完成后,运行时系统正在等待某个事件(例如用户的鼠标点击),该事件将激活某个函数并进入一个新的执行上下文。
在下一个图中,有一些函数上下文作为EC1,全局上下文作为Global EC,我们在从全局上下文中输入和退出EC1时进行了以下堆栈修改:

这正是 ECMAScript 的运行时系统管理代码执行的方式。当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
正如我们所说,堆栈中的每个执行上下文都可以表示为一个对象。让我们看看它的结构,以及执行代码需要上下文的哪种状态(哪些属性)。
执行上下文(Execution context)
执行上下文可以抽象地表示为简单对象,他定义了变量或者函数有权访问的其他数据,并决定了他们各自的行为。 每个执行上下文都具有跟踪其关联代码的执行进度所必需的一组属性(我们可以将其称为上下文状态)。 下图显示了上下文的结构:

除了这三个必需的属性(变量对象,this值和作用域链)之外,根据实现的不同,执行上下文可以具有任何其他状态。
可以大致认为执行环境有两种(其他的先不管):全局环境 和 函数环境,两种环境中都会关联一个 变量对象(variable object) ,而在函数环境中将以他的 活动对象(activation object) 作为变量对象。作用域链(scope chain) 是在代码执行到某个环境中的时候被创建,每个环境中都存在,在代码退出该环境的时候被销毁。执行环境是有层级的,作用域链就是沿着前端(内部)向顶端(外部)延伸的,直到全局环境。
一个好问题
let nAdd;let t = () => {let n = 99;nAdd = () => {n++;};let t2 = () => {console.log(n);};return t2;};let a1 = t();let a2 = t();nAdd();a1(); //99a2(); //100
首先:同一个函数形成的多个闭包的值都是相互独立的。
当执行 var a1 = t()的时候,变量 nAdd 被赋值为一个函数 ,这个函数是function (){n++},我们命名这个匿名函数为 fn1 吧。接着执行 var a = t()的时候,变量 nAdd 又被重写了,这个函数跟以前的函数长得一模一样,也是function (){n++},但是这已经是一个新的函数了,我们就命名为 fn2 吧。
所以当执行 nAdd 函数,我们执行的是其实是 fn2,而不是 fn1,我们更改的是 a2 形成的闭包里的 n 的值,并没有更改 a1 形成的闭包里的 n 的值。所以 a1() 的结果为 99 ,a2()的结果为 100。
接下来我们分三篇文章详细讨论一下上下文的这些重要属性,变量对象、作用域链、this。
注意,作用域链和this值都是在函数调用的时候才被确定的,而在全局函数中this的值为window。
参考
- MDN官方对变量提升的描述
- 关于变量提升
- 理解 JavaScript 中的执行上下文和执行栈
- 了解JavaScript的执行上下文
- 维基百科 - 作用域
- Facebook 的 ECMAScript 理论专家 Dmitry Soshnikov(最优秀的ECMAScript解读)
恶补JavaScript基础系列
恶补JavaScript基础系列目录地址:https://www.sixtyden.com/archive。
恶补JavaScript基础系列是我在从学校毕业入坑前端的学习产物,它主要是我看完书以及其他资料后的一个浓缩总结。以下是我参考的主要资料:
- JavaScript高级程序设计
- 你不知道的JavaScript(上卷)
- 陪你读书(JavaScript web前端)
- 王福朋的博客
- 冴羽写博客的地方
- 汤姆大叔深入 JavaScript 系列
- Facebook 的 ECMAScript 理论专家 Dmitry Soshnikov(最优秀的ECMAScript解读)
本人能力有限,如果有错误或者不严谨的地方,请务必给予指出,十分感谢!愿与君共勉。
(完)
