[关闭]
@gyyin 2018-03-16T13:53:15.000000Z 字数 3994 阅读 334

underscore源码分析之_.keys

这篇文章主要分析underscore中.keys以及相关方法的源码,着重分析.keys内部的collectNonEnumProps方法。

在看源码前,我们先了解keys的用法。keys和原生的Object.keys的用法一样,都是返回一个以对象本身属性名(不包括原型上的)为集合的数组,如果是重写了原型上面的属性,也会返回。

我们再来看一下keys的源码:

  1. _.keys = function(obj) {
  2. // 如果不是对象,则直接返回空数组
  3. if (!_.isObject(obj)) return [];
  4. // 如果存在Object.keys方法,则直接调用
  5. if (nativeKeys) return nativeKeys(obj);
  6. var keys = [];
  7. // 遍历对象,并将只存在对象本身的属性push到数组中
  8. for (var key in obj)
  9. // has方法来判断属性是否在对象上
  10. if (_.has(obj, key)) keys.push(key);
  11. // ie9以下会有一个bug,待会儿细讲。
  12. if (hasEnumBug) collectNonEnumProps(obj, keys);
  13. return keys;
  14. };

_.has

keys方法大致这这么多,这里我先讲has方法。has方法是用来判断对象自身是否具有某个属性,这里可以看一下has的源码:

  1. _.has = function(obj, key) {
  2. // hasOwnProperty === Object.prototype.hasOwnProperty
  3. return obj != null && hasOwnProperty.call(obj, key);
  4. };

这里使用了Object原型上的hasOwnProperty方法,也许你会好奇,为什么不直接用hasOwnProperty来判断?因为hasOwnProperty方法可能会被重写覆盖掉,所以用hasOwnProperty在一开始就保存了一份Object.prototype.hasOwnProperty的引用,这样就不会出现被覆盖的情况了。

collectNonEnumProps

看完了has方法,我们来重点讲collectNonEnumProps这个方法。这里我们不得不先说一下for in循环。
我们都知道,for in循环只遍历可枚举属性。像使用像Object内置构造函数所创建的对象都会继承来自Object.prototype的不可枚举属性。
我们前面说过keys返回对象的属性名集合,但是不包括原型上的,除非你在对象自身上写了一个和原型上名字一模一样的属性。但是在ie9以下浏览器中会出现一个bug,那就是Object原型上的包括['valueOf', 'isPrototypeOf','toString','propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']在内的6个方法,即使在对象上写了一模一样的方法,也不会被for in遍历到。

  1. // 即使toString在obj上,但在ie9以下的浏览器依然无法用for in访问到
  2. var obj = {toString: "hello"}
  3. for (var k in obj) console.log(k); // 什么都不会打印

我们先搬出源码,简单的注释一下:

  1. /* 这是一个重写过toString方法的对象,使用propertyIsEnumerable方法来判断他的toString方法是否可以枚举,如果为false,则存在上面的枚举bug
  2. */
  3. var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
  4. // 列举出会出现enumBug的几个原型方法
  5. var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString','propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
  6. function collectNonEnumProps(obj, keys) {
  7. var nonEnumIdx = nonEnumerableProps.length;
  8. // 获取obj的constructor,如果constructor没有被重写过,那应该就是Object这个构造函数,实际上obj.constructor === Object.prototype.constructor
  9. var constructor = obj.constructor;
  10. // 如果constructor被重写过,重写后的constructor不是function或者constructor不存在prototype,那么会返回Object.prototype,否则就直接返回constructor.prototype
  11. var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
  12. // 这里对constructor进行单独处理
  13. var prop = 'constructor';
  14. // 如果obj自身没有constructor属性,并且keys数组中不存在constructor
  15. if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
  16. // 对其他几个方法进行处理
  17. while (nonEnumIdx--) {
  18. prop = nonEnumerableProps[nonEnumIdx];
  19. /* 判断当前prop是否在obj上(in是用来判断属性是否在对象上或者对象原型上),并且比较obj[prop]和proto[prop]是否相等(一般来说如果对应方法被重写了,那么obj[prop]访问的是重写后的方法,而proto[pros]访问的是原型上的方法,所以obj[prop]以一定不会和proto[prop]相等)
  20. */
  21. if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
  22. keys.push(prop);
  23. }
  24. }
  25. }

可能上面注释不是很详细,我在这里逐条解释一下。

首先是hasEnumBug这个属性,我们知道propertyIsEnumerable是判断对象上的某个属性是否可被枚举的,当然这个在ie9以下的浏览器中也会出现for in上面的那个bug,所以这里可以用一个重写过toString方法的对象来判断toString方法是否可枚举,如果返回了false,那么就说明浏览器中存在这个bug。

这里可能大家会有两个疑问,一个是为什么constructor要单独用_.has处理?直接放到while里面和大家一起处理不行吗?
我自己的理解是,可能是为了防止出现下面这种情况。

  1. // 用Object重写了constructor
  2. 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等方法了,比如:

  1. obj.constructor.prototype = null;
  2. console.log("toString" in obj); // false

写到这里,其实我也有一个疑问,那就是为什么不全部都用_.has来判断,反而将toString等六个属性用prop in obj && obj[prop] !== proto[prop]来判断?prop in obj && obj[prop] !== proto[prop]判断一定可靠吗?
看下面这个例子:

  1. var toString = Object.prototype.toString
  2. var obj = {toString: toString}

这种场景类似上面的constructor,因为obj[prop] !== proto[prop]必定会返回false,所以其他几种方法也有我刚说的constructor类似的情况,所以到这里我对自己前面讲的constructor单独处理的原因产生质疑,既然大家都会遇到这种问题,为什么不都直接用_.has来处理呢?

按照我自己的理解,这里我重写一下collectNonEnumProps方法,如下:

  1. var nonEnumerableProps = ['valueOf', 'isPrototypeOf','toString','propertyIsEnumerable', 'hasOwnProperty', 'constructor' 'toLocaleString'];
  2. function collectNonEnumProps(obj, keys) {
  3. var nonEnumIdx = nonEnumerableProps.length;
  4. var prop;
  5. while (nonEnumIdx--) {
  6. prop = nonEnumerableProps[nonEnumIdx];
  7. if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
  8. }
  9. }

以上都是个人理解,如果理解有偏差,望指出。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注