JavaScript中的作用域与作用域链
作用域
作用域是在运行时代码中的某些特定部分中变量、函数和对象的可访问性。换句话来说,作用域决定了代码区块中变量和其他资源的可见性。举个粟子:
js
function test () {
var inner = 'inner';
}
console.log(inner); // Uncaught ReferenceError: inner is not defined
从上面的例子中,我们可以看出变量inner
在全局作用域下并没有声明,所以在全局作用域下访问会报错。我们可以这样理解:作用域是一个独立的地盘,让变量不会外泄出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
在ES6
之前JavaScript中并没有块级作用域,只有全局作用域和函数作用域。
全局作用域和函数作用域
全局作用域
在代码中任何地方都能访问到的变量拥有全局作用域,一般来说,以下几种情形拥有全局作用域:
-
最外层函数和在最外层函数外面定义的变量:
jsvar 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
-
所有未被定义,但直接赋值的变量(暗示全局变量:imply global variable):
jsfunction 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
-
所有
window对象
的属性:一般情况下,
window对象
的内置属性都拥有全局作用域,例如window.location
、window.name
、window.top
等。
函数作用域
函数作用域,是指声明在函数内部的变量,和全局作用域不同,局部作用域一般只在固定的代码片段中才可以访问到。
js
function test () {
var inner = 'inner';
function say () {
console.log(inner);
}
say();
}
test(); // 'inner'
console.log(inner); // Uncaught ReferenceError: inner is not defined
作用域是分层的,内层作用域可以访问到外层作用域的变量,反之则不行。我们看个例子:
- 红色框内是全局作用域,有
函数foo
; - 绿色框内是
函数foo
的作用域,有变量a
、b
以及函数bar
; - 蓝色框内是
函数bar
的作用域,有变量c
。
值得注意的是:代码块({}
中间的语句),如if
或switch
条件语句或for
、while
循环语句,它们不会创建一个新的作用域。
js
if (true) {
var a = 'a'; // a 依然在全局作用域下,这个问题在上一篇文章中的预编译中就已经说明。
}
console.log(a); // 'a'
块级作用域
在ES6
中,有一个块级作用域的概念,可以通过let
和const
声明变量,所声明的变量在指定的代码块的作用域外无法被访问。
具体的信息可以查阅:let,var和const三者的区别
作用域链
在JavaScript中,函数也是一种对象类型、引用类型,也称作引用值。
对象中有一些属性是我们无法访问到的,它们是JS引擎内部固有的隐式属性(也可以称之为私有属性)。接下来要说的[[scope]]
便是其中之一。
[[scope]]
[[scope]]
是函数在创建时,生成的一个JS内部的隐式属性;它也是函数存储作用域容器。而作用域链可以认为是一个保存AO
、GO
的容器。
图解作用域链形成过程
先看下以下的代码
js
function a () {
function b () {
var b = 2;
}
var a = 1;
b();
}
var c = 3;
a();
-
当
函数a
被定义时,系统生成[[scope]]
属性,[[scope]]
中保存该函数的作用域链,该作用域链的第0
位存储当前环境下的全局执行期上下文(GO),GO里存储全局下所有对象,其中包含函数a
和全局变量c
。 -
函数a
执行的前一刻,作用域链的顶端(第0
位)存储函数a
生成的函数执行期上下文AO,同时GO被挤到第1
位。查找变量是在函数a
存储的作用域链中从顶端开始依次向下查找。 -
当
函数b
被定义时,是在函数a
的环境下,所以函数b
此时的作用域链就是函数a
被执行时的作用域链。 -
函数b执行的前一刻,系统生成
函数b
的[[scope]]
属性,存储函数b
的作用域链,第0
位存储函数b
的AO,函数a
的AO和全局的GO被依次挤下去。 -
当函数b执行完毕,
函数b
的AO被销毁,回归到被定义时的状态。 -
当函数a被执行结束时,
函数a
的AO被销毁,同时函数b
的[[scope]]
也将不存在了。函数a
回归到被定义时的状态。
注意:
- 每一个函数的作用域链上都会有GO
- 每一个函数的作用域链最顶端都是自身的AO
作用域与执行期上下文
很多人都经常混淆作用域和执行期上下文的概念,误认为它们是相同的概念,但事实并非如此。
我们知道JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段:
解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定
执行阶段:
- 创建执行期上下文
- 执行函数代码
- 垃圾回收
JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定;而执行期上下文是函数执行之前创建的。执行期上下文最明显的就是this的指向是执行时确定的;而作用域访问的变量是编写代码的结构确定的。
作用域和执行期上下文之间的最大区别是:
执行期上下文在运行时确定,随时可能改变;作用域在定义时就确定,并不会改变。
一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(如:函数不被调用);有可能有,当函数被调用完毕后,上下文环境被销毁了;也有可能同时存在一个或多个(闭包)。同一作用域下,不同的调用产生不同的执行期上下文环境,继而产生不同的变量的值。