本系列通过阅读 underscore 源码与实战进而体验函数式编程的思想, 而非通过冗长的文字教程, 细读精度 约 1500 行的 underscore 有利于写出耦合度低, 符合函数式编程思想的代码, 并且可以学到 call 与 apply 执行效率的不同进而进行代码性能优化的技巧等.
欢迎大家 star 或者 watch 本系列, 您的关注是作者的最大动力, 让我们一起持续进步. 本系列仓库: https://github.com/zhangxiang958/underscore-analysis
我们在遍历对象的时候往往会使用 for...in 循环得到 object 的键值对,但是其实 for...in 是有兼容性问题的。 for in 循环是有 bug 的, 不过 bug 的触发条件是有限制的. 条件有两个:
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 表示支持吧, 谢谢大家.
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.