JavaScript中的作用域与作用域链

前端开发
2020年02月14日
380

作用域

作用域是在运行时代码中的某些特定部分中变量、函数和对象的可访问性。换句话来说,作用域决定了代码区块中变量和其他资源的可见性。举个粟子:

js
function test () { var inner = 'inner'; } console.log(inner); // Uncaught ReferenceError: inner is not defined

从上面的例子中,我们可以看出变量inner在全局作用域下并没有声明,所以在全局作用域下访问会报错。我们可以这样理解:作用域是一个独立的地盘,让变量不会外泄出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6之前JavaScript中并没有块级作用域,只有全局作用域函数作用域

全局作用域和函数作用域

全局作用域

在代码中任何地方都能访问到的变量拥有全局作用域,一般来说,以下几种情形拥有全局作用域:

  1. 最外层函数和在最外层函数外面定义的变量:

    js
    var outerVariable = 'outer'; function outerFunc () { var innerVariable = 'inner'; function innerFunc () {} } // 其中,outerVariable 与 outerFunc 就具有全局作用域 console.log(outerVariable); console.log(outerFunc); // console.log(innerVariable); // Uncaught ReferenceError: innerVariable is not defined console.log(innerFunc); // 作用域与作用域链.html:25 Uncaught ReferenceError: innerFunc is not defined
  2. 所有未被定义,但直接赋值的变量(暗示全局变量:imply global variable):

    js
    function test () { var a = 1; b = 2; var c = d = 3; } test(); // 变量 b 和 d 未定义,但直接赋值 // console.log(a); // Uncaught ReferenceError: a is not defined console.log(b); // 2 // console.log(c); // Uncaught ReferenceError: c is not defined console.log(d); // 3
  3. 所有window对象的属性:

    一般情况下,window对象的内置属性都拥有全局作用域,例如window.locationwindow.namewindow.top等。

函数作用域

函数作用域,是指声明在函数内部的变量,和全局作用域不同,局部作用域一般只在固定的代码片段中才可以访问到。

js
function test () { var inner = 'inner'; function say () { console.log(inner); } say(); } test(); // 'inner' console.log(inner); // Uncaught ReferenceError: inner is not defined

作用域是分层的,内层作用域可以访问到外层作用域的变量,反之则不行。我们看个例子:

作用域0

  1. 红色框内是全局作用域,有函数foo
  2. 绿色框内是函数foo的作用域,有变量ab以及函数bar
  3. 蓝色框内是函数bar的作用域,有变量c

值得注意的是:代码块({}中间的语句),如ifswitch条件语句或forwhile循环语句,它们不会创建一个新的作用域。

js
if (true) { var a = 'a'; // a 依然在全局作用域下,这个问题在上一篇文章中的预编译中就已经说明。 } console.log(a); // 'a'

块级作用域

ES6中,有一个块级作用域的概念,可以通过letconst声明变量,所声明的变量在指定的代码块的作用域外无法被访问。

具体的信息可以查阅:let,var和const三者的区别

作用域链

在JavaScript中,函数也是一种对象类型、引用类型,也称作引用值。

对象中有一些属性是我们无法访问到的,它们是JS引擎内部固有的隐式属性(也可以称之为私有属性)。接下来要说的[[scope]]便是其中之一。

[[scope]]

[[scope]]是函数在创建时,生成的一个JS内部的隐式属性;它也是函数存储作用域容器。而作用域链可以认为是一个保存AOGO的容器。

图解作用域链形成过程

先看下以下的代码

js
function a () { function b () { var b = 2; } var a = 1; b(); } var c = 3; a();
  1. 函数a被定义时,系统生成[[scope]]属性,[[scope]]中保存该函数的作用域链,该作用域链的第0位存储当前环境下的全局执行期上下文(GO),GO里存储全局下所有对象,其中包含函数a和全局变量c

    作用域1

  2. 函数a执行的前一刻,作用域链的顶端(第0位)存储函数a生成的函数执行期上下文AO,同时GO被挤到第1位。查找变量是在函数a存储的作用域链中从顶端开始依次向下查找。

    作用域2

  3. 函数b被定义时,是在函数a的环境下,所以函数b此时的作用域链就是函数a被执行时的作用域链。

    作用域3

  4. 函数b执行的前一刻,系统生成函数b[[scope]]属性,存储函数b的作用域链,第0位存储函数b的AO,函数a的AO和全局的GO被依次挤下去。

    作用域4

  5. 当函数b执行完毕函数b的AO被销毁,回归到被定义时的状态。

    作用域5

    作用域6

  6. 当函数a被执行结束时函数a的AO被销毁,同时函数b[[scope]]也将不存在了。函数a回归到被定义时的状态。

    作用域7

    作用域8

注意:

  • 每一个函数的作用域链上都会有GO
  • 每一个函数的作用域链最顶端都是自身的AO

作用域与执行期上下文

很多人都经常混淆作用域和执行期上下文的概念,误认为它们是相同的概念,但事实并非如此。

我们知道JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段:

解释阶段:

  • 词法分析
  • 语法分析
  • 作用域规则确定

执行阶段:

  • 创建执行期上下文
  • 执行函数代码
  • 垃圾回收

JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定;而执行期上下文是函数执行之前创建的。执行期上下文最明显的就是this的指向是执行时确定的;而作用域访问的变量是编写代码的结构确定的。

作用域和执行期上下文之间的最大区别是:

执行期上下文在运行时确定,随时可能改变;作用域在定义时就确定,并不会改变。

一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(如:函数不被调用);有可能有,当函数被调用完毕后,上下文环境被销毁了;也有可能同时存在一个或多个(闭包)。同一作用域下,不同的调用产生不同的执行期上下文环境,继而产生不同的变量的值。