lodash源码解析:zip家族

839 阅读5分钟

本篇继续分析下 zip 家族的方法,zip 方法主要是把目标数组群合成为数组或对象。合成数组的方法包括zipunzipzipWithunzipWith,合成对象的方法包括zipObjectzipObjectDeep及核心方法baseZipObjectbaseSet

对应源码分析已推到 github 仓库: github.com/MageeLin/lo…

zip 家族方法的依赖路径图如下所示:

zip

zip

zip 方法比较有意思,它和 unzip 既是互为逆操作,却又是相同的操作。在 zip 方法内部就是调用的 return unzip(arrays)

unzip

unzipzipzipWithunzipWith 实现的基础。但是 unzip 本质上其实只有两步:

  1. 找到 arrays 中最长的 array 的长度;
  2. arrays 的每个 array 中拿出对应位置的元素组装到一起,形成新的 arrays
import filter from './filter.js';
import map from './map.js';
import baseProperty from './.internal/baseProperty.js';
import isArrayLikeObject from './isArrayLikeObject.js';

/**
 * 这个方法类似于`zip`,但是它接收分组`array`组成的`arrays`,
 * 并且创建一个`arrays`,`arrays`中的`array`是zip前的分组结构
 *
 * @since 1.2.0
 * @category Array
 * @param {Array} array 要执行的的arrays
 * @returns {Array} 返回重新分组后的arrays
 * @see unzipWith, zip, zipObject, zipObjectDeep, zipWith
 * @example
 *
 * const zipped = zip(['a', 'b'], [1, 2], [true, false])
 * // => [['a', 1, true], ['b', 2, false]]
 *
 * unzip(zipped)
 * // => [['a', 'b'], [1, 2], [true, false]]
 */
function unzip(array) {
  // 当array是空数组时,返回[]
  if (!(array != null && array.length)) {
    return [];
  }
  let length = 0;
  // 迭代执行了两个功能,第一个是讲数组筛选出来
  // 第二个是让length为最长子数组的长度
  array = filter(array, (group) => {
    if (isArrayLikeObject(group)) {
      length = Math.max(group.length, length);
      return true;
    }
  });
  let index = -1;
  // 根据最长长度length建一个空数组
  const result = new Array(length);
  // 迭代
  while (++index < length) {
    // 把每一个array都取对应的位置处的值map成一个新array
    // 然后赋值给result[index]
    result[index] = map(array, baseProperty(index));
  }
  return result;
}

export default unzip;

zip

刚才说zip 方法和 unzip 既是互为逆操作,却又是相同的操作。就是因为本质上都是调用 unzip,但是对同一个 arrays 不停的用 unzip 来调用时,其实就是在不停的重复形成两个数组。

import unzip from './unzip.js';

/**
 * 创建一个分组元素的`arrays`,`arrays`的第一个`array`包含所有给定`array`的第一个元素,
 * `arrays`的第二个`array`包含所有给定`array`的第二个元素,以此类推。
 *
 * @since 0.1.0
 * @category Array
 * @param {...Array} [arrays] 要处理的arrays
 * @returns {Array} 返回分组后的array组成的arrays
 * @see unzip, unzipWith, zipObject, zipObjectDeep, zipWith
 * @example
 *
 * zip(['a', 'b'], [1, 2], [true, false])
 * // => [['a', 1, true], ['b', 2, false]]
 */
function zip(...arrays) {
  // 返回了unzip(arrays)的结果,unzip和zip在本质上是一样的
  return unzip(arrays);
}

export default zip;

unZipWith

同样的原理 unZipWithzipWith 也是同样的关系。但是他们都依赖了 unZip,只不过是每次循环的时候调用了一次 iteratee

import map from './map.js';
import unzip from './unzip.js';

/**
 * 此方法类似于`unzip`,但是它接受一个`iteratee`迭代方法参数
 * 来指定重组值应该如何被组合。
 * iteratee 调用时会传入每个分组的值: (...group)。
 *
 * @since 3.8.0
 * @category Array
 * @param {...Array} [arrays] 要处理的arrays
 * @param {Function} iteratee 迭代方法迭代每个array,决定如何组合分组值
 * @returns {Array} 返回重新分组后的arrays
 * @example
 *
 * const zipped = zip([1, 2], [10, 20], [100, 200])
 * // => [[1, 10, 100], [2, 20, 200]]
 *
 * unzipWith(zipped, add)
 * // => [3, 30, 300]
 */
function unzipWith(array, iteratee) {
  // 当array是空数组时,返回[]
  if (!(array != null && array.length)) {
    return [];
  }
  // 直接调用unzip返回结果
  const result = unzip(array);
  // 返回iteratee.apply(undefined, group)来转化group中的结果
  return map(result, (group) => iteratee.apply(undefined, group));
}

export default unzipWith;

zipWith

zipWith 中需要注意的地方是对 iteratee 参数的判断,如果最后一个参数是函数,就把他从 arrayspop 出去。

import unzipWith from './unzipWith.js';

/**
 * 这个方法类似于`zip`,但是它接受一个`iteratee`(迭代方法),
 * 来指定分组的值应该如何被组合。
 * iteratee函数调用每个组的元素: (...group).
 *
 * @since 3.8.0
 * @category Array
 * @param {...Array} [arrays] 要处理的arrays
 * @param {Function} iteratee 迭代方法迭代每个array,决定如何组合分组值
 * @returns {Array} 返回分组后的array组成的arrays
 * @see unzip, unzipWith, zip, zipObject, zipObjectDeep, zipWith
 * @example
 *
 * zipWith([1, 2], [10, 20], [100, 200], (a, b, c) => a + b + c)
 * // => [111, 222]
 */
function zipWith(...arrays) {
  // 拿到最后一个参数iteratee,并把它从arrays中pop出去
  const length = arrays.length;
  let iteratee = length > 1 ? arrays[length - 1] : undefined;
  iteratee =
    typeof iteratee === 'function' ? (arrays.pop(), iteratee) : undefined;
  // 和zip方法类似,本质是调用unzipWith(arrays, iteratee)
  return unzipWith(arrays, iteratee);
}

export default zipWith;

zipObject

zipObject 方法和 zip 系列的功能不太一样,zipObject 接受的两个参数,第一个是键的数组,第二个是值的数组,最后拼成一个对象。

下面是前置依赖的方法 assignValuebaseSet,前者用于普通赋值,后者用于深度赋值。

baseAssignValue

这个地方不太懂为什么要给 enumerable: true__proto__enumerable 不应该是 false 吗?需要再思考思考。

/**
 * `assignValue` and `assignMergeValue`的基础实现,没有进行value检查
 *
 * @private
 * @param {Object} object 要修改的对象
 * @param {string} key 要分配的属性键
 * @param {*} 要分配的值
 */
function baseAssignValue(object, key, value) {
  // 当给对象的原型分配值时,使用Object.defineProperty()
  if (key == '__proto__') {
    Object.defineProperty(object, key, {
      configurable: true,
      enumerable: true,
      value: value,
      writable: true,
    });
  } else {
    // 普通情况就直接分配
    object[key] = value;
  }
}

export default baseAssignValue;

assignValue

import baseAssignValue from './baseAssignValue.js';
import eq from '../eq.js';

/** 用于检查对象的自身属性 */
const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
 * 当`object`上`key`对应的值与`value`不相等时,就把`value`分配给它
 *
 * @private
 * @param {Object} object 要修改的对象
 * @param {string} key 要分配的属性键
 * @param {*} value 要分配的属性值
 */
function assignValue(object, key, value) {
  // 拿到object上对应的属性值
  const objValue = object[key];

  // 当key不是object的自身属性,或者 要分配的值与objValue对应的值不相等时
  if (!(hasOwnProperty.call(object, key) && eq(objValue, value))) {
    // 当value为有效数字时
    if (value !== 0 || 1 / value === 1 / objValue) {
      // 执行baseAssignValue()
      baseAssignValue(object, key, value);
    }
    // 或者当 value为undefined 且 object上没有key时
  } else if (value === undefined && !(key in object)) {
    // 执行baseAssignValue()
    baseAssignValue(object, key, value);
  }
}

export default assignValue;

baseSet

import assignValue from './assignValue.js';
import castPath from './castPath.js';
import isIndex from './isIndex.js';
import isObject from '../isObject.js';
import toKey from './toKey.js';

/**
 * `set`的基础实现
 *
 * @private
 * @param {Object} object 要修改的对象
 * @param {Array|string} path 要设置的属性路径
 * @param {*} value 要设置的属性值
 * @param {Function} [customizer] 自定义路径创建的方法
 * @returns {Object} 返回对象
 */
function baseSet(object, path, value, customizer) {
  // 不是对象就直接返回
  if (!isObject(object)) {
    return object;
  }

  // 返回路径对应的路径数组
  path = castPath(path, object);

  const length = path.length;
  const lastIndex = length - 1;

  let index = -1;
  let nested = object;

  // 这里的迭代就是在用路径数组不停的给object添加后代属性,
  // object,nested(变),objValue(变)都是原对象上的
  // value,newValue(变)都是要设置的属性上的
  while (nested != null && ++index < length) {
    const key = toKey(path[index]);
    let newValue = value;

    // 这下面都是怎么整理出对应的每一层的newValue
    if (index != lastIndex) {
      // nested是每一层的object
      const objValue = nested[key];
      // 如果有自定义路径创建的方法,就用,没有就undefined,方便下一步
      newValue = customizer ? customizer(objValue, key, nested) : undefined;
      // 当对象上没有下一级路径时,就开始创建
      if (newValue === undefined) {
        newValue = isObject(objValue)
          ? // 原对象上有这个key就直接赋给newValue
            objValue
          : // 是index则代表空数组,否则就是空对象
          isIndex(path[index + 1])
          ? []
          : {};
      }
    }
    assignValue(nested, key, newValue);
    nested = nested[key];
  }
  return object;
}

export default baseSet;

baseZipObject

/**
 * `zipObject` 的基础实现,使用 `assignFunc`来分配属性值。
 *
 * @private
 * @param {Array} props 属性标识符
 * @param {Array} values 属性值
 * @param {Function} assignFunc 分配属性的函数
 * @returns {Object} 返回一个新的对象
 */
function baseZipObject(props, values, assignFunc) {
  // 初始化
  let index = -1;
  const length = props.length;
  const valsLength = values.length;
  // 新建一个结果对象
  const result = {};

  // 迭代
  while (++index < length) {
    // 规范values中的value值
    const value = index < valsLength ? values[index] : undefined;
    // 使用assignFunc来分配value
    assignFunc(result, props[index], value);
  }
  return result;
}

export default baseZipObject;

zipObject

zipObject 方法在调用 baseZipObject 时,传的参数为 assignValue,所以是根据 props 的每个元素直接当 key,实现赋值。

import assignValue from './.internal/assignValue.js';
import baseZipObject from './.internal/baseZipObject.js';

/**
 * 这个方法类似 `fromPairs`,但是它接受2个数组,
 * 第一个数组中的值作为属性标识符(属性名),第二个数组中的值作为相应的属性值。
 * @since 0.4.0
 * @category Array
 * @param {Array} [props=[]] 属性标识符数组
 * @param {Array} [values=[]] 属性值数组
 * @returns {Object} 返回一个新的对象
 * @see unzip, unzipWith, zip, zipObjectDeep, zipWith
 * @example
 *
 * zipObject(['a', 'b'], [1, 2])
 * // => { 'a': 1, 'b': 2 }
 */
function zipObject(props, values) {
  return baseZipObject(props || [], values || [], assignValue);
}

export default zipObject;

zipObjectDeep

zipObjectDeep 方法在调用 baseZipObject 时,传的参数为 baseSet,所以是根据 props 的每个元素的不同形式来进行深度赋值。

import baseSet from './.internal/baseSet.js';
import baseZipObject from './.internal/baseZipObject.js';

/**
 * 这个方法类似 `zipObject`,但是它支持属性路径。
 *
 * @since 4.1.0
 * @category Array
 * @param {Array} [props=[]] 属性标识符
 * @param {Array} [values=[]] 属性值
 * @returns {Object} 返回新对象
 * @see unzip, unzipWith, zip, zipObject, zipWith
 * @example
 *
 * zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2])
 * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } }
 */
function zipObjectDeep(props, values) {
  // 同样是调用的baseZipObject(),但是分配方法用的是baseSet
  return baseZipObject(props || [], values || [], baseSet);
}

export default zipObjectDeep;

前端记事本,不定期更新,欢迎关注!