关于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》中,作者提到:

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

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

你可能感兴趣的:(关于for...in循环、var、块作用域引发的思考)