实例分析 JavaScript 作用域
了解作用域对于编写代码至关重要,作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。对于JavaScript中作用域我们可能已经了解了很多。建议看看 深入理解JavaScript中的作用域和上下文 和 JavaScript 核心概念之作用域和闭包。今天从其他方面介绍一下 JavaScript 中作用域,以帮助我们更好的完整的了解 JavaScript 作用域。
作用域模型
作用域有两种常见的模型:词法作用域(Lexical Scope,通常也叫做 静态作用域) 和 动态作用域(Dynamic Scope)。其中词法作用域更常见,被 JavaScript 等大多数语言采用。(愚人码头注:这里避开了with
和eval
特殊语句,不再做介绍)。
首先了解一下这两种模型的说明:
- 词法作用域:词法作用域是指在词法分析阶段就确定了,不会改变。变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。
- 动态作用域:动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们在何处调用。
JavaScript 的词法作用域
如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:
- 读入第一个代码段(js执行引擎并非一行一行地分析程序,而是一段一段地分析执行的)
- 做词法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
- 对
var
变量和function
定义做“预解析“(永远不会报错的,因为只解析正确的声明) - 执行代码段,有错则报错(比如变量未定义)
- 如果还有下一个代码段,则读入下一个代码段,重复步骤2
- 完成
JavaScript 解析过程
JavaScript 中每个函数都都表示为一个函数对象(函数实例),函数对象有一个仅供 JavaScript 引擎使用的[[scope]]
属性。通过语法分析和预解析,将[[scope]]
属性指向函数定义时作用域中的所有对象集合。这个集合被称为函数的作用域链(scope chain),包含函数定义时作用域中所有可访问的数据。
JavaScript 执行过程
执行具体的某个函数时,JS引擎在执行每个函数实例时,都会创建一个执行期上下文(Execution Context)和**对象(active Object)(它们属于宿主对象,与函数实例执行的生命周期保持一致,也就是函数执行完成,这些对象也就被销毁了,闭包例外。)
执行期上下文(Execution Context)定义了一个函数正在执行时的作用域环境。它使用函数[[scope]]
属性进行初始化。
随后,执行期上下文 顶部 的会创建一个**对象(active Object),这个**对象保存了函数中的所有形参,实参,局部变量,this
指针等函数执行时函数内部的数据情况。这个时候**对象中的那些属性并没有被赋值,执行函数内的赋值语句,这才会对变量集合中的变量进行赋值处理。也就是说 **对象是一个可变对象,里面的数据随着函数执行时的数据变化而变化。
具体请查看 JavaScript 核心概念之作用域和闭包 中的解释。有点啰嗦了。
考虑一下下图中的代码:
分析过程:
- 作用域1 (绿色) :即全局作用域,包含变量
foo
; - 作用域2 (黄色) :
foo
函数的作用域,包含变量a
,bar
,b
- 作用域3 (蓝色) :
bar
函数的作用域,包含变量c
bar
作用域里完整的包含了 foo
的作用域, 因为 bar
是定义在 foo
中的,产生嵌套作用域。值得注意的是,一个函数作用域只有可能存在于一个父级作用域中,不会同时存在两个父级作用域。还有诸如this
, window
, document
等全局对象这里就不说了,避免混乱。
执行过程:
- 语句
console.log
寻找变量a
,b
,c
; - 其中
c
在自己的作用域中找到, -
a
,b
在自己的作用域中找不到,于是向上级作用域中查找,在foo
的作用域中找到,并且调用。
函数在执行时,每遇到一个变量,都会去执行期上下文的作用域链的顶部,也就是执行函数的**对象开始搜索,如果在第一个作用域链(即,Activation Object **对象)中找到了,那么就返回这个变量。如果没有找到,那么继续向下查找,直到找到为止。如果在整个执行期上下文中都没有找到这个变量,在这种情况下,该变量被认为是未定义的。也就是说如果foo
的作用域中也定义了c
,但bar
函数只调用自己作用域里的c
。这就是我们说的变量取值。
实例分析
上面讲了很多概念性的东西。下面我们用实际代码讨论一下变量的作用域。这些可能在一下“坑人”的面试题中很常见。
不同作用域中的同名变量
同名变量可能在存在于多个作用域中,具体取值在执行时变量查找时决定。
示例1,作用域中变量查找规则:
JavaScript 代码:
- function DoSomething()
- {
- var a = 2;
- console.log(a); // 2
- console.log(window.a); // 1
- }
- var a = 1;
- DoSomething();
这个很好理解,全局作用域中定义并赋值了变量a
,DoSomething
函数中也定义并赋值了变量a
。DoSomething
函数在执行时,首先找到的是**对象中的变量a
,也就是2
。
console.log(window.a)
也很好理解,就是首先找window
对象,在**对象中没找到,继续向上搜索,本例中,window
对象在Global Object(全局对象)中找到,window
的a
属性值为1
。
这段代码我相信对大家来说,理解起来都没问题。那么我们稍微对代码做一下修改:
示例2,作用域中没赋值的变量:
JavaScript 代码:
- function DoSomething()
- {
- var a; // 注意这一行,定义了一个变量 a 但是不赋值。
- console.log(a); // 'undefined'
- }
- var a = 1;
- DoSomething();
DoSomething
函数中定义了变量a
,但是没有赋值,那么这里打印结果是什么呢?结果是 undefined
,原因和示例1一样,只不过DoSomething
函数在执行时,首先找到的是**对象中的变量a
,只不过这里的变量a
并没有赋值,所以是 undefined
。
示例3,**对象是一个可变对象
JavaScript 代码:
- function DoSomething()
- {
- console.log(a); // 'undefined'
- var a = 2; // 注意这一行。
- console.log(a); // 2
- }
- var a = 1;
- DoSomething();
根据作用域中变量查找规则,当前的**对象中找到了有变量 a
的定义,执行到第一条console.log(a)
语句时值为 undefined
。执行到第二条console.log(a)
语句时,变量 a
已经被赋值为2
。也就是说,**对象是一个可变对象,里面的数据随着函数执行时的数据变化而变化。
参数和同名变量
上面3个示例讲了不同作用域中的同名变量,这里我们来讲讲 形参(parameters)、实参(arguments)与同名变量的关系。
首先了解一下形参和实参:
JavaScript 代码:
- function one(a,b,c) {
- console.log(one.length);//形参数量
- }
- function two(a,b,c,d,e,f,g){
- console.log(arguments.length);//实参数量
- }
- one(1)
- two(1)
顾名思义,形参就是形式参数,是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传递的参数。实参就是实际参数,是在调用时传递给函数的参数,即传递给被调用函数的值。实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。
那么形参、实参与变量同名又该如何呢?
示例4,形参、实参与同名全局变量的关系
JavaScript 代码:
- // DoSomething传入了一个 a 参数。
- var b = 2;
- function DoSomething(a,b)
- {
- var a;
- console.log(a); // 1
- console.log(b); // 'undefined'
- }
- DoSomething( 1 );
打印的结果为1
和undefined
,为什么呢?
首先来看形参b
和全局变量b
,这两者毫无关系,只是同名。完全符合作用域中变量查找规则,DoSomething
函数在执行时,首先找到的是**对象中的形参b
,形参b
没有值,所以是 undefined
。
接着来说说 参数a
和局部变量a
,看上面的代码似乎并不符合作用域中变量查找规则。如果作用域中变量查找规则去理解,应该是这样的,DoSomething
函数在执行时,首先找到的是**对象中的参数a
,然后定义局部变量a
,不赋值,所以console.log(a)
是 undefined
。但是结果恰恰是1
。这又是为什么呢?
示例4并不能解释这个问题,让我们再来看一段代码。
示例5,形参、实参与同名局部变量的关系
JavaScript 代码:
- function DoSomething(a)
- {
- console.log(a); // 1
- console.log(arguments[0]); // 1
- var a = 2;
- console.log(a); // 2
- console.log(arguments[0]); // 2
- }
- DoSomething( 1 );
打印的结果是1
,1
,2
,2
。从上面的代码可以看到,参数a
和局部变量a
值是完全相同的,即使是局部变量a
重新定义和赋值之后。这样就好理解了,参数和同名变量之间是 “引用” 关系,也就是说 JavaScript 引擎的处理参数和同名局部变量是都引用同一个内存地址。所以示例5中修改局部变量会影响到arguments
的情况出现。
建议看看 深入理解JavaScript中的作用域和上下文 和 JavaScript 核心概念之作用域和闭包。这篇文章只是上面两篇文章的后续。
本系列知识相关阅读: