underscore源码解析(二)Object工具函数

434 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

今天来跟大家聊聊Object的一些相关的工具函数,比如keysallkeysextendspickclone等函数。

keys和allkeys

_.keys({one: 1, two: 2, three: 3});
=> ["one", "two", "three"]

// allkeys
function Stooge(name) {
  this.name = name;
}
Stooge.prototype.silly = true;
_.allKeys(new Stooge("Moe"));
=> ["name", "silly"]

先看keys,如何获取一个对象的key呢(不包括原型链的),直接看代码吧,思路如下:

var nativeKeys = Object.keys// Retrieve the names of an object's own properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`.
function keys(obj) {
  // 不是一个对象,直接返回 []
  if (!isObject(obj)) return [];
  // Object.keys(),支持Object.keys()就优先使用
  if (nativeKeys) return nativeKeys(obj);
  var keys = [];
// for-in循环会遍历原型上可枚举的所有属性。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回
  // 只遍历自身属性,不要继承来的
  for (var key in obj) if (has$1(obj, key)) keys.push(key);
  // Ahem, IE < 9.
  // IE < 9 下,重写了 toString 等方法时的兼容性问题
  if (hasEnumBug) collectNonEnumProps(obj, keys);
  return keys;
}

思路比较简单,但是最后却调用了collectNonEnumProps函数,作用到底是什么呢?

原来是for ... in 存在的浏览器兼容问题,直接贴代码(大佬的注释),大概思路就是:

  1. 判断在当前环境下时候存在兼容问题,列举有问题的属性;
  2. 判断传入对象的constructor属性是否被修改,如果没有就用构造函数的原型对象,修改了就是Object.prototype
  3. 对象有constructor并且keys数组没有,就添加进去
  4. 循环存在bug的属性列表,依次取出,判断其属性值是否和原型对象上的值相同,不同则说明被重写了添加进去
 // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
    // IE < 9 下 不能用 for key in ... 来枚举对象的某些 key
    // 比如重写了对象的 `toString` 方法,这个 key 值就不能在 IE < 9 下用 for in 枚举到
    // IE < 9,{toString: null}.propertyIsEnumerable('toString') 返回 false
    // IE < 9,重写的 `toString` 属性被认为不可枚举
    // 据此可以判断是否在 IE < 9 浏览器环境中
    var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
  
    // IE < 9 下不能用 for in 来枚举的 key 值集合
    // 其实还有个 `constructor` 属性
    // 个人觉得可能是 `constructor` 和其他属性不属于一类
    // nonEnumerableProps[] 中都是方法
    // 而 constructor 表示的是对象的构造函数
    // 所以区分开来了
    var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
                        'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
  
    // obj 为需要遍历键值对的对象
    // keys 为键数组
    // 利用 JavaScript 按值传递的特点
    // 传入数组作为参数,能直接改变数组的值
    function collectNonEnumProps(obj, keys) {
      var nonEnumIdx = nonEnumerableProps.length;
      var constructor = obj.constructor;
  
      // 获取对象的原型
      // 如果 obj 的 constructor 被重写
      // 则 proto 变量为 Object.prototype
      // 如果没有被重写
      // 则为 obj.constructor.prototype
      var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
  
      // Constructor is a special case.
      // `constructor` 属性需要特殊处理 (是否有必要?)
      // see https://github.com/hanzichi/underscore-analysis/issues/3
      // 如果 obj 有 `constructor` 这个 key
      // 并且该 key 没有在 keys 数组中
      // 存入 keys 数组
      var prop = 'constructor';
      if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
  
      // 遍历 nonEnumerableProps 数组中的 keys
      while (nonEnumIdx--) {
        prop = nonEnumerableProps[nonEnumIdx];
        // prop in obj 应该肯定返回 true 吧?是否有判断必要?
        // obj[prop] !== proto[prop] 判断该 key 是否来自于原型链
        // 即是否重写了原型链上的属性
        if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
          keys.push(prop);
        }
      }
    }

那么allKeys的实现和这也大概相同,就直接代码吧:

// Retrieve all the enumerable property names of an object.
    function allKeys(obj) {
      if (!isObject(obj)) return [];
      var keys = [];
      for (var key in obj) keys.push(key);
      // Ahem, IE < 9.
      if (hasEnumBug) collectNonEnumProps(obj, keys);
      return keys;
    }

keys的区别就在于for...in的时候是否需要判断是自身属性

获取到了对象的keys那么实现valuespairs方法就很简单了

// _.values({one: 1, two: 2, three: 3}); ==> [1, 2, 3]

// Retrieve the values of an object's properties.
function values(obj) {
  var _keys = keys(obj); // 获取对象自身所有的keys
  var length = _keys.length;
  var values = Array(length);
  for (var i = 0; i < length; i++) { // 循环获取对象的key对应的属性值
    values[i] = obj[_keys[i]];
  }
  return values;
}

我看起pairs是和Object.entrie一样功能,但是这竟然没用Object.entrie做判断

// _.pairs({one: 1, two: 2, three: 3}); // => [["one", 1], ["two", 2], ["three", 3]]

// Convert an object into a list of `[key, value]` pairs.
// The opposite of `_.object` with one argument.
function pairs(obj) {
  var _keys = keys(obj);
  var length = _keys.length;
  var pairs = Array(length);
  for (var i = 0; i < length; i++) {
    pairs[i] = [_keys[i], obj[_keys[i]]];
  }
  return pairs;
}

_.extend & _.extendOwn & _.defaults

keysallkeys类似的是,这三个的实现也都依赖了一个函数,调用函数createAssigner生成上面三个方法,只想说别人写的代码真6,那么这个函数是怎么实现的呢?

/**
 * extend:将 source 对象中的所有属性简单地覆盖到 destination 对象上,并且返回 destination 对象. 复制是按顺序的, 所以后面的对象属性会把前面的对象属性覆盖掉(如果有重复)。
 * extendOwn: 类似于 extend, 但只复制自己的属性覆盖到目标对象。(注:不包括继承过来的属性)
 * defaults: 用 defaults 对象填充 object 中的 undefined 属性。 并且返回这个 object
 */
// An internal function for creating assigner functions.
function createAssigner(keysFunc, defaults) {
    // 返回一个函数,这个函数接受 n 个参数,将第二个及以后的对象的属性  给到 第一个对象
    // function(target, [source]){}  将 [source] 中的对象挨个遍历,将每个对象的属性都复制到 target
    return function (obj) {
        var length = arguments.length;
        if (defaults) obj = Object(obj); // var defaults = createAssigner(allKeys, true);
        if (length < 2 || obj == null) return obj; // 只传了1个或0个,说明没有 被合并的对象,直接返回 目标对象
        // 遍历传入的参数(排除第一个,因为他是目标对象)
        for (var index = 1; index < length; index++) {
            // 被合并对象
            var source = arguments[index],
                //根据不同情况返回 source 的 keys(_.extendOwn) or allKeys
                keys = keysFunc(source),
                l = keys.length;
            // 遍历该对象的键值对
            for (var i = 0; i < l; i++) {
                var key = keys[i];
                // 不是 defaults(extend、extendOwn)  直接添加,会覆盖目标对象的属性值
                // 或者 是 defaults  就将 目标对象的 undefiend 属性进行填充,要是目标对象有值就不做处理(不覆盖目标对象的属性值)
                if (!defaults || obj[key] === void 0) obj[key] = source[key];
            }
        }
        return obj;
    };
}

实现了上面函数后其他就简单了,至此这三个方法就都实现了。

// Extend a given object with all the properties in passed-in object(s).
var extend = createAssigner(allKeys);
  
// Assigns a given object with all the own properties in the passed-in
// object(s).
// (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
var extendOwn = createAssigner(keys);

// Fill in a given object with default properties.
var defaults = createAssigner(allKeys, true);

isEqual

接下来干一件大事,就是来看看它是怎么来实现isEqual函数,来判断两个元素是否相等的,这可以说是underscore中最复杂的一个函数了

先分析一下,要是我们来实现这么一个函数,该怎么做呢,要分为哪几步呢?

在js中分为基本数据类型和引用类型,基本数据类型值相等就是相同的,但是在基本数据类型中有这么几种特殊情况,0-0是相等的,但是他们是不同的;NaN 不等于自己;null undefined只等于自己;然后判断两个参数类型,类型不同肯定不等,要是引用类型就继续深度比较。具体就看代码注释吧:

// Perform a deep comparison to check if two objects are equal.
// 判断两者是否相同
function isEqual(a, b) {
    return eq(a, b);
}
// Internal recursive comparison function for `_.isEqual`.
function eq(a, b, aStack, bStack) {
    // Identical objects are equal. `0 === -0`, but they aren't identical.
    // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal).
    /**
     * 直接判断是否相等,
     * 1. 两者相等,一个不是0,说明两者就是就是相同的 ==> 基本类型值相等(1===1)、引用类型引用地址相同(a=b={} a===b)
     * 2. 如果a === 0,那么两者相等,说明 b === 0 or b === -0, 判断 1 / a === 1 / b是否相等(Infinity、-Infinity) 就知道 b 的值了
     */
    if (a === b) return a !== 0 || 1 / a === 1 / b;

    /** 以下就是 a !== b 的逻辑了,引用类型可能不相等,但是可以相同(属性和属性值都相同) */

    // `null` or `undefined` only equal to itself (strict comparison).
    // 如果两个中有一个是 `null` or `undefined`时,就说明两个不相同,因为 `null` or `undefined`只等于自己
    if (a == null || b == null) return false;

    // `NaN`s are equivalent, but non-reflexive.
    // NaN 不等于自己,这种情况说明是 NaN,如果两个都是 NaN,咋也是相同的
    if (a !== a) return b !== b;

    // Exhaust primitive checks
    var type = typeof a;
    // 说明 a 是基本类型,b 不是对象类型(就是基础类型),两个基础类型,还不相等也不是特殊情况,那么两者不同
    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

    // 引用类型或基本类型的包装类型判断
    return deepEq(a, b, aStack, bStack);
}

那么如何判断两个引用类型是否相同呢?

  1. 传入的两个参数是不是 _ 函数的实例,是,重新赋值,比较两者的值
// Internal recursive comparison function for `_.isEqual`.
function deepEq(a, b, aStack, bStack) {
    // Unwrap any wrapped objects.
    // 是否是 _ 函数的实例,是,比较两者的值
    if (a instanceof _$1) a = a._wrapped;
    if (b instanceof _$1) b = b._wrapped;
}
  1. 通过Object.prototype.toString.call判断两者类型是否相同,不相同,则返回false(类型不同,两者肯定不同)
// Compare `[[Class]]` names.
// 通过原型链判断,两者是否是同一类型,如果不是同一类型,直接false
var className = toString.call(a);
if (className !== toString.call(b)) return false;
  1. 两者类型相同
  2. 他们是RegExp or String的时候,比较两者字符串是否相等就行了
`return '' + a === '' + b;`
  1. 是Number时,就将Number的包装类型转为基本类型+a,先判断NaN,然后判断0-0,最后就是正常情况了
  2. Date or Boolean的时候,将他们转为number类型进行判断return +a === +b;Date类型变为时间戳,比较时间戳;Boolean类型变为0、1进行比较
// 同一类型,进行具体区分
switch (className) {
    // These types are compared by value.
    case '[object RegExp]':
    // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
    case '[object String]':
        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
        // equivalent to `new String("5")`.
        return '' + a === '' + b;
    case '[object Number]':
        // `NaN`s are equivalent, but non-reflexive.
        // Object(NaN) is equivalent to NaN.
        if (+a !== +a) return +b !== +b;
        // An `egal` comparison is performed for other numeric values.
        return +a === 0 ? 1 / +a === 1 / b : +a === +b;
    case '[object Date]':
    case '[object Boolean]':
        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
        // millisecond representations. Note that invalid dates with millisecond representations
        // of `NaN` are not equivalent.
        return +a === +b;
    case '[object Symbol]':
        return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
    case '[object ArrayBuffer]':
    case tagDataView:
        // Coerce to typed array so we can fall through.
        return deepEq(toBufferView(a), toBufferView(b), aStack, bStack);
}

到这就还剩ArrayObject了,这里就要采取递归判断了,咱们一个一个来看。

// Internal recursive comparison function for `_.isEqual`.
function deepEq(a, b, aStack, bStack) {

    var areArrays = className === '[object Array]';
    // 
    if (!areArrays && isTypedArray$1(a)) {
        var byteLength = getByteLength(a);
        if (byteLength !== getByteLength(b)) return false;
        if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true;
        areArrays = true;
    }
    // 不是数组
    if (!areArrays) {
        // 如果 a 不是 object 或者 b 不是 object 则返回 false
        if (typeof a != 'object' || typeof b != 'object') return false;

        // Objects with different constructors are not equivalent, but `Object`s or `Array`s
        // from different frames are.
        // 具有不同构造函数的对象是不等价的
        var aCtor = a.constructor, bCtor = b.constructor;
        if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor &&
            isFunction$1(bCtor) && bCtor instanceof bCtor)
            && ('constructor' in a && 'constructor' in b)) {
            return false;
        }
    }
    // Assume equality for cyclic structures. The algorithm for detecting cyclic
    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
    // 假设循环结构相等。检测循环结构的算法改编自ES 5.1第15.12.3节,抽象操作“JO”

    // Initializing stack of traversed objects. 
    // It's done here since we only need them for objects and arrays comparison.
    // 初始化遍历对象的堆栈,这是在这里完成的,因为我们只需要它们来比较对象和数组。
    // 第一次调用 eq() 函数,没有传入 aStack 和 bStack 参数,之后递归调用都会传入这两个参数
    aStack = aStack || [];
    bStack = bStack || [];

    var length = aStack.length;
    while (length--) {
        // Linear search. Performance is inversely proportional to the number of
        // unique nested structures.
        if (aStack[length] === a) return bStack[length] === b;
    }

    // Add the first object to the stack of traversed objects.
    aStack.push(a);
    bStack.push(b);

    // Recursively compare objects and arrays.
    if (areArrays) {
        // Compare array lengths to determine if a deep comparison is necessary.
        length = a.length;
        // 两者都是数组,数组长度都不一样,说明肯定不是相同的
        if (length !== b.length) return false;
        // Deep compare the contents, ignoring non-numeric properties.
        // 递归遍历每一个子项,只要有一个不一样,就是不同的
        while (length--) {
            if (!eq(a[length], b[length], aStack, bStack)) return false;
        }
    } else {
        // Deep compare objects. 深入对比两个对象
        var _keys = keys(a), key;
        length = _keys.length;
        // Ensure that both objects contain the same number of properties before comparing deep equality.
        // 同理,两者都是对象,对象的属性个数不一样,说明肯定不是相同的
        if (keys(b).length !== length) return false;
        while (length--) {
            // Deep compare each member
            key = _keys[length];
            if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
        }
    }
    // Remove the first object from the stack of traversed objects.
    // 与 aStack.push(a) 对应 此时 aStack 栈顶元素正是 a 而代码走到此步 a 和 b isEqual 确认 所以 a,b 两个元素可以出栈
    aStack.pop();
    bStack.pop();
    return true;
}

至此,isEqual函数到此结束,实现思路还是很清楚的,也比较容易理解,大家可以仔细看看。

create

_.create(prototype, props)创建具有给定原型的新对象,可选附加 props 作为 own 的属性。基本上,和 Object.create 一样,但是没有所有的属性描述符。

// An internal function for creating a new object that inherits from another.
function baseCreate(prototype) {
    if (!isObject(prototype)) return {};
    if (nativeCreate) return nativeCreate(prototype);
    var Ctor = ctor();
    Ctor.prototype = prototype;
    var result = new Ctor;
    Ctor.prototype = null;
    return result;
}

// Creates an object that inherits from the given prototype object.
// If additional properties are provided then they will be added to the
// created object.
function create(prototype, props) {
    var result = baseCreate(prototype);
    if (props) extendOwn(result, props);
    return result;
}

Objects相关的方法基本都介绍完了,其他的多比较简单直接,大家哟兴趣可以直接去看,在这里就不多说了,接下来就准备去看 Array 扩展方法相关代码