Javascript之var、let、const与块级作用域

最近看到一个关于js中的let关键字帖子:JavaScript let 关键字问题求解 ,说的是下面一段代码:

function clickImageIcon(msgArr, options) {
    for (var i = 0; i < msgArr.length; i ++) {
        $('.file-wrapper:eq(' + i + ')').bind('click', function () {
            recognitionContent(msgArr[i]);
            $('#myModal').modal(options);
        });
    }
}

代码的目的是使用循环为多个元素帮点点击事件,但是测试发现并没有按照预想的那样正确的为元素绑定事件。为什么呢?

别急,先来看这段代码

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

在Chrome网页中的console中运行这段js代码,你猜运行结果是啥?答案是1。
作为一个从事C系语言编程的人,看到这个结果还是蛮惊异的,原来你竟是这样的var——原因其实也很简单,js中var 是函数级的,无视内部大括号。
更深层的原因是缘于JavaScript引擎的工作方式:先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。

用闭包可以解决本文开头代码的问题:

function clickImageIcon(msgArr, options) {
    for (var i = 0; i < msgArr.length; i ++) {
        (function(index) {
            $('.file-wrapper:eq(' + index + ')').bind('click', function () {
                recognitionContent(msgArr[index]);
                $('#myModal').modal(options);
            });
        })(i);
    }
}

解释:由于运行时闭包的存在,该匿名函数中定义的变量(包括参数表)在它内部匿名函数执行完毕之前都不会释放,因此我们在其中访问到的 i 就分别是不同的闭包实例,这个实例是在循环体执行的过程中创建的,保留了不同的值。

但是有一个很巧妙的解决办法是把var改成let:

function clickImageIcon(msgArr, options) {
    for (let i = 0; i < msgArr.length; i ++) {
        $('.file-wrapper:eq(' + i + ')').bind('click', function () {
            recognitionContent(msgArr[i]);
            $('#myModal').modal(options);
        });
    }
}

这是为啥呢?这就要提到ES6中引入的let的特点了:

  • let声明的变量拥有块级作用域。
    也就是说用let声明的变量的作用域只是外层块,而不是整个外层函数。let 声明仍然保留了提升特性,但不会盲目提升,在示例一中,通过将var替换为let可以快速修复问题,如果你处处使用let进行声明,就不会遇到类似的bug。
  • let声明的全局变量不是全局对象的属性。
    这就意味着,你不可以通过window.变量名的方式访问这些变量。它们只存在于一个不可见的块的作用域中,这个块理论上是Web页面中运行的所有JS代码的外层块。
  • 形如for (let x...)的循环在每次迭代时都为x创建新的绑定。
    这是一个非常微妙的区别,拿示例二来说,如果一个for (let...)循环执行多次并且循环保持了一个闭包,那么每个闭包将捕捉一个循环变量的不同值作为副本,而不是所有闭包都捕捉循环变量的同一个值。
    所以示例中,也以通过将var替换为let修复bug。
    这种情况适用于现有的三种循环方式:for-of、for-in、以及传统的用分号分隔的类C循环。
    用let重定义变量会抛出一个语法错误(SyntaxError)。这个很好理解,跟C系语言变量定义一致。

等一等——形如for (let x...)的循环在每次迭代时都为x创建新的绑定——这句话怎么看都有点疑惑,为啥会这样?C/Objective-C中的变量作用域虽然并没有像var那样不堪,但也没有for循环中的这个let效果,你们不好奇它是怎么做到的吗?

我们来换一段代码试试:

for (let i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
    let i; //你猜这里会不会报错?
    console.log("in for block", i);
}

运行一下,你会发现并不报错。运行结果是:

in for block undefined
in for expression 0
in for block undefined
in for expression 1
in for block undefined
in for expression 2

好像真是天大的发现呢,大括号里的let i不会报错,说明大括号里的let作用域并不是for循环的let子作用域?

我们来看看把上面的代码进行ES6 to ES5转换后的代码:

for (var i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
    var i = void 0; //你猜这里会不会报错?
    var s;
    console.log("in for block", i);
}

是不是大失所望,只是简单地把let换成var而已?非也,这只是因为内部没有异步调用。
现在我们往for循环里加入一个异步调用的延时函数:

for (let i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
    let i; //你猜这里会不会报错?
    setTimeout(function(){console.log(i)})
}

转换为ES5的结果是:

var _loop = function _loop(i) {
    var i = void 0; //你猜这里会不会报错?
    setTimeout(function () {
        console.log(i);
    });
};

for (var i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
    _loop(i);
}

可以看到for循环大括号的内容被提取到一个单独的_loop函数里去了。
简单的理解就是:

  1. 所有的let声明最终还是var声明
  2. 带let声明的for循环中如果有异步调用,则大括号内容最终会提取为一个单独的函数

对于本文第二段代码,如果把var b = 1改成let b = 1,进行ES6 to ES5转换后的代码:

var a = true;
if (a) {
    var _b = 1;
}
console.log(b);

只不过是很简单的变量重命名。

至于const,则是更特殊的let——即不可修改的let变量声明。

参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343