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