【译】深入理解 ES2015,第一趴:块作用域 let 和 const
ES2015 最大的特性之一就是有了一个全新的作用域。在这个章节里,我们将开始学习什么是作用域。我们将继续学习如何创建新的作用域类型,以及给我们代码带来的好处
快速了解作用域
作用域描述为一个变量,函数,标识符可以被访问的区域。JavaScript 传统上有两种作用域类型:全局作用域和函数作用域,你定义变量的位置会影响其他代码是否可以访问。让我们来看一个简单的例子来阐述作用域的概念。想象一下,你的 JavaScript 文件只包含以下代码:
1 | var globalVariable = 'This is global'; |
在上面的代码中,我们首先声明了一个变量 globalVariable。这个语句不在函数内部,所以会自动存到全局作用域中。浏览器用 window 对象创建了一个全局作用域,除了可以用 globalVariable 访问,我们还可以通过挂在 window 对象上的 window.globalVariable 访问。我们可以在文件的任何地方访问这个变量,这两个函数的之前或之后,甚至是在函数的内部(这就是为什么我们说全局变量是 “隐藏的”,我们可以在任何地方正确的访问他们),甚至是在附在同一页面的其他 JavaScript 文件
在全局作用域里,我们定义了两个函数,globalFunction1 和 globalFunction2,就像全局变量一样,他们是 “可见的” 并且可以在这个文件的任何地方调用,也可以被同一页面的其他 JavaScript 文件调用。然而,当 JavaScript 引擎解析这些函数时,会分别创建他们自己的作用域。因吹斯听,这两个新的函数作用域被嵌套在全局作用域下,成为子作用域。这也就意味着函数内的代码可以访问全局变量,就像是和在函数 “内部的” 定义变量一样
当我们试图访问 JavaScript 里的标识符时,浏览器会首先在当前作用域中查找。如果没有找到,浏览器会在当前作用域的父作用域中查找,并且继续向上查找,直到找到这个变量,或者到达全局作用域为止。如果这个变量在全局作用域里依旧没有找到的话,那么浏览器会抛出一个 ReferenceError 错误。这种嵌套的作用域被称作作用域链,而这个检查当前作用域和父作用域的过程被称作变量查找。这种查找只会向上查找作用域链,它永远不会在它的子作用域里查找
在上面的作用域链查找方向我们得知,例子中的 innerVariable1 变量只能在 globalFunction1 函数内部被访问,innerVariable2 变量只能在 globalFunction2 函数内部被访问。innerVariable1 变量不能在 globalFunction2 函数内部或全局作用域内被访问,innerVariable2 变量也不能在 globalFunction1 函数内部或全局作用域内被访问
下面的图片是上面代码中作用域的抽象表示:
全局作用域包含了
globalVariable以及两个内嵌的函数作用域。每个内嵌的函数作用域又包含自己的变量,但是这些变量不能被全局作用域访问。虚线表示的是作用域链的查找方向
让我们来看下另一个简短的代码示例,彻底的了解下到目前为止我们所介绍到的作用域概念。假设 JavaScript 文件只包含如下代码:
1 | function outer() { |
在这段代码里,我们在全局作用域里声明了一个叫 outer 的函数。因为它是一个函数,所以它创建了一个函数作用域,嵌套在全局作用域下。在这个作用域下,我们又声明了一个叫 variable1 的变量和 一个叫 inner 的函数。因为 inner 也是一个函数,所以一个新的作用域又被创建了,嵌套在 outer 函数的作用域下
在 inner 函数中,我们既可以访问 variable2 也可以访问 variable1。当我们在 inner 函数中访问 variable1 时,浏览器首先会在它的作用域里查找这个变量;当这个变量没有被找到时,会继续向上在父作用域里查找(也就是 outer 函数的作用域)。代码里作用域如下图所示:
函数作用域可以嵌套在其他的函数作用域里,但是作用域链查找规则是一样的,因此在
inner作用域下可以访问到variable1和variable2,但是在outer作用域下只能访问variable1
这个示例中的作用域链比较长,从 inner 函数延伸到 outer 函数,直到全局对象 window
JavaScript 的新作用域
在 JavaScript 中,一个块是由一个或多个语句用大括号包裹起来的。诸如 if,for,while 的条件表达式,都是用块基于特定的条件来执行块语句
其他流行的常见的编程语言都有块作用域,JavaScript 作用域中,直到如今却只有全局作用域和函数作用域,因此使我们变得很困惑。ES2015 在 JavaScript 新增了块作用域,对于我们的代码来说有很大的影响,并且对于那些熟悉其他编程语言的开发者来说变得更直观
块作用域意味着一个块可以创建它自己的作用域,而不是简单的存在于它最近到父级函数作用域或全局作用域下。让我们在认识块作用域是如何工作的之前,先来了解下传统上块里的 JavaScript 是如何工作的:
1 | function fn() { |
var 语句是不能够创建块作用域的,即使是在块里,因此 console.log 语句可以访问到 x 和 y 变量。 fn 函数创建了一个函数作用域而且 x 和 y 变量都是可以通过作用域内的作用域链访问到
声明提升
理解提升的概念是理解 JavaScript 如何工作的基础。JavaScript 有两个阶段:解析阶段(JavaScript 引擎读取所有的代码)、执行阶段(执行已解析的代码)。大多数的事情都发生在第二阶段;例如,当你使用 console.log 语句时,实际的日志消息会在执行阶段打印到控制台
然而,一些重要的事情也会在解析阶段发生,包括变量的内存分配、作用域创建。提升这个术语指的是 JavaScript 引擎在遇到标识符,如变量、函数声明时所发生到事情;当发生声明提升时,它的行为就像是把它定义的字面量提升到当前作用域的顶部。鉴于此,上面到代码示例实际会变成如下情况:
1 | function fn() { |
只有变量到声明会提升到它的作用域的顶部;在这个例子的 if 语句中,变量赋值依然发生在我们所赋值的地方。当然,我们到变量并不会移动,而是引擎行为表现如此,因此这样可以更好的帮助我们理解代码
除了变量,函数声明也会被提升。结果就是,从 JavaScript 引擎到角度来看,代码实际上看起来是这样的:
1 | function fn() { |
innerFn 的声明也被提升到了它的作用域的顶部。但是,记住它仅仅是函数声明被提升了,函数调用没有被提升。上面的代码并不会报任何错,因为 innerFn 在 x 和 y 赋值之前并没有被调用
使用 let
即使使用了 ES2015,var 声明也不会创建块作用域。为了创建块作用域,我们需要在块里使用 let 或 const 声明。我们一会再看 const,首先来看下 let
表面上,let 和 var(我们用它来声明变量)的行为很相似:
1 | function fn() { |
在这个简单的例子中,var 和 let 声明都做了相同的事情(在 fn 创建的作用域下初始化了一个新的变量)。为了创建一个新的块作用域,我们需要在块里使用 let:
1 | function fn() { |
在这个代码示例中,抛出了一个引用错误(reference error);让我们来探索下为什么会这样。fn 函数创建了一个新作用域,里面声明了变量 variable1。然后我们在 if 语句的块里,声明了变量 variable2。然而,因为我们在块里使用了 let 声明,因此一个新的块作用域在 fn 的作用域下被创建了
如果 console.log 语句也在 if 块中的话,那么它就和 variable2 在相同的作用域下了,也能够通过作用域链找到 variable1。但是因为 console.log 在外头,因此它不能访问 variable2,所以会抛出一个引用错误
块作用域和函数作用域的行为相同,但是他们是为块创建的,而不是函数
暂时性死区
当一个用 var 声明的常规变量被创建时,会被提升到它的作用域的顶部,然后并初始化一个 undefined 值,这样就允许我们能够在它赋值之前引用一个常规变量
1 | console.log(x); // undefined |
记住,由于存在声明提升,代码实际看起来是这样的:
1 | var x = undefined; |
这个行为会阻止抛出引用错误 ReferenceError
用 let 声明的变量也被提升了,但重要的是,他们并不会自动初始化值 undefined,因此意味着下面的代码会产生一个错误:
1 | console.log(x); // Uncaught ReferenceError: x is not defined |
这个错误是由暂时性死区(TDZ)引起的。TDZ 存在于作用域初始化到变量声明期间。为了修复这个错误(ReferenceError),我们需要在访问它前声明它:
译者注:TDZ
1 | let x; |
TDZ 这样设计是为了使开发更容易(试图引用一个还没声明的变量通常视为一个错误,而不是故意为之),因此这个错误可以立即提醒我们
使用 const
新的 const 被用来声明一个不可再次赋值的变量。它和 let 的在 TDZ 的行为非常相似,但是,const 变量必须初始化一个值
1 | const VAR1 = 'constant'; |
从现在开始, 变量 VAR1 的值将永远是 “constant” 这个字符串。如果我们试图再次对它赋值,我们会得到一个错误:
TypeError: Assignment to
constantvariable
如果我们试图创建一个没有初始化的 const 变量,我们将看到一个语法错误:
SyntaxError: Missing initializer in
constdeclaration
相似地,一个 const 变量不能被再次声明。如果我们试图再次用 const 声明一个相同变量时,我们将得到一个不同类型的语法错误
SyntaxError: Identifier ‘VAR1′ has already been declared
和其他编程语言一样,常量是被用来保存我们的程序在生命周期里不希望改变的值
记住 let 和 const 都是 JavaScript 的保留词,因此在严格模式下,是不能被用作标识符名称的(变量名,函数名等)。随着 ES2015 越来越普遍,let 和 const 优于 var 已形成一个共识,因为变量创建的作用域更与其他现代编程语言看齐,并且代码的行为也更好预测。
因此,在大多数情况下尽可能的避免使用 var
不可变性
用 const 声明的变量不能被再次赋值的,但是 const 声明的变量并不是完全不可变的。如果我们用对象或数组初始化了一个 const 变量,我们依然可以修改对象的属性和增加删除数组的元素
练习
- 在
for循环里用let来初始化计数器变量 - 修复下面
const的错误:
1 | const VAR1 = 'constant'; |
成功是通过不断的练习和知识的积累,而非智力
- 本文仅代表原作者个人观点,译者不发表任何观点
- Markdown 文件由译者手动整理,如有勘误,欢迎指正
- 译文和原文采用一样协议,侵删