lodash里的toArray方法实现

326 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第27天,点击查看活动详情

前言

lodash里的to系列中,将数据转换为目标类型数组的方法主要是toArray方法。

toArray

toArray方法主要是将参数value转换为一个数组。

使用如下:

_.toArray({ 'a': 1, 'b': 2 });
// => [1, 2]
 
_.toArray('abc');
// => ['a', 'b', 'c']
 
_.toArray(1);
// => []
 
_.toArray(null);
// => []

_.toArray()
// => []

_.toArray({})
// => []

实现上,toArray方法需要借助内部封装的各个方法实现,具体处理逻辑如下:

  • 对于不存在的参数直接返回空数组。
  • 对于类数组,会先判断是否属于字符串,是的话调用stringToArray方法,不是的话调用copyArray方法。
  • 如果是身上存在迭代器的对象,则调用iteratorToArray方法。
  • 如果是Map对象则调用mapToArray,如果是Set对象则调用setToArray,否则调用values方法。

源码如下:

import copyArray from './.internal/copyArray.js'
import getTag from './.internal/getTag.js'
import isArrayLike from './isArrayLike.js'
import isString from './isString.js'
import iteratorToArray from './.internal/iteratorToArray.js'
import mapToArray from './.internal/mapToArray.js'
import setToArray from './.internal/setToArray.js'
import stringToArray from './.internal/stringToArray.js'
import values from './values.js'

const mapTag = '[object Map]'
const setTag = '[object Set]'

const symIterator = Symbol.iterator

function toArray(value) {
  if (!value) {
    return []
  }
  if (isArrayLike(value)) {
    return isString(value) ? stringToArray(value) : copyArray(value)
  }
  if (symIterator && value[symIterator]) {
    return iteratorToArray(value[symIterator]())
  }
  const tag = getTag(value)
  const func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values)

  return func(value)
}

copyArray

copyArray方法是内部方法,通过该方法可以生成一个全新的数组,内部通过while实现数组项的拷贝,所以并不是深度拷贝。

function copyArray(source, array) {
  let index = -1
  const length = source.length

  array || (array = new Array(length))
  while (++index < length) {
    array[index] = source[index]
  }
  return array
}

iteratorToArray

iteratorToArray是一个内部方法,该方法会对迭代器参数进行遍历。

function iteratorToArray(iterator) {
  let data
  const result = []

  while (!(data = iterator.next()).done) {
    result.push(data.value)
  }
  return result
}

mapToArray

mapToArray方法是一个内部方法,该方法通过调用forEach循环遍历Map对象,输出一个二维数组。

function mapToArray(map) {
  let index = -1
  const result = new Array(map.size)

  map.forEach((value, key) => {
    result[++index] = [key, value]
  })
  return result
}

setToArray

setToArray方法是一个内部方法,该方法通过调用forEach循环遍历Set对象,输出一个二维数组。

function setToArray(set) {
  let index = -1
  const result = new Array(set.size)

  set.forEach((value) => {
    result[++index] = value
  })
  return result
}

stringToArray

stringToArray是一个内部方法,在实现上通过内部封装的asciiToArray、hasUnicode、unicodeToArray三个方法处理不同编码类型的数据,将字符串参数转换为数组类型。

stringToArray源码如下:

import hasUnicode from './hasUnicode.js'
import asciiToArray from './asciiToArray.js'
import unicodeToArray from './unicodeToArray.js'

function stringToArray(string) {
  return hasUnicode(string)
    ? unicodeToArray(string)
    : asciiToArray(string)
}

接下来我们先看各个依赖方法的实现。

hasUnicode

hasUnicode方法是一个内部方法,主要判断字符串是否含有指定的编码类型,实现上内部枚举了各个编码范围,通过正则匹配返回匹配结果。

const rsAstralRange = '\ud800-\udfff'
const rsComboMarksRange = '\u0300-\u036f'
const reComboHalfMarksRange = '\ufe20-\ufe2f'
const rsComboSymbolsRange = '\u20d0-\u20ff'
const rsComboMarksExtendedRange = '\u1ab0-\u1aff'
const rsComboMarksSupplementRange = '\u1dc0-\u1dff'
const rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange + rsComboMarksExtendedRange + rsComboMarksSupplementRange
const rsVarRange = '\ufe0e\ufe0f'

const rsZWJ = '\u200d'

const reHasUnicode = RegExp(`[${rsZWJ + rsAstralRange + rsComboRange + rsVarRange}]`)

function hasUnicode(string) {
  return reHasUnicode.test(string)
}

asciiToArray

asciiToArray方法是一个内部方法,实现上调用原型链上的split方法切割字符串为数组。

function asciiToArray(string) {
  return string.split('')
}

unicodeToArray

unicodeToArray方法主要是将unicode编码的字符串转换为数组,内部定义了一系列编码匹配规则,通过正则匹配相应的编码,返回匹配结果。

const rsAstralRange = '\ud800-\udfff'
const rsComboMarksRange = '\u0300-\u036f'
const reComboHalfMarksRange = '\ufe20-\ufe2f'
const rsComboSymbolsRange = '\u20d0-\u20ff'
const rsComboMarksExtendedRange = '\u1ab0-\u1aff'
const rsComboMarksSupplementRange = '\u1dc0-\u1dff'
const rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange + rsComboMarksExtendedRange + rsComboMarksSupplementRange
const rsVarRange = '\ufe0e\ufe0f'

const rsAstral = `[${rsAstralRange}]`
const rsCombo = `[${rsComboRange}]`
const rsFitz = '\ud83c[\udffb-\udfff]'
const rsModifier = `(?:${rsCombo}|${rsFitz})`
const rsNonAstral = `[^${rsAstralRange}]`
const rsRegional = '(?:\ud83c[\udde6-\uddff]){2}'
const rsSurrPair = '[\ud800-\udbff][\udc00-\udfff]'
const rsZWJ = '\u200d'

const reOptMod = `${rsModifier}?`
const rsOptVar = `[${rsVarRange}]?`
const rsOptJoin = `(?:${rsZWJ}(?:${[rsNonAstral, rsRegional, rsSurrPair].join('|')})${rsOptVar + reOptMod})*`
const rsSeq = rsOptVar + reOptMod + rsOptJoin
const rsNonAstralCombo = `${rsNonAstral}${rsCombo}?`
const rsSymbol = `(?:${[rsNonAstralCombo, rsCombo, rsRegional, rsSurrPair, rsAstral].join('|')})`

const reUnicode = RegExp(`${rsFitz}(?=${rsFitz})|${rsSymbol + rsSeq}`, 'g')

function unicodeToArray(string) {
  return string.match(reUnicode) || []
}

values

values方法是对外导出的方法,接受对象类型的参数,该方法返回包含对象值的一个数组。

import baseValues from './.internal/baseValues.js'
import keys from './keys.js'


function values(object) {
  return object == null ? [] : baseValues(object, keys(object))
}

baseValues

baseValues方法是values方法的核心,返回一个数组,数组项是对象参数的值。

function baseValues(object, props) {
  return props == null ? [] : props.map((key) => object[key])
}

keys

keys方法返回包含对象参数一系列键值的数组,该方法是lodash对外导出的方法。

实现上借助arrayLikeKeys方法和isArrayLike方法判断类数组。如果是类数组则调用arrayLikeKeys方法,普通数组则直接调用原生的Object.keys方法。

import arrayLikeKeys from './.internal/arrayLikeKeys.js'
import isArrayLike from './isArrayLike.js'

function keys(object) {
  return isArrayLike(object)
    ? arrayLikeKeys(object)
    : Object.keys(Object(object))
}

arrayLikeKeys

arrayLikeKeys方法借助一系列is方法判断,通过循环遍历类数组输出每一项的键值。

import isArguments from '../isArguments.js'
import isBuffer from '../isBuffer.js'
import isIndex from './isIndex.js'
import isTypedArray from '../isTypedArray.js'

const hasOwnProperty = Object.prototype.hasOwnProperty

function arrayLikeKeys(value, inherited) {
  const isArr = Array.isArray(value)
  const isArg = !isArr && isArguments(value)
  const isBuff = !isArr && !isArg && isBuffer(value)
  const isType = !isArr && !isArg && !isBuff && isTypedArray(value)
  const skipIndexes = isArr || isArg || isBuff || isType
  const length = value.length
  const result = new Array(skipIndexes ? length : 0)
  let index = skipIndexes ? -1 : length
  while (++index < length) {
    result[index] = `${index}`
  }
  for (const key in value) {
    if ((inherited || hasOwnProperty.call(value, key)) &&
        !(skipIndexes && (
          (key === 'length' ||
           isIndex(key, length))
        ))) {
      result.push(key)
    }
  }
  return result
}

小结

本篇章我们通过toArray的实现认识了lodash里封装的一系列内部方法,以及前面篇章介绍到的is系列判断方法,同时也了解到了对外导出的keys方法的实现。