We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
译者:前端小智 原文:https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/
译者:前端小智
原文:https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/
为了保证的可读性,本文采用意译而非直译。
提升是将变量或函数定义移动到作用域头部的过程,通常是 var 声明的变量和函数声明function fun() {...}。
var
function fun() {...}
当 ES6 引入let(以及与let类似声明的const和class)声明时,许多开发人员都使用提升定义来描述如何访问变量。但是在对这个问题进行了更多的探讨之后,令我惊讶的是提升并不是描述let变量的初始化和可用性的正确术语。
let
const
class
ES6 为let提供了一个不同的和改进的机制。它要求更严格的变量声明,在定义之前不能使用,从而提高代码质量。
想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!
有时候我们会在zuo内作用域内看到一个奇怪的变量var varname和函数函数function funName() {...} 声明:
var varname
funName() {...}
// var hoisting num; // => undefined var num; num = 10; num; // => 10 // function hoisting getPi; // => function getPi() {...} getPi(); // => 3.14 function getPi() { return 3.14; }
变量num在声明var num之前被访问,因此它被赋值为undefined。fucntion getPi(){…}在文件末尾定义。但是,可以在声明getPi()之前调用该函数,因为它被提升到作用域的顶部。
num
var num
undefined
fucntion getPi(){…}
getPi()
事实证明,先使用然后声明变量或函数的可能性会造成混淆。假设您滚动一个大文件,突然看到一个未声明的变量,它到底是如何出现在这里的,以及它在哪里定义的?
当然,一个熟练的JavaScript开发人员不会这样编写代码。但是在成千上万的JavaScript中,GitHub repos是很有可能处理这样的代码的。
即使查看上面给出的代码示例,也很难理解代码中的声明流。
当然,首先要声明再使用。let 鼓励咱们使用这种方法处理变量。
当引擎处理变量时,它们的生命周期由以下阶段组成:
**声明阶段(Declaration phase)**是在作用域中注册一个变量。
**初始化阶段(Initialization phase)**是分配内存并为作用域中的变量创建绑定。 在此步骤中,变量将使用undefined自动初始化。
**赋值阶段(Assignment phase)**是为初始化的变量赋值。
变量在通过声明阶段时尚未初始化状态,但未达到初始化状态。
请注意,就变量生命周期而言,声明阶段与变量声明是不同的概念。 简而言之,JS引擎在3个阶段处理变量声明:声明阶段,初始化阶段和赋值阶段。
熟悉生命周期阶段之后,让我们使用它们来描述JS引擎如何处理var变量。
假设JS遇到一个函数作用域,其中包含var变量语句。变量在执行任何语句之前通过声明阶段,并立即通过作用域开始处的初始化阶段(步骤1)。函数作用域中var变量语句的位置不影响声明和初始化阶段。
在声明和初始化之后,但在赋值阶段之前,变量具有undefined 的值,并且已经可以使用。
在赋值阶段variable = 'value' 时,变量接收它的初值(步骤2)。
variable = 'value'
严格意义的提升是指在函数作用域的开始处声明并初始化一个变量。声明阶段和初始化阶段之间没有差别。
让我们来研究一个例子。下面的代码创建了一个包含var语句的函数作用域
function multiplyByTen(number) { console.log(ten); // => undefined var ten; ten = 10; console.log(ten); // => 10 return number * ten; } multiplyByTen(4); // => 40
开始执行multipleByTen(4)并进入函数作用域时,变量ten在第一个语句之前通过声明和初始化步骤。因此,当调用console.log(ten)时,打印undefined。语句ten = 10指定一个初值。赋值之后,console.log(ten) 将正确地打印10。
multipleByTen(4)
ten
console.log(ten)
ten = 10
10
在函数声明语句function funName() {...}的情况下,它比变量声明生命周期更简单。
function funName() {...}
声明、初始化和赋值阶段同时发生在封闭函数作用域的开头(只有一步)。可以在作用域的任何位置调用funName(),而不依赖于声明语句的位置(甚至可以在末尾调用)。
funName()
下面的代码示例演示了函数提升:
function sumArray(array) { return array.reduce(sum); function sum(a, b) { return a + b; } } sumArray([5, 10, 8]); // => 23
当执行sumArray([5,10,8])时,它进入sumArray函数作用域。在这个作用域内,在任何语句执行之前,sum都会通过所有三个阶段:声明、初始化和赋值。这样,array.reduce(sum)甚至可以在它的声明语句sum(a, b){…}之前使用sum。
sumArray([5,10,8])
sumArray
sum
array.reduce(sum)
sum(a, b){…}
let 变量的处理方式与var不同,主要区别在于声明和初始化阶段是分开的。
现在来看看一个场景,当解释器进入一个包含let变量语句的块作用域时。变量立即通过声明阶段,在作用域中注册其名称(步骤1)。
然后解释器继续逐行解析块语句。
如果在此阶段尝试访问变量,JS 将抛出 ReferenceError: variable is not defined。这是因为变量状态未初始化,变量位于暂时死区 temporal dead zone。
ReferenceError: variable is not defined
未初始化
当解释器执行到语句let variable时,传递初始化阶段(步骤2)。变量退出暂时死区。
let variable
接着,当赋值语句variable = 'value'出现时,将传递赋值阶段(步骤3)。
如果JS 遇到let variable = 'value',那么初始化和赋值将在一条语句中发生。
let variable = 'value'
让我们看一个例子,在块作用域中用 let 声明变量 number
number
let condition = true; if (condition) { // console.log(number); // => Throws ReferenceError let number; console.log(number); // => undefined number = 5; console.log(number); // => 5 }
当 JS 进入if (condition) {...} 块作用域,number立即通过声明阶段。
if (condition) {...}
由于number已经处于单一化状态,并且处于的暂时死区,因此访问该变量将引发ReferenceError: number is not defined。接着,语句let number进行初始化。现在可以访问变量,但是它的值是undefined。
ReferenceError: number is not defined
let number
const和class 类型与let具有相同的生命周期,只是分配只能发生一次。
如上所述,提升是变量在作用域顶部的耦合声明和初始化阶段。然而,let生命周期分离声明和初始化阶段。解耦消除了let的提升期限。
这两个阶段之间的间隙产生了暂时死区,在这里变量不能被访问。
使用var声明变量很容易出错。在此基础上,ES6 引入了let。它使用一种改进的算法来声明变量,并附加了块作用域。
由于声明和初始化阶段是解耦的,提升对于let变量(包括const和class)无效。在初始化之前,变量处于暂时死区,不能访问。
为了保持变量声明的流畅性,建议使用以下技巧
声明、初始化然后使用变量,这个流程是正确的,易于遵循。
尽量隐藏变量。公开的变量越少,代码就越模块化。
这个问题说明:如果 let x 的初始化过程失败了,那么
x 变量就将永远处于 created 状态。
你无法再次对 x 进行初始化(初始化只有一次机会,而那次机会你失败了)。
由于 x 无法被初始化,所以 x 永远处在暂时死区
有人会觉得 JS 坑,怎么能出现这种情况;其实问题不大,因为此时代码已经报错了,后面的代码想执行也没机会。
参考:
我用了两个月的时间才理解 let
干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。
https://github.com/qq449245884/xiaozhi
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
为了保证的可读性,本文采用意译而非直译。
提升是将变量或函数定义移动到作用域头部的过程,通常是
var
声明的变量和函数声明function fun() {...}
。当 ES6 引入
let
(以及与let
类似声明的const
和class
)声明时,许多开发人员都使用提升定义来描述如何访问变量。但是在对这个问题进行了更多的探讨之后,令我惊讶的是提升并不是描述let
变量的初始化和可用性的正确术语。ES6 为
let
提供了一个不同的和改进的机制。它要求更严格的变量声明,在定义之前不能使用,从而提高代码质量。想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!
1. 容易出错的 var 提升
有时候我们会在zuo内作用域内看到一个奇怪的变量
var varname
和函数函数functionfunName() {...}
声明:变量
num
在声明var num
之前被访问,因此它被赋值为undefined
。fucntion getPi(){…}
在文件末尾定义。但是,可以在声明getPi()
之前调用该函数,因为它被提升到作用域的顶部。事实证明,先使用然后声明变量或函数的可能性会造成混淆。假设您滚动一个大文件,突然看到一个未声明的变量,它到底是如何出现在这里的,以及它在哪里定义的?
当然,一个熟练的JavaScript开发人员不会这样编写代码。但是在成千上万的JavaScript中,GitHub repos是很有可能处理这样的代码的。
即使查看上面给出的代码示例,也很难理解代码中的声明流。
当然,首先要声明再使用。
let
鼓励咱们使用这种方法处理变量。2. 理解背后原理:变量生命周期
当引擎处理变量时,它们的生命周期由以下阶段组成:
**声明阶段(Declaration phase)**是在作用域中注册一个变量。
**初始化阶段(Initialization phase)**是分配内存并为作用域中的变量创建绑定。 在此步骤中,变量将使用
undefined
自动初始化。**赋值阶段(Assignment phase)**是为初始化的变量赋值。
变量在通过声明阶段时尚未初始化状态,但未达到初始化状态。
请注意,就变量生命周期而言,声明阶段与变量声明是不同的概念。 简而言之,JS引擎在3个阶段处理变量声明:声明阶段,初始化阶段和赋值阶段。
3.var 变量的生命周期
熟悉生命周期阶段之后,让我们使用它们来描述JS引擎如何处理
var
变量。假设JS遇到一个函数作用域,其中包含
var
变量语句。变量在执行任何语句之前通过声明阶段,并立即通过作用域开始处的初始化阶段(步骤1)。函数作用域中var
变量语句的位置不影响声明和初始化阶段。在声明和初始化之后,但在赋值阶段之前,变量具有
undefined
的值,并且已经可以使用。在赋值阶段
variable = 'value'
时,变量接收它的初值(步骤2)。严格意义的提升是指在函数作用域的开始处声明并初始化一个变量。声明阶段和初始化阶段之间没有差别。
让我们来研究一个例子。下面的代码创建了一个包含
var
语句的函数作用域开始执行
multipleByTen(4)
并进入函数作用域时,变量ten
在第一个语句之前通过声明和初始化步骤。因此,当调用console.log(ten)
时,打印undefined
。语句ten = 10
指定一个初值。赋值之后,console.log(ten)
将正确地打印10
。4. 函数声明生命周期
在函数声明语句
function funName() {...}
的情况下,它比变量声明生命周期更简单。声明、初始化和赋值阶段同时发生在封闭函数作用域的开头(只有一步)。可以在作用域的任何位置调用
funName()
,而不依赖于声明语句的位置(甚至可以在末尾调用)。下面的代码示例演示了函数提升:
当执行
sumArray([5,10,8])
时,它进入sumArray
函数作用域。在这个作用域内,在任何语句执行之前,sum
都会通过所有三个阶段:声明、初始化和赋值。这样,array.reduce(sum)
甚至可以在它的声明语句sum(a, b){…}
之前使用sum
。5. let 变量的生命周期
let
变量的处理方式与var
不同,主要区别在于声明和初始化阶段是分开的。现在来看看一个场景,当解释器进入一个包含
let
变量语句的块作用域时。变量立即通过声明阶段,在作用域中注册其名称(步骤1)。然后解释器继续逐行解析块语句。
如果在此阶段尝试访问变量,JS 将抛出
ReferenceError: variable is not defined
。这是因为变量状态未初始化
,变量位于暂时死区 temporal dead zone。当解释器执行到语句
let variable
时,传递初始化阶段(步骤2)。变量退出暂时死区。接着,当赋值语句
variable = 'value'
出现时,将传递赋值阶段(步骤3)。如果JS 遇到
let variable = 'value'
,那么初始化和赋值将在一条语句中发生。让我们看一个例子,在块作用域中用
let
声明变量number
当 JS 进入
if (condition) {...}
块作用域,number
立即通过声明阶段。由于
number
已经处于单一化状态,并且处于的暂时死区,因此访问该变量将引发ReferenceError: number is not defined
。接着,语句let number
进行初始化。现在可以访问变量,但是它的值是undefined
。const
和class
类型与let
具有相同的生命周期,只是分配只能发生一次。5.1 提升在
let
生命周期中无效的原因如上所述,提升是变量在作用域顶部的耦合声明和初始化阶段。然而,
let
生命周期分离声明和初始化阶段。解耦消除了let
的提升期限。这两个阶段之间的间隙产生了暂时死区,在这里变量不能被访问。
总结
使用
var
声明变量很容易出错。在此基础上,ES6 引入了let
。它使用一种改进的算法来声明变量,并附加了块作用域。由于声明和初始化阶段是解耦的,提升对于
let
变量(包括const
和class
)无效。在初始化之前,变量处于暂时死区,不能访问。为了保持变量声明的流畅性,建议使用以下技巧
声明、初始化然后使用变量,这个流程是正确的,易于遵循。
尽量隐藏变量。公开的变量越少,代码就越模块化。
番外
如何理解 let x = x 报错之后,再次 let x 依然会报错?
这个问题说明:如果 let x 的初始化过程失败了,那么
x 变量就将永远处于 created 状态。
你无法再次对 x 进行初始化(初始化只有一次机会,而那次机会你失败了)。
由于 x 无法被初始化,所以 x 永远处在暂时死区
有人会觉得 JS 坑,怎么能出现这种情况;其实问题不大,因为此时代码已经报错了,后面的代码想执行也没机会。
参考:
我用了两个月的时间才理解 let
交流
干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。
The text was updated successfully, but these errors were encountered: