消失的var

作者:温荣蛟

相信大部分有接触前端开发的朋友对var关键字非常的熟悉,但是到现代工程中你会发现var关键字正在舍弃,有的甚至使用lint(eslint no-var)工具明确禁用var,var关键字正在消失。

1.var的定义

MDN文档对var的描述是:

变量声明,无论发生在何处,都在执行任何代码之前进行处理。用 var 声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,或者对于声明在任何函数外的变量来说是全局。如果你重新声明一个 JavaScript 变量,它将不会丢失其值。

描述的第一句话提到的就是var的变量提升特性,最后一句提到的是var变量可重复声明的特性。这两个特性给开发者带来便利的同时带来了一些变量取值的异常。如:

var a = 1;
// 其他逻辑代码
function a(){}

console.log(a); // 1

以上代码中,先是声明了a变量,并且赋值为1,接着又声明了a函数。开发者本意是想执行a函数,但在开发中忘记已经对a做过声明,导致a被赋值为1,出现异常。浏览器将以上代码处理为如下

var a;
function a(){}
a = 1;

console.log(a)

2. var的特性与缺陷

我们在 var的定义 部分中已经用一个示例演示了var变量提升和变量重新声明特性及其给开发者带来的困扰。

2.1 变量提升

js解析过程中由于变量声明(以及其他声明)总是在任意代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明。这意味着变量可以在声明之前使用,这个行为叫做“hoisting”。“hoisting”就像是把所有的变量声明移动到函数或者全局代码的开头位置。

var a = 1;
console.log(b); // undefined
var b = 2;
console.log(b); // 2

浏览器会将其解析为

var a;
var b;
a = 1;
console.log(b);
b = 2;
console.log(b);

变量提升看似很美好,开发者可以在同一作用域任意位置声明变量。但是他也带来了一种潜在的风险,如:

var a = 1;

function fn(){
  // 开发者想访问上层作用域中的a变量
  console.log(a);
  // 其他代码逻辑
  // 在这里开发者忘记上方程序有访问a变量,又声明了一个局部变量a
  var a = {}
  // 其他逻辑
}

问题出现了,开发者想要访问的是a的值1,结果实际值是undefined,然而我们调试断点时又不能直观的体现出在函数局部作用域中a已经声明了。

2.2 变量重新声明
使用var关键字声明的变量可以重新声明,并且不会丢失其值

`var a = 1;
var a;
console.log(a); // 1`

重新声明,值仍是1。

同样的,过于宽松的声明规则,带来了一些问题,当然只是重新声明对原变量是没有影响的,只是重新声明之后我们一般都会配置赋值逻辑。这会带来什么问题呢,我们依然看第一节中的例子

var a = 1;
// 其他逻辑代码
function a(){}

console.log(a); // 1

a被重新声明过了,但是开发者没有感知,导致的是a又被赋值成了1。

2.3 没有块作用域

在JS中作用域分为全局作用域和局部作用域,创建局部作用域的方式es5之前只能通过function() {}函数创建,即在es5之前'{}'是不会创建作用域的,这也导致了if/while 等等语句中声明的变量也会提升到作用域顶端

if (0) {
    var a = 1;
}
console.log(a); // undefined

这给开发者带来了巨大的困惑,不可达代码居然能影响程序。

在该示例中浏览器将其处理为

var a;
if (0) {
    a = 1
}
console.log(a);

在JS中使用一个未声明的变量时,浏览器会将其挂载到window对象下。如何证明以上示例是变量提升了而不是挂载到window顶层对象上了呢。我们改动一下示例

"use strict";
 if (0) {
    var a = 1;
}
console.log(a); // undefined

我们开启严格模式(strict mode),我们知道严格模式下是不能使用未声明的变量的。而a变量的值依然是undefined,说明浏览器确实是将条件循环语句中的var声明变量做了声明提升。

{

var a = 1;

}
console.log(a); // 1

3. 替代品 let const

3.1 javascript发展史

我们上面举了很多例子来演示var的特性,也都体验过var可能带来的额外异常。所幸的是,ECMA规范制订者们也在不断的完善与丰富JS语法,下表列举了JS版本的时间节点和主要变更内容

消失的var_第1张图片

作为一个前端开发者对es6的重要性都有深刻的理解。我们看看es6(es2015)带来了些什么。class、extend的引入,和Array Object对象方法的丰富我们暂且抛开不谈。 我们来看看let、const 关键字的引入。

3.2 let

MDN给let的描述是:

let允许你声明一个作用域被限制在 块级中的变量、语句或者表达式。与 var 关键字不同的是, var声明的变量只能是全局或者整个函数块的。

3.21 对比一下var
与var的变量提升相对应的,let存在暂存死区的一个特征,即 通过let 声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致 ReferenceError。该变量处在一个自块顶部到初始化处理的“暂存死区”中。

console.log(a);  // ReferenceError
let a = 1;

let声明的变量会创建块级作用域

{
    let a = 1;
}
console.log(a); // ReferenceError

不能重复声明

let a = 1;
let a = 2;  // Uncaught SyntaxError: Identifier 'a' has already been declared

这就从语法上限制了变量覆盖的可能。

特别提一下,在 switch 语句中只有一个块,会出现以下错误。

let x = 1;
switch(x) {
  case 0:
    let a;
    break;

  case 1:
    let a; // Uncaught SyntaxError: Identifier 'a' has already been declared
    break;
}

使用 {} 块,会创建一个块作用域,就不会出现上述重复声明的问题

let x = 1;
switch(x) {
  case 0: {
    let a;
    break; 
  }
  case 1: {
    let a;
    break;
  }
}

我们回想一下我们之前模块化处理的经典手段,使用一个自执行函数包裹模块代码。基于let语法的块作用域特性,我们处理模块化是不是多了一种直接使用 '{}' 包裹的形式。当然现代前端工程中已经以单文件作为模块划分了,并不需要我们代码自行处理模块化。

3.3 const

此声明创建一个常量,其作用域可以是全局或本地声明的块。 与var变量不同,全局常量不会变为 window 对象的属性。需要一个常数的初始化器;也就是说,您必须在声明的同一语句中指定它的值(这是有道理的,因为以后不能更改)。

const和let的特性非常类似 - const声明的变量引用值不可更改; - const声明变量必须在声明时初始化。

4. 总结
var声明变量提升和可重复声明的特性,很容易出现变量覆盖造成值异常。 let const是一种语法更加严格的设计,从语法本身就做出了约束,避免了变量覆盖的问题,同时也创建了更为安全的块作用域。

消失的var_第2张图片

综上,开发中建议统一使用es6语法,舍弃var关键字的使用。加入no-var规则吧,拥抱let const。

5. 参考文档
VAR描述https://developer.mozilla.org...
LEThttps://developer.mozilla.org...
CONSThttps://developer.mozilla.org...
MDNhttps://developer.mozilla.org...
no-varhttps://eslint.bootcss.com/do...
ES6https://www.w3cschool.cn/ecma...

你可能感兴趣的:(javascript)