underscore 源码阅读系列 -- for...in 循环的兼容性问题

本系列通过阅读 underscore 源码与实战进而体验函数式编程的思想, 而非通过冗长的文字教程, 细读精度
约 1500 行的 underscore 有利于写出耦合度低, 符合函数式编程思想的代码, 并且可以学到 call 与 apply 执行效率的不同进而进行代码性能优化的技巧等.

欢迎大家 star 或者 watch 本系列, 您的关注是作者的最大动力, 让我们一起持续进步.
本系列仓库: github.com/zhangxiang9…

兼容 for...in 循环

我们在遍历对象的时候往往会使用 for...in 循环得到 object 的键值对,但是其实 for...in 是有兼容性问题的。
for in 循环是有 bug 的, 不过 bug 的触发条件是有限制的.
条件有两个:

  1. 需要在 IE9 以下的浏览器
  2. 被枚举的对象被重写了一些不可枚举属性.
    下面这段代码是用来兼容 for in 中的 bug 的。在 underscore 里面很少会使用 for..in 来遍历对象, 作者通过 .keys 方法加上 for 循环代替了 for...in, 但是还是无法避免地在 .keys 中使用了 for...in 循环:

    if (hasEnumBug) collectNonEnumProps(obj, keys);

    上面这段代码就是在 _.keys 函数内部中的 for...in 循环中的一句代码, 判断是否有兼容问题, 若有则做兼容处理。
    下面是兼容的详细代码:

    var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
    var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
                       'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
    
    var collectNonEnumProps = function(obj, keys) {
     var nonEnumIdx = nonEnumerableProps.length;
     var constructor = obj.constructor;
     var proto = _.isFunction(constructor) && constructor.prototype || ObjProto;
    
     // Constructor is a special case.
     var prop = 'constructor';
     if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
    
     while (nonEnumIdx--) {
       prop = nonEnumerableProps[nonEnumIdx];
       if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
         keys.push(prop);
       }
     }
    };

    这段代码就是用来兼容 bug 的.

    var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');

    第一: hasEnumBug 是用来判断有没有 for in bug 的, 因为重写了Object 原型中的 toString 属性,对于有兼容问题的浏览器而言,是打印不出对象中 toString 属性的值的, 因为 toString 属性在那些有问题的浏览器中属于不可重写的不可枚举属性,所以根据这个来判断是否有兼容问题。
    然后. 核心在于在 collectNonEnumProps 函数, 第一先确定哪些属性名在低版本浏览器是不可枚举的, 分别是 valueOf, isPrototypeOf, toString, propertyIsEnumberable, hasOwnProperty toLocalString 这几个方法, 当然还有 constructor, 那为什么 constructor 属性需要独立抽取出来做特殊判断呢?

    var constructor = obj.constructor;
     var proto = _.isFunction(constructor) && constructor.prototype || ObjProto;
    
     // Constructor is a special case.
     var prop = 'constructor';
     if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);

    因为 constructor 属性有其特殊性,因为对于 toString 那些属性而言, 判断是否需要加入到 keys 数组中是根据 obj[prop0] !== proto[prop] 来判断的, 不一样说明重写了,说明这个属性是用户新定义的属性,需要作为一个数据属性名放到 keys 数组中去,但是 constructor 不能做这个判断,不能单纯使用 === 来判断是否被重写,因为对象在继承的时候 constructor 函数通常都不会等于 Object, 所以 constructor 需要被提出来单独判断。
    在函数最后,得到不可枚举的属性名列表接下来就好办了, 就是判断对象里面的和原型链里面的同属姓名的值相不相同, 不相同就是重写了, 将这个属性名加到 keys 数组里面, 如果相同就不加. 这就是为什么一进入函数就需要存储 obj.prototype.
    之前有人说说循环里面 prop in obj 多余, 这个并不是, 可以试试: 下面是我在 chrome 浏览器做的测试代码

    var obj = Object.create(null);
    'toString' in obj;  // false

    这样是 false 的, underscore 为了避免这样的情况发生, 添加了判断.
    最后说一下感受, for in 循环到底有没有必要进行兼容, 不兼容的危害大不大? 我认为并不大, 但是作为一个类库, 代码的严谨性是需要的, 但是我们在平时使用 for in 循环的时候有没有必要担惊受怕呢?
    其实没有必要的, 我们如果不是特别需要, IE9 以下的兼容情况会越来越少, 而且一般来说我们也不会在对象里面去覆盖像 toString 这样的原型属性.

如果觉得有收获, 请到 github 给作者一个 star 表示支持吧, 谢谢大家.

相关推荐