JavaScript 变量的生命周期:为什么 let 不存在变量提升

hubuke 8年前
   <p>变量提升是一个将变量或者声明函数提升到作用域起始处的过程,通常指的是变量声明 <code>var</code> 和函数声明 <code>function fun() {...}</code></p>    <p>当 <code>let</code>(以及具备了和 <code>let</code> 相似声明行为的 <code>const</code> 和 <code>class</code>)等声明方式在 ES2015 中被引入后,许多的开发者包括我都使用了<em>变量提升</em>的定义来描述变量是如何被访问的。但经过对这个问题更多的搜索后,我十分惊讶的发现<em>变量提升</em>并不是可以用来准确描述 <code>let</code> 变量初始化和可用性的合适术语。</p>    <p>ES2015 为 <code>let</code> 提供了一个不同的改进机制。它要求了更严格的变量声明方式(你在定义变量前是无法访问它的)并且这也在结果上保证了更好的代码质量。</p>    <p>现在让我们一起深入了解关于这个过程的更多细节。</p>    <h3>1. 容易出错的 <code>var</code> 变量提升</h3>    <p>有时我会在作用域下的任何位置上看到一个奇怪的变量声明 <code>var varname</code> 和函数声明 <code>function funName() {...}</code> 。</p>    <p><a href="/misc/goto?guid=4959677122373654878">Try in JS Bin</a></p>    <pre>  <code class="language-javascript">// var hoisting  num;     // => undefined  var num;  num = 10;  num;     // => 10  // function hoisting  getPi;   // => function getPi() {...}  getPi(); // => 3.14  function getPi() {    return 3.14;  }</code></pre>    <p>变量 <code>num</code> 在它的声明语句 <code>var num</code> 之前就被访问了,所以它的值为 <code>undefined</code></p>    <p>函数 <code>function getPi() {...}</code> 是定义在文件的末尾的。然而函数可以在它声明 <code>getPi()</code> 之前就被调用,因为它被提升到了作用域的顶部。</p>    <p>这就是典型的<em>变量提升</em>。</p>    <p>事实证明,在首次使用变量或函数后才声明变量或函数会很容易产生困惑。假设你正滚动查看一个大文件,然后发现了一个未声明的变量...你肯定会想它到底为什么在这里出现并且它是在哪定义的呢?</p>    <p>当然一个熟练的 JavaScript 开发者并不会这样编写代码。但在成千上万个 JavaScript Github 库中却可能存在着相当数量的这样的代码。</p>    <p>甚至在上面给出的代码示例中,我们也很难去明白代码中的声明流程。</p>    <p>我们应当自然地首先声明或是描述一个未知的术语。在这之后再对它进行使用。<code>let</code> 便是鼓励你遵循这种方法来设置变量。</p>    <h3>2. 深层内容: 变量的生命周期</h3>    <p>当引擎使用变量时,它们的生命周期包含以下阶段:</p>    <ol>     <li> <p><strong>声明阶段</strong> 这一阶段在作用域中注册了一个变量。</p> </li>     <li> <p><strong>初始化阶段</strong> 这一阶段分配了内存并在作用域中让内存与变量建立了一个绑定。在这一步变量会被自动初始化为 <code>undefined</code> 。</p> </li>     <li> <p><strong>赋值阶段</strong> 这一阶段为初始化变量分配具体的一个值。</p> </li>    </ol>    <p>一个变量在通过声明阶段时它还是处于 <strong>未初始化的</strong> 状态,这时它仍然还没有到达初始化阶段。</p>    <p><img alt="JavaScript 变量的生命周期:为什么 let 不存在变量提升 " src="https://simg.open-open.com/show/037a4bcb9f75933a2a9c7431993e161c.jpg"></p>    <p>注意,按照变量的生命周期过程,<em>声明阶段</em>与我们通常所说的<em>变量声明</em>是不同的术语。简单来讲,引擎处理变量声明需要经过完整的这 3 个阶段:声明阶段,初始化阶段和赋值阶段。</p>    <h3>3. <code>var</code> 变量的生命周期</h3>    <p>稍微熟悉下这些生命周期阶段,现在让我们用它们来描述引擎是如何处理 <code>var</code> 变量的。</p>    <p><img alt="JavaScript 变量的生命周期:为什么 let 不存在变量提升 " src="https://simg.open-open.com/show/8dc351ef3f26a59cf1a17a5f024e361c.jpg"></p>    <p>假设一个场景,当 JavaScript 遇到了一个函数作用域,其中包含了 <code>var variable</code> 的语句。则在任何语句执行之前,这个变量在作用域的开头就通过了<em>声明阶段</em>并马上来到了<em>初始化阶段</em>(步骤一)。</p>    <p>同时 <code>var variable</code> 在函数作用域中的位置并不会影响它的声明和初始化阶段的进行。</p>    <p>在声明和初始化阶段之后,赋值阶段之前,变量的值便是 <code>undefined</code> 并已经可以被使用了。</p>    <p>在<em>赋值阶段</em> <code>variable = 'value'</code> 语句使变量接受了它的初始化值(步骤二)。</p>    <p>这里的<em>变量提升</em>严格的说是指变量在函数作用域的<em>开始位置就完成了声明和初始化阶段</em>。在这里这两个阶段之间并没有任何的间隙。</p>    <p>让我们参考一个示例来研究。下面的代码创建了一个包含 <code>var</code> 语句的函数作用域:</p>    <p><a href="/misc/goto?guid=4959677122477286120">Try in JS Bin</a></p>    <pre>  <code class="language-javascript">function multiplyByTen(number) {    console.log(ten); // => undefined    var ten;    ten = 10;    console.log(ten); // => 10    return number * ten;  }  multiplyByTen(4); // => 40</code></pre>    <p>当 JavaScript 开始执行 <code>multipleByTen(4)</code> 时进入了函数作用域中,变量 <code>ten</code> 在第一个语句之前就经过了声明和初始化阶段,所以当调用 <code>console.log(ten)</code> 时打印为 <code>undefined</code>。</p>    <p>当语句 <code>ten = 10</code> 为变量赋值了初始化值。在赋值后,语句 <code>console.log(ten)</code> 打印了正确的 <code>10</code> 值。</p>    <h3>4. 函数声明的生命周期</h3>    <p>对于一个 <em>函数声明语句</em> <code>function funName() {...}</code> 那就更简单了。</p>    <p><img alt="JavaScript 变量的生命周期:为什么 let 不存在变量提升 " src="https://simg.open-open.com/show/cb0404adff86d2943db562a7595a3e34.jpg"></p>    <p><em>声明、初始化和赋值阶段</em>在封闭的函数作用域的开头便立刻进行(只有一步)。 <code>funName()</code> 可以在作用域中的任意位置被调用,这与其声明语句所在的位置无关(它甚至可以被放在程序的最底部)。</p>    <p>下面的代码是一个函数提升的演示:</p>    <p><a href="/misc/goto?guid=4959677122570314524">Try in JS Bin</a></p>    <pre>  <code class="language-javascript">function sumArray(array) {    return array.reduce(sum);    function sum(a, b) {      return a + b;    }  }  sumArray([5, 10, 8]); // => 23</code></pre>    <p>当 JavaScript 执行 <code>sumArray([5, 10, 8])</code> 时,它便进入了 <code>sumArray</code> 的函数作用域。在作用域内,任何语句执行之前的瞬间,<code>sum</code> 就经过了所有的三个阶段:声明,初始化和赋值阶段。</p>    <p>这样 <code>array.reduce(sum)</code> 即使在它的声明语句 <code>function sum(a, b) {...}</code> 之前也可以使用 <code>sum</code>。</p>    <h3>5. <code>let</code> 变量的生命周期</h3>    <p><code>let</code> 变量的处理方式不同于 <code>var</code>。它的主要区分点在于声明和初始化阶段是分开的。</p>    <p><img alt="JavaScript 变量的生命周期:为什么 let 不存在变量提升 " src="https://simg.open-open.com/show/d56bb5cfe1469659f7994f0f5bcb1b27.jpg"></p>    <p>现在让我们研究这样一个场景,当解释器进入了一个包含 <code>let variable</code> 语句的块级作用域中。这个变量立即通过了<em>声明阶段</em>,并在作用域内注册了它的名称(步骤一)。</p>    <p>然后解释器继续逐行解析块语句。</p>    <p>这时如果你在这个阶段尝试访问 <code>variable</code>,JavaScript 将会抛出 <code>ReferenceError: variable is not defined</code>。因为这个变量的状态依然是<em>未初始化</em>的。</p>    <p>此时 <code>variable</code> 处于<em>临时死区</em>中。</p>    <p>当解释器到达语句 <code>let variable</code> 时,此时变量通过了初始化阶段(步骤二)。现在变量状态是<em>初始化的</em>并且访问它的值是 <code>undefined</code>。</p>    <p>同时变量在此时也离开了<em>临时死区</em>。</p>    <p>之后当到达赋值语句 <code>variable = 'value'</code> 时,变量通过了赋值阶段(步骤三)。</p>    <p>如果 JavaScript 遇到这样的语句 <code>let variable = 'value'</code> ,那么变量会在这一条语句中同时经过初始化和赋值阶段。</p>    <p>让我们继续看一个示例。这里 <code>let</code> 变量 <code>number</code> 被创建在了一个块级作用域中:</p>    <p><a href="/misc/goto?guid=4959677122653436743">Try in JS Bin</a></p>    <pre>  <code class="language-javascript">let condition = true;  if (condition) {    // console.log(number); // => Throws ReferenceError    let number;    console.log(number); // => undefined    number = 5;    console.log(number); // => 5  }</code></pre>    <p>当 JavaScript 进入 <code>if (condition) {...}</code> 块级作用域中,<code>number</code> 立即通过了声明阶段。</p>    <p>因为 <code>number</code> 尚未初始化并且处于临时死区,此时试图访问该变量会抛出 <code>ReferenceError: number is not defined</code>.</p>    <p>之后语句 <code>let number</code> 使其得以初始化。现在变量可以被访问,但它的值是 <code>undefined</code>。</p>    <p>之后赋值语句 <code>number = 5</code> 当然也使变量经过了赋值阶段。</p>    <p><code>const</code> 和 <code>class</code> 类型与 <code>let</code> 有着相同的生命周期,除了它们的赋值语句只会发生一次。</p>    <p>5.1 为什么变量提升在 <code>let</code> 的生命周期中无效</p>    <p>如上所述,<em>变量提升</em>是变量的<em>耦合</em>声明并且在作用域的顶部完成初始化。</p>    <p>然而 <code>let</code> 生命周期中将声明和初始化阶段<em>解耦</em>。这一解耦使 <code>let</code> 的<em>变量提升</em>现象消失。</p>    <p>由于两个阶段之间的间隙创建了临时死区,在此时变量无法被访问。</p>    <p>这就像科幻的风格一样,在 <code>let</code> 生命周期中由于<em>变量提升</em>失效所以产生了临时死区。</p>    <h3>6. 结论</h3>    <p>使用 <code>var</code> 自由的去声明变量很容易出现错误。</p>    <p>基于这一点,ES2015 引进了 <code>let</code>。它使用了一种改进的算法来声明变量并添加了块作用域。</p>    <p>因为声明和初始化阶段是解耦的,变量提升对于 <code>let</code> 变量(也包括 <code>const</code> 和 <code>class</code>)是无效的。在初始化之前,变量处于临时死区中并不可被访问。</p>    <p>为了保证平稳的变量声明,推荐这些技巧以供参考:</p>    <ul>     <li> <p>声明,初始化变量后再使用变量。这个流程才是正确并易于遵循的。</p> </li>     <li> <p>尽可能的减少变量数。你暴露的变量越少,你的代码则会变得更加模块化。</p> </li>    </ul>    <p>这就是今天所有的内容。我们在下一篇文章再见。</p>    <p><span style="color:rgb(255, 255, 255)">Save</span></p>    <p>来自:http://www.zcfy.cc/article/javascript-variables-lifecycle-why-let-is-not-hoisted-976.html</p>