作用域与作用域链本应该是一个非常简单的概念。可是在近两年多的留言中,我发现这些概念反而成为了大多数人想不明白的点,而感到困惑的原因在于,别的文章里,常常会提到词法作用域,词法分析等概念,到底是什么东西?好像跟我说的有一点不一样,但又不知道哪里不对?
为了避免接下来更多的同学造成同样的困扰,我写了一篇名为(v8引擎是如何工作的)文章,为大家分析JS的工作原理。后续会整理进入基础进阶系列文章。大家可以随时阅读。
词法环境(Lexical Environments)
官方规范对词法环境的说明是:词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。
官方就是爱把简单的东西复杂化,专业!
通俗来说,词法环境就是一套约定好的规则。我们写代码,应该按照这个规则来。JS引擎对JS源码进行词法分析,也是按照这个规则来。
因此,词法环境,在我们写代码的时候就已经确定了。明白了这一点,对于区分词法环境与执行上下文有非常关键的意义。
词法环境,其实就是作用域,有的人叫词法作用域等等
得出结论:
一、在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理JS引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
这里的标识符,指的是变量名或者函数名
二、JavaScript中有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。
现在,大家知道词法环境,作用域和执行上下文的区别了吗?
更多的关于JS引擎是如何处理JS代码的,大家可以参考(V8引擎是如何工作的)一文
除此之外,一个词法环境,由环境记录(Environment Records)与一个外部指向outer组成。
环境记录如果非要细分的话,又可以分为函数环境记录和声明性的环境记录。而环境记录对应的,其实就是变量对象,outer对应的,就是上文中出现过,但是没有引起重视的scopeChain。
作用域链
首先我们要明确的一点是,作用域和作用域链是不同的。
作用域是一套规则。
而作用域链则是在代码执行过程中,会动态变化的一条索引路径。
在理解之前,需要回顾一下上一篇文章我们分析的执行上下文的生命周期,如下图。
我们知道函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。之前一篇文章我们详细说明了变量对象,而这里,我们将详细说明作用域链。
作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
为了帮助大家理解作用域链,我我们先结合一个例子,以及相应的图示来说明。
var a = 20; function test() { var b = a + 10; function innerTest() { var c = 10; return b + c; } return innerTest(); } test();
在上面的例子中,全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。
innerTestEC = { VO: {...}, // 变量对象 scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链 }
我们这里使用一个数组来模拟作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象。
很多人会误解为当前作用域与上层作用域为包含关系,但其实并不是。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图。
注意,因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,因此图中使用了AO来表示。Active Object
作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样,就能访问到上一层作用域中的变量。
最后,用一个问题,验证一下大家对作用域和作用域链以及变量对象的理解:
foo的执行结果是什么?
var a = 20; function foo() { if (!a) { a = 100; } var a = 10; return a; } console.log(foo());