@gyyin
2018-03-16T13:53:15.000000Z
字数 3994
阅读 334
这篇文章主要分析underscore中.keys以及相关方法的源码,着重分析.keys内部的collectNonEnumProps方法。
在看源码前,我们先了解keys的用法。keys和原生的Object.keys的用法一样,都是返回一个以对象本身属性名(不包括原型上的)为集合的数组,如果是重写了原型上面的属性,也会返回。
我们再来看一下keys的源码:
_.keys = function(obj) {
// 如果不是对象,则直接返回空数组
if (!_.isObject(obj)) return [];
// 如果存在Object.keys方法,则直接调用
if (nativeKeys) return nativeKeys(obj);
var keys = [];
// 遍历对象,并将只存在对象本身的属性push到数组中
for (var key in obj)
// has方法来判断属性是否在对象上
if (_.has(obj, key)) keys.push(key);
// ie9以下会有一个bug,待会儿细讲。
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};
keys方法大致这这么多,这里我先讲has方法。has方法是用来判断对象自身是否具有某个属性,这里可以看一下has的源码:
_.has = function(obj, key) {
// hasOwnProperty === Object.prototype.hasOwnProperty
return obj != null && hasOwnProperty.call(obj, key);
};
这里使用了Object原型上的hasOwnProperty方法,也许你会好奇,为什么不直接用hasOwnProperty来判断?因为hasOwnProperty方法可能会被重写覆盖掉,所以用hasOwnProperty在一开始就保存了一份Object.prototype.hasOwnProperty的引用,这样就不会出现被覆盖的情况了。
看完了has方法,我们来重点讲collectNonEnumProps这个方法。这里我们不得不先说一下for in循环。
我们都知道,for in循环只遍历可枚举属性。像使用像Object内置构造函数所创建的对象都会继承来自Object.prototype的不可枚举属性。
我们前面说过keys返回对象的属性名集合,但是不包括原型上的,除非你在对象自身上写了一个和原型上名字一模一样的属性。但是在ie9以下浏览器中会出现一个bug,那就是Object原型上的包括['valueOf', 'isPrototypeOf','toString','propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']在内的6个方法,即使在对象上写了一模一样的方法,也不会被for in遍历到。
// 即使toString在obj上,但在ie9以下的浏览器依然无法用for in访问到
var obj = {toString: "hello"}
for (var k in obj) console.log(k); // 什么都不会打印
我们先搬出源码,简单的注释一下:
/* 这是一个重写过toString方法的对象,使用propertyIsEnumerable方法来判断他的toString方法是否可以枚举,如果为false,则存在上面的枚举bug
*/
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
// 列举出会出现enumBug的几个原型方法
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString','propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
// 获取obj的constructor,如果constructor没有被重写过,那应该就是Object这个构造函数,实际上obj.constructor === Object.prototype.constructor
var constructor = obj.constructor;
// 如果constructor被重写过,重写后的constructor不是function或者constructor不存在prototype,那么会返回Object.prototype,否则就直接返回constructor.prototype
var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
// 这里对constructor进行单独处理
var prop = 'constructor';
// 如果obj自身没有constructor属性,并且keys数组中不存在constructor
if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
// 对其他几个方法进行处理
while (nonEnumIdx--) {
prop = nonEnumerableProps[nonEnumIdx];
/* 判断当前prop是否在obj上(in是用来判断属性是否在对象上或者对象原型上),并且比较obj[prop]和proto[prop]是否相等(一般来说如果对应方法被重写了,那么obj[prop]访问的是重写后的方法,而proto[pros]访问的是原型上的方法,所以obj[prop]以一定不会和proto[prop]相等)
*/
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
keys.push(prop);
}
}
}
可能上面注释不是很详细,我在这里逐条解释一下。
首先是hasEnumBug这个属性,我们知道propertyIsEnumerable是判断对象上的某个属性是否可被枚举的,当然这个在ie9以下的浏览器中也会出现for in上面的那个bug,所以这里可以用一个重写过toString方法的对象来判断toString方法是否可枚举,如果返回了false,那么就说明浏览器中存在这个bug。
这里可能大家会有两个疑问,一个是为什么constructor要单独用_.has处理?直接放到while里面和大家一起处理不行吗?
我自己的理解是,可能是为了防止出现下面这种情况。
// 用Object重写了constructor
var obj = {constructor: Object}
如果没有重写,obj.proto.constructor原本就是指向Object构造函数的,现在重写后其实根本没有改变,那么obj[prop] !== proto[prop]这个返回的肯定是true,但是constructor却是实实在在在obj上被重写过了,所以这里对constructor用_.has来处理。
另一个疑问则是prop in obj && obj[prop] !== proto[prop]中为什么还有prop in obj这一句?难道这个不是一定会返回true吗?当前的prop不是一定会存在于原型上?
同样是我自己的理解,我觉得可能会出现一种情况,那就是如果obj的原型被修改了,那么原型上就不会有toString等方法了,比如:
obj.constructor.prototype = null;
console.log("toString" in obj); // false
写到这里,其实我也有一个疑问,那就是为什么不全部都用_.has来判断,反而将toString等六个属性用prop in obj && obj[prop] !== proto[prop]来判断?prop in obj && obj[prop] !== proto[prop]判断一定可靠吗?
看下面这个例子:
var toString = Object.prototype.toString
var obj = {toString: toString}
这种场景类似上面的constructor,因为obj[prop] !== proto[prop]必定会返回false,所以其他几种方法也有我刚说的constructor类似的情况,所以到这里我对自己前面讲的constructor单独处理的原因产生质疑,既然大家都会遇到这种问题,为什么不都直接用_.has来处理呢?
按照我自己的理解,这里我重写一下collectNonEnumProps方法,如下:
var nonEnumerableProps = ['valueOf', 'isPrototypeOf','toString','propertyIsEnumerable', 'hasOwnProperty', 'constructor' 'toLocaleString'];
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var prop;
while (nonEnumIdx--) {
prop = nonEnumerableProps[nonEnumIdx];
if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
}
}
以上都是个人理解,如果理解有偏差,望指出。