词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)。
下面通过几个小小的案例,开始深入的了解对理解词法作用域和闭包必不可少的,JS执行时底层的一些概念和理论知识。
经典案列重现
1、经典案例一
1 /*全局(window)域下的一段代码*/ 2 function a(i) { 3 var i; 4 alert(i); 5 }; 6 a(10);
疑问:上面的代码会输出什么呢?
答案:没错,就是弹出10。具体执行过程应该是这样的
- a 函数有一个形参 i,调用 a 函数时传入实参 10,形参 i=10
- 接着定义一个同名的局部变量 i,未赋值
- alert 输出 10
- 思考:局部变量 i 和形参 i 是同一个存储空间吗?
- 按照定义来理解:局部变量 i 和形参 i 是同一个存储空间(引用同一个内存地址)。ECMAScript中,函数执行时,传入函数的实际参数会在函数内部用一个数组来表示,可以通过arguments对象来访问这个参数数组。命名的形参仅是提供便利,但不是必需的。javascript权威指南里说道:Arguments对象有一个非同寻常的特性。当函数具有了命名了的参数时,Arguments对象的数组元素是存放函数参数的局部变量的同义词。arguments[]数组和命名了的参数是引用同一变量的两种不同方法。用参数名改变一个参数的值时同时会改变通过arguments[]数组获得的值,反之亦然。所以可以把函数的参数想象成一早就声明了的局部变量并已赋值(如果传入参数的话),而且此变量不管写入的值是基本类型还是引用类型,都会改变arguments[]数组对应的值,所以上面案列第3行代码定义了一个同名的局部变量i且未赋值是会被忽略的,因为ECMAScript规定在同一作用域里,如果重复声明一个变量并赋予初始值,那么它担当的不过是一个赋值语句的角色;如果重复声明一个变量但没有初始值,那么它不会对原来存在的变量有任何的影响。如下图:
从上图很明显看出,在语句var i;未执行时,i的值已经是10了。另一种理解是:对【var】变量做“预解析“,也就是说在函数执行之前,【var】变量就已经声明了但未赋值,当执行到var语句时仅仅是赋值而已。所以在函数声明局部变量时,一般都写在函数体的开头,以免影响理解,如经典案例四。
2、经典案例二
1 /*全局(window)域下的一段代码*/ 2 function a(i) { 3 alert(i); 4 alert(arguments[0]); //arguments[0]应该就是形参 i 5 var i = 2; 6 alert(i); 7 alert(arguments[0]); 8 }; 9 a(10);
疑问:上面的代码又会输出什么呢?(10,10,2,2 )
答案:在FireBug中的运行结果是第二个10,10,2,2,猜对了… ,下面简单说一下具体执行过程
- a 函数有一个形参i,调用 a 函数时传入实参 10,形参 i=10
- 第一个 alert 把形参 i 的值 10 输出
- 第二个 alert 把 arguments[0] 输出,应该也是 i
- 接着定义个局部变量 i 并赋值为2,这时候局部变量 i=2
- 第三个 alert 就把局部变量 i 的值 2 输出
- 第四个alert再次把 arguments[0] 输出
- 思考:这里能说明局部变量 i 和形参 i 的值相同吗?
3、经典案例三
1 /*全局(window)域下的一段代码*/ 2 function a(i) { 3 var i = i; 4 alert(i); 5 }; 6 a(10);
疑问:上面的代码又又会输出什么呢?(10 )
答案:在FireBug中的运行结果是 10,下面简单说一下具体执行过程
- 第一句声明一个与形参 i 同名的局部变量 i,根据结果我们知道,后一个 i 是指向了
- 形参 i,所以这里就等于把形参 i 的值 10 赋了局部变量 i
- 第二个 alert 当然就输出 10
- 思考:结合案列二,这里基本能说明局部变量 i 和形参 i 指向了同一个存储地址!
4、经典案例四
1 /*全局(window)域下的一段代码*/ 2 var i=10; 3 function a() { 4 alert(i); 5 var i = 2; 6 alert(i); 7 }; 8 a();
疑问:上面的代码又会输出什么呢?
答案:在FireBug中的运行结果是 undefined, 2,下面简单说一下具体执行过程
- 第一个alert输出undefined
- 第二个alert输出 2
- 思考:到底怎么回事儿?
看到上面的几个例子,你可能会弄错。原因是:我们能很快的写出一个方法,但到底方法内部是怎么执行的呢?执行的细节又是怎么样的呢?你可能没有进行过深入的学习和了解。要了解这些细节,那就需要了解 JS 引擎的工作方式,所以下面我们就把 JS 引擎对一个方法的解析过程进行一个稍微深入一些的介绍
解析过程
1、执行顺序
- 编译型语言,编译步骤分为:词法分析、语法分析、语义检查、代码优化和字节生成。
- 解释型语言,通过词法分析和语法分析得到语法分析树后,就可以开始解释执行了。这里是一个简单原始的关于解析过程的原理,仅作为参考,详细的解析过程(各种JS引擎还有不同)还需要更深一步的研究
JavaScript执行过程,如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:
步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)
步骤2. 做词法分析和语法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
步骤3. 对【var】变量和【function】定义做“预解析“(永远不会报错的,因为只解析正确的声明)
步骤4. 执行代码段,有错则报错(比如变量未定义)
步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
步骤6. 结束
2、特殊说明
全局域(window)域下所有JS代码可以被看成是一个“匿名方法“,它会被自动执行,而此“匿名方法“内的其它方法则是在被显示调用的时候才被执行
3、关键步骤
上面的过程,我们主要是分成两个阶段
- 解析:就是通过语法分析和预解析构造合法的语法分析树。
- 执行:执行具体的某个function,JS引擎在执行每个函数实例时,都会创建一个执行环境(ExecutionContext)和活动对象(activeObject)(它们属于宿主对象,与函数实例的生命周期保持一致)
3、关键概念
到这里,我们再更强调以下一些概念,这些概念都会在下面用一个一个的实体来表示,便于大家理解
- 语法分析树(SyntaxTree)可以直观地表示出这段代码的相关信息,具体的实现就是JS引擎创建了一些表,用来记录每个方法内的变量集(variables),方法集(functions)和作用域(scope)等
- 执行环境(ExecutionContext)可理解为一个记录当前执行的方法【外部描述信息】的对象,记录所执行方法的类型,名称,参数和活动对象(activeObject)
- 活动对象(activeObject)可理解为一个记录当前执行的方法【内部执行信息】的对象,记录内部变量集(variables)、内嵌函数集(functions)、实参(arguments)、作用域链(scopeChain)等执行所需信息,其中内部变量集(variables)、内嵌函数集(functions)是直接从第一步建立的语法分析树复制过来的
- 词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)
- 作用域链:词法作用域的实现机制就是作用域链(scopeChain)。作用域链是一套按名称查找(Name Lookup)的机制,首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着作用域链到父 ActiveObject 中寻找,一直找到全局调用对象(Global Object)
4、实体表示
5、函数的运行过程
- 建立执行环境(execution context)的阶段,函数将初始化各种变量,并将它们记录在一个内部的变量对象(variable object)中。记录在该变量对象中的变量依次有下面三种:(a)函数的实际参数;(b)内部的函数声明;(c)内部变量集。此时前面两种变量有了具体的值,内部变量集的值未undefined。
- 创建实参(arguments)对象,同名的实参,形参和变量之间是【引用】关系
- 执行方法内的赋值语句,这才会对变量集中的变量进行赋值处理
- 变量查找规则是首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着执行环境中属性 ScopeChain 指向的 ActiveObject 中寻找,一直到 Global Object(window)
- 方法执行完成后,内部变量值不会被重置,至于变量什么时候被销毁,请参考下面一条
- 方法内变量的生存周期取决于方法实例是否存在活动引用,如没有就销毁活动对象
- 6和7 是使闭包能访问到外部变量的根本原因
6、重释经典案例
案列一二三:根据【在一个方法中,同名的实参,形参和变量之间是引用关系,也就是JS引擎的处理是同名变量和形参都引用同一个内存地址】,所以才会有案例二中的修改arguments会影响到局部变量的情况出现
案例四:根据【JS引擎变量查找规则,首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着执行环境中属性 ScopeChain 指向的 ActiveObject 中寻找,一直到 Global Object(window)】,所以在案例四中,因为在当前的ActiveObject中找到了有变量 i 的定义,只是值为 “undefined”,所以直接输出 “undefined” 了