Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

underscore源码剖析之keys #1

Open
yinguangyao opened this issue Mar 14, 2018 · 0 comments
Open

underscore源码剖析之keys #1

yinguangyao opened this issue Mar 14, 2018 · 0 comments

Comments

@yinguangyao
Copy link
Owner

yinguangyao commented Mar 14, 2018

underscore源码分析之_.keys

这篇文章主要分析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;
  };

_.has

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的引用,这样就不会出现被覆盖的情况了。

collectNonEnumProps

看完了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);
    }
  }

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

@yinguangyao yinguangyao changed the title underscore源码分析之keys underscore源码剖析之keys Mar 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant