去年下半年,红宝书新发布了第四版,删减了一些老旧内容,并一直更新到了 ES2019,传说第三版译者李松峰老师正在翻译中,今年下半年可以出中文版。现在迫不及待地提前学习就只能看英文原版,边学边把部分新增章节翻译下。
下述内容来自 Professional JavaScript for Web Developes,4th Edition(JavaScript 高级程序设计第四版),29 页,VARIABLES(变量)章节。
变量
ECMAScript 变量是松散类型的,所谓松散类型就是可以用来保存任何类型的数据。换句话说,每个变量仅仅是一个用于保存值的占位符而已。声明一个变量时可以使用三种关键词:var(所有 ECMAScript 版本可用)、const 和 let(ECMASCript6 引入)。
var 关键词
使用 var 操作符(注意 var 是一个关键字),后跟一个变量名(即一个标识符),如下所示:
1 | var message; |
这行代码定义了一个名为 message 的变量,该变量可以用来保存任何值(像这样未经过初始化的变量,会保存一个特殊的值——undefined,相关内容在之后章节讨论)。ECMAScript 支持直接初始化变量,因此在定义变量的同时就可以设置变量的值,如下所示:
1 | var message = 'hi'; |
在此,变量 message 中保存了一个字符串值“hi”,像这样初始化变量并不会把它标记为字符串类型,初始化的过程就是给变量赋一个值那么简单。因此,可以在修改变量值的同时修改值的类型,如下所示:
1 | var message = 'hi'; |
在这个例子中,变量 message 一开始保存了一个字符串值“hi”,然后该值又被一个数字值 100 取代。虽然我们不建议修改变量所保存值的类型,但这种操作在 ECMAScript 中完全有效。
声明作用域
有一点必须注意,即用 var 操作符定义的变量将成为定义该变量的函数作用域中的局部变量。也就是说,如果在函数中使用 var 定义一个变量,那么这个变量在函数退出后就会被销毁,,例如:
1 | function test() { |
这里,变量 message 是在函数中使用 var 定义的。当函数被调用时,就会创建该变量并为其赋值。在此之后,这个变量又会立即被销毁,因此例子中的下一行代码就会导致错误。不过,可以像下面这样省略 var 操作符,从而创建一个全局变量:
1 | function test() { |
这个例子省略了 var 操作符,因而 message 就成了全局变量。这样,只要调用过一次 test()函数,这个变量就有了定义,就可以在函数外部的任何地方被访问到。
注意:虽然省略 var 操作符可以定义全局变量,但这也不是我们推荐的做法。因为在局部作用域中定义的全局变量很难维护,而且如果有意地忽略了 var 操作符,也会由于相应变量不会马上就有定义而导致不必要的混乱。给未经声明的变量赋值在严格模式下会导致抛出 ReferenceError 错误。
可以使用一条语句定义多个变量,只要像下面这样把每个变量(初始化或不初始化均可)用逗号分隔开即可:
1 | var message = 'hi', |
这个例子定义并初始化了 3 个变量。同样由于 ECMAScript 是松散类型的,因而使用不同类型初始化变量的操作可以放在一条语句中来完成。虽然代码里的换行和变量缩进不是必需的,但这样做可以提升可读性。
在严格模式下,不能定义名为 eval 或 arguments 的变量,否则会导致语法错误。
var 声明提升
当使用 var 时,由于变量声明会被提升到函数作用域的顶部,所以下述代码是可行的:
1 | function foo() { |
由于 ECMAScript 运行时会按照下述的逻辑来执行,所以不会报错:
1 | function foo() { |
解释器会把所有的变量声明拉到所在定义域的顶部,这就叫声明提升。这也允许你使用多个 var 来声明同一个变量而不会报错。
1 | function foo() { |
let 声明
let 操作符的使用与 var 类似,但也有一些重要的不同。最需要注意的就是在声明时 let 是块级作用域,而 var 是函数作用域。
1 | if (true) { |
在这里,因为 let 声明变量的作用域不会延伸到块的外部,所以使用 let 声明的变量不能在 if 代码块外引用。块级作用域是函数作用域的严格子集,所以 var 声明时的任何限制同样也适用于 let 声明。
在块级作用域内,不允许出现对同一个变量的多次 let 声明,否则会导致如下错误。
1 | var name; |
当然,JavaScript 引擎将会持续追踪变量声明时所使用的标识符和所在的块级作用域,所以在嵌套内使用相同的标识符时,不会当成重复声明而报错。
1 | var name = 'Nicholas'; |
重复声明的错误不是一个有序函数,不会被 if 和 var 混用影响。不同的关键词不会声明出不同的变量类型——只会影响到与变量相关的作用域。
1 | var name; |
暂时性死区
let 和 var 的另外一个重要的不同点就是 let 不会声明提升:
1 | // name被提升 |
当解析代码时,JavaScript 引擎仍然能提前识别出代码块中后出现的 let 声明,但是无论如何都不能在真正使用 let 声明前使用变量。在声明前执行的片段被称作为“暂时性死区(TDZ)”,任何试图去引用变量的操作都会抛出 ReferenceError 错误。
全局声明
与 var 关键词不同,当在全局环境中使用 let 时,变量不会附加到 window 对象中。
1 | var name = 'Matt'; |
无论如何,在页面的生命周期内,使用 let 在全局块级作用域中声明的变量一直存在。因此为了避免抛出 SyntaxError 的错误,必须确定你的页面不会再试图声明该变量。
条件声明
当使用 var 声明变量时,由于声明提升,JavaScript 引擎会将多余的多个声明组合成一个声明并放在函数作用域顶部。但由于 let 声明在块级作用域内,所以检查之前变量是否被声明过并条件性的声明变量是不可行的。
1 | <script> |
由于 let 声明会被包裹在条件块代码中,所以使用 try/catch 语句或者 typeof 操作符也解决不了问题。
1 | <script> |
由于上述原因,在使用 ES6 声明关键词时不能依赖于条件声明。
注意:不能使用 let 进行条件声明是有积极作用的,在代码中使用条件声明不是一个好习惯。条件声明会使代码流更难理解,如果你发现自己经常使用条件声明,ES6 是非常好的改变机会,你会有更好的方式去实现代码。
循环中的 let 声明
在 let 语法出现之前,for 循环中的迭代变量会溢出到循环体外部:
1 | for (var i = 0; i < 5; ++i) { |
切换到 let 声明后,迭代变量只会作用到 for 循环体的作用域内部:
1 | for (let i = 0; i < 5; ++i) { |
使用 var 声明时,经常会遇到迭代变量单一声明和修改的问题:
1 | for (var i = 0; i < 5; ++i) { |
发生的原因是循环结束的时候,迭代变量仍然被设为导致循环结束的值:5。当 timeout 稍后执行时,所有的 timeout 会引用相同的值,因此打印出来全是最终值。
当使用 let 声明迭代变量时,Javascript 引擎会真正的给每个循环体声明一个迭代变量。每一个 setTimeOut 会引用一个单独的实例,因此将会打印出期望输出的结果:当循环执行时使用当前循环的迭代变量。
1 | for (let i = 0; i < 5; ++i) { |
这个每次迭代单独声明的行为对所有风格的 for 循环都适用,包括 for-in 和 for-of 循环。
const 声明
const 的表现与 let 大部分是相同的,除了非常重要的一点区别——const 声明的变量在声明之后不能重新赋值,否则会导致运行时错误。
1 | const age = 26; |
const 声明只会强制指向的变量的引用。如果一个 const 变量引用了一个对象,那么修改对象内部的属性不会违反 const 的限制。
1 | const person = {}; |
在 for 循环中,尽管 JavaScript 引擎会对 let 声明的迭代变量创建一个新的实例,并且 const 变量和 let 变量的表现相似,但在循环体中并不能使用 const 来声明。
1 | for (const i = 0; i < 10; ++i) {} // TypeError: assignment to constant variable |
然而,如果你想声明一个不会改变的迭代变量,const 是可以使用的——这恰好是因为每次迭代都声明了一个新变量。这对 for-of 和 for-in 循环的情况是非常重要的:
1 | let i = 0; |
声明风格和最佳实践
ECMAScript6 中 let 和 const 的引入提高了声明作用域和语法的精确度,从客观上为该语言带来了更好的工具。众所周知,由于 var 声明的异常行为导致 JavaScript 的问题数不胜数。在引入这些新关键字之后,出现了一些越来越常见的可以提高代码质量的模式。
不使用 var
在使用 let 和 const 之后,大部分的开发者可以发现在自己代码的任何地方都将不再需要 var。将变量声明限制为仅让 let 和 const 出现的模式,由于对变量作用域,声明局部性和 const 正确使用的细心管理,将有助于提高代码库质量。
优先使用 const
使用 const 声明允许浏览器运行时可以强制一个变量为常量,还可以使用静态代码分析工具来预见非法的赋值操作。因此,许多开发者认为默认情况下将变量声明为 const 是最合适的,除非他们明确知道需要在某个时候重新赋值。这使开发者可以更具体地推断出永远不会改变的值,并在代码执行试图执行重新赋值的情况下快速检测错误行为。