关于for...in循环、var、块作用域引发的思考

某天,公司代码审查会的时候,从某同事的代码里发现这样的代码:

for(var i in this.pointObject){
    if(i == editCircle.id){
        for(var i in this.pointObject){
            if(editCircle.overlay.id == this.pointObject[i].shapeId){
                this.$emit("editShape", this.pointObject[i][editCircle.overlay.id]);
                this.editRangeCircle(editCircle,1);
            }
        }
    }
}

这段代码的运行从实现的功能来看并不会出现bug、但是从业务逻辑与代码可读性、js的语法问题、执行性能方面来看,都颇具有槽点,于是,提出了修改要求,但是同事说:“这样子写没事,块作用域。”。看他那坚定的态度,以及各种原因,有一瞬间,我甚至怀疑了自己对js的var声明与块作用域的理解,虽然很快就打消了,但是我还是决定自己验证一遍、顺便做个完整、说服力的JavaScript语法科普。以下的内容会将这块代码称为代码A;

  • 关于块作用域
    我们知道块作用域是作用于{// 代码块}这样的代码块中的作用域,使用过java、c、C#灯语言的小伙伴肯定不陌生,但是早期的javaScript并不具备有块作用域,只有全局作用域、函数作用域,在ECMAScript 6后才新增了块作用域的实现,并且var定义的变量,是没有块的概念的,他是可以跨块访问,此处我们以这样的代码块来证明这一结论:
var i = 3;
for(var i = 0; i < 5 i++) {}
console.log(i); // 输出5

如果此处for中声明的i具有块作用域,此时输出的i的值不应该为5,而应该是3,所以很明显的,for循环中的i泄露到了全局作用域。有的小伙伴可能会问,为什么会这样呢,这就涉及到javaScript的编译原理了。简而言之,js在运行前,会将用var声明的变量进行提升,所以即使先输出后声明,也只是会输出undefined并不会报错。具体的编译顺序可以参考《你不知道的javaScript·上》第一章的内容。
此处我们回到开头,代码A中两个for循环都用i来进行循环,是会发生变量泄露的,不信看以下的例子

for(var i = 0; i < 5; i++) {
    if(i == 3) {
        for(var i = 0;i < 5; i++) {
        }
    }
    console.log(i); // 输出结果为 0、1、2、5
}

很明显的,第二个for循环的i污染了第一个for循环的i,导致输出的结果为0、1、2、5,如果没有发生变量泄露我们输出的结果应该是0、1、2、3、4,不信的小伙伴可以用let来进行测试

for(var i = 0; i < 5; i++) {
    if(i == 3) {
        for(let i = 0;i < 5; i++) {
        }
    }
    console.log(i); // 输出结果为0、1、2、3、4
}

综上所述,我们可以知道,代码A所声明的var i并不具备块作用域,是存在变量泄露的问题的,但是细心的伙伴会说,代码A的循环是使用 for...in...,并不是使用普通的for循环,所以我在证明、测试的时候也使用了 for...in...,测试代码如下

obj = {1: 'a',2: 'b',3: 'c',4: 'd',5: 'e'};

for(var i in obj) {
    if(i == 3) {
        for(i in obj) {
            console.log(i)
        }
        
    }
    console.log(i, "?");
}
// 输出结果如下
// 1 ?   2 ?   1   2   3   4   5   5 ?   4 ?   5 ?

输出的结果可以说是在意料之中、也在意料之外(涉及到了for...in循环的不同)。
从输出结果来看,我们可以看到在第二层for里输出了1、2、3、4、5之后,外部的console.log输出了5 ?,所以很明显的此时第二层循环中的var i变量是不具备块作用域的,他泄露到了外部。所以这里的结论与之前的结论都是一样的:

var声明的变量不具备块作用域的特性!

但是细心的小伙伴会发现,此时在输出5 ?之后,外面的循环并没有结束,而是继续进行循环,输出了4 ? 5 ?,这个就是涉及到了我上面提到关于for...in...循环的不同

  • for...in...循环
for (variable in object)
  statement

MDN中提到,for...in...循环是以任意顺序遍历一个对象的除Symbol以外的可枚举属性,在每次迭代时,variable会被赋值为不同的属性名。这句话以及上面的现象,我们可以获得一个结论,在variable in object这句代码中,variable是我们外部循环所声明的变量,但是for...in...内部是有自己的作用域的,所以即使我们在循环内部修改了值,并不会影响for...in...循环的循环次数以及某种顺序。for...in...会取出对象的所有可枚举的属性名(除了Symbol以外),每次循环将其中一个属性名赋值给变量variable ,从而运行循环体。

for(var i in this.pointObject){
    if(i == editCircle.id){
        for(var i in this.pointObject){
            if(editCircle.overlay.id == this.pointObject[i].shapeId){
                this.$emit("editShape", this.pointObject[i][editCircle.overlay.id]);
                this.editRangeCircle(editCircle,1);
            }
        }
    }
}

最后我们在根据上述的js基础对代码A进行总结,在代码A中,第一层循环会在i == editCircle.id时进行第二层循环,此时i在第二层for...in循环时,依旧是从this.pointObject里的第一个属性名开始进行遍历的,在第二层for...in循环结束后,代码A并不会结束循环,而是继续第一层的循环,直到结束。所以在此处第一层循环显得冗余、多余、是不必要的循环。
从代码的可读性、逻辑、性能方面出发,这段代码其实可以简化为

if(this.pointObject.hasOwnProperty(editCircle.id)) {
    for(var i in this.pointObject) {
        if(editCircle.overlay.id == this.pointObject[i].shapeId){
            this.$emit("editShape", this.pointObject[i][editCircle.overlay.id]);
            this.editRangeCircle(editCircle,1);
        }
    }
}

这样我们能从代码很快的理解到,当this.pointObject中含有editCircle.id值的属性名时,我们才需要循环变量,从而做一系列的处理。
在《你不知道的javascript》中,作者提到:

不满足于只是让代码正常工作,而是要弄清楚“为什么”。

在我的思想里,团队工作中,代码审查应该重点关注“我需要完全理解这部分代码才能确保它能够正常工作,如果由我来修复代码中的问题,我是不会这么写的,因此希望你也不要这么来写”。所以才有了这篇文章。使代码具有健壮性、简洁性、可读性、以及更好的性能,我觉得是每个程序员应该有的态度与目标。

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