JS的奇妙旅程:var与let

当初Brendan Eich发明JavaScript的时候,只用了10天,比上帝创造人类差不了多少。在如此短的时间内创造一门新的语言,设计上的缺陷无可避免(当然其他语言或多或少都会有)。其中一个就是声明变量的关键字var
var用于声明一个变量,该变量的作用范围是整个函数范围内,或者是全局的。例如如下语句:

// case 1 
function func() {
    var v = 'hello world';
    if (true) {
        // do something
    }
    console.log(v);
}
// case 2
function func() {
    if (true) {
        // do something
        var v = 'hello world';
    }
    console.log(v);
}

从结果上,以上两个函数的运行结果是完全一致的。因为存在吊升(hoisting),不管var在哪里声明一个变量,该变量声明都会优先于其他代码被处理,因此在声明前引用该变量是完全没问题的,不会引发任何异常,只不过结果不是我们所预期的。不过值得一提的是,虽然存在hoisting,但是变量真正赋值依旧是要等待代码执行到它所在的位置。例如上面的例子case 2中,如果在声明变量v之前使用它,会得到undefined
正是由于这个特性的存在,可能会引发一些意想不到的问题。请看大屏幕:

class clazz {
  show() {
    console.log('hello there!');
  }
}

function func() {
var v = new clazz();
  if (true) {

    // 一不小心再次定义的 v
     var v = 'oh no...'
  }
  v.show();
}

此时,程序已经无法给你想要的结果。
再举个栗子:

var messages = ["Am", "I", "a", "bug?"];

for (var i = 0; i < messages.length; i++) {
  setTimeout(function () {
    console.log(messages[i]);
  }, i * 1500);
}

我们的初衷是延迟1.5秒后分别依次输出Am I a bug?这四个单词。但实际结果却是四个undefined。为什么会这样?注意for循环中var声明的变量i,实际上,由于是var声明的,所以整个循环中只有一个i,这同一个ifor循环以及三个回调函数共用。因此当循环结束时,其实i=4,而messages[4]显然已经超出了原本数组的范围,因此显示undefined。那么我们要怎么解决这个问题呢?好了,为了解决这个问题,关键先生登场,它就是let关键字。只要把上面的for循环中var换成let,问题就解决了。
let是何方神圣,它是谁?它从哪里来?它能干什么?这些问题很好回答,简单一句话:let是新的var

十年前,JavaScript的作者Brendan Eich打算修复var存在的毛病(例如上面所举的例子),但是由于要兼容历史遗留的代码,这些缺陷一旦存在就几乎不可能再被修复,毕竟让已经在网上存在已久的一大堆代码瞬间出毛病,是个开发者都要拿起菜刀拼命。因此,要修复var的缺陷只有一个办法,原封不动的保留var的语义,而引入另外一个新的关键字作为它的替代,这个关键字就是let
那么究竟他们俩有什么不同呢?莫急,听我慢慢道来。

  1. let 的作用范围只在它声明的代码块内,也就是说,出了声明的代码块再去引用它就属于违规了,这一点就和java等类似了;而var 声明的变量,var定义的变量是全局的,或者是在整个函数中生效的,不管他在那里被声明的,都会先于其他任何语句被处理,这个称之为“吊装(hoisting)”。虽然let也存在hoisting,但是并没有被滥用,它只被吊升到声明该变量的代码块顶部,而不是整个函数的顶部。
function varTest() {
  var x = 1;
  if (true) {
    var x = 2;  // same variable!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}

function letTest() {
  let x = 1;
  if (true) {
    let x = 2;  // different variable
    console.log(x);  // 2
  }
  console.log(x);  // 1
}
  1. let定义的的全局变量并不是全局对象的一个属性,因此你并不能通过window.variableName的方式去访问一个let定义的变量variableName
  2. let声明的变量在吊升时不会被初始化成默认值;var声明的变量会初始化成undefined
  3. for (let i=0, ...)的形式在每次迭代的时候都会创造一个新的变量i的副本,并不是所有迭代共享一个。因此可以解决我们上面的例子存在的问题。
  4. var声明的变量可以在再次被声明,但是在let声明的变量的函数或者是代码块内再次声明该变量会抛出SyntaxError。如:
let x = 1;
switch(x) {
  case 0:
    let foo;
    break;
    
  case 1:
    let foo; // SyntaxError for redeclaration.
    break;
}
  1. 对于var声明的变量,声明之前的地方使用得到的值是undefined,因为吊升(hoisting)的时候给他初始换成undefined。但是let声明的变量虽然也有吊升,但却不会被初始化,直到执行声明的地方才会被初始化成明确的值,因此在声明的地方之前使用let声明的变量会引发ReferenceError。在代码块开始到let声明的变量中间的这段,称之为“时间死区(temporal dead zone)”。
  2. 在“时间死区”内使用typeof,对于let声明的变量,会抛出ReferenceError。如:
// prints out 'undefined'
console.log(typeof undeclaredVariable);
// results in a 'ReferenceError'
console.log(typeof i);
let i = 10;

可以说,能用var的地方都可以用let来代替。当然这是指新开发的情况下,对于历史遗留的代码,你并不能通过简单的查找替换全部的var来解决,因为可能你的代码的正常运行,正是依赖的var的一些古怪的特性。let出现,对于既有代码已经于事无补,它的作用是让你今后开发的时候,能够尽量远离一些var引入的一些稀奇古怪的坑。
以上就是varlet相爱相杀的故事。

bonus time

用于定义变量的关键字其实还有另外一个,const,它定义的变量基本和let一样,唯一不同的,就是它的变量不能被再次赋值,也即是说,过了过了这村儿没这店儿,你只有在声明的时候顺路赋值那一次机会。


本文首发于个人公众号TensorBoy。如果你觉得内容还不错,欢迎分享并关注我的公众号TensorBoy,扫描下方二维码获取更多精彩原创内容!

公众号二维码

发布了45 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ZM_Yang/article/details/86240126