lodash 对象函数 at

461 阅读3分钟

本函数涉及到基本类型校验,数组扁平化,函数结果缓存以及正则表达式匹配对象属性(嵌套属性)相关的内容。at函数用于获取指定对象属性的值。

类型校验函数

getTag

利用Object.prototype.toString方法获取类型的tag([object Object]),并对nullundefined类型做特殊处理

const toString = Object.prototype.toString

function getTag(value) {
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  return toString.call(value)
}

isObjectLike

判断是否是一个对象类型的数据。借助typeof方法,并对null作单独处理。因为typeof null === 'object

function isObjectLike(value) {
  return typeof value === 'object' && value !== null
}

isArguments

判断一个数据是否为arguments类型。借助了上述的getTag方法以及isObjectLike

function isArguments(value) {
  // isObjectLike 是否是一个对象 typeof value === 'object' && value !== null
  return isObjectLike(value) && getTag(value) == '[object Arguments]'
}

isFlattenable

是否能被扁平化。@@isConcatSpreadable协议,用于判断一个对象是否能被concat函数扁平化。

const spreadableSymbol = Symbol.isConcatSpreadable
function isFlattenable(value) {
  // 数组对象, arguments对象,或者是可扁平化对象
  return Array.isArray(value) || isArguments(value) ||
    !!(value && value[spreadableSymbol])
}

isKey

判断传入的值是否是一个对象的键

// 是否是一个.开头或[]包裹的属性
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/
// 字母数字下划线属性,以0-9A-Za-z_开头并结尾的属性
const reIsPlainProp = /^\w*$/

function isKey(value, object) {
  // 数组返回false
  if (Array.isArray(value)) {
    return false
  }
  const type = typeof value
  // number boolean null symbol 返回 true
  if (type === 'number' || type === 'boolean' || value == null || isSymbol(value)) {
    return true
  }
  // 正则校验,普通属性,嵌套属性
  return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
    (object != null && value in Object(object))
}

reIsDeepProp正则校验进行代码拆分:

// 表达式1
const reIsDeepProp1 = /\[(?:[^[\]]*)\]/
// 去掉性能优化 (?:) 不捕获当前子表达式 匹配任意数量非 [ 或 ]
const reIsDeepProp2 = /\[[^[\]]*\]/
// 表达式2
const reIsDeepProp4 = /\[(?:(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/
// 去掉性能优化 (?:) 不捕获当前子表达式 非贪婪匹配"或'且未跟"或'且非\并以"或'结尾 
const reIsDeepProp5 = /\[(["'])(?!\1)[^\\]*?\1\]/
// 表达式3
const reIsDeepProp7 = /\[(["'])(?:\\.)*?\1)\]/
// 去掉性能优化 (?:) 不捕获当前子表达式 非贪婪匹配"或'\并匹配任意除换行回车以外的字符
const reIsDeepProp8 = /\[(["'])\\.*?\1)\]/

不想看上述代码的可以直接看图:

image.png

reIsPlainProp正则: image.png

isSymbol

借助getTag函数,兼容javascript早期版本

function isSymbol(value) {
  const type = typeof value
  return type == 'symbol' || (type === 'object' && value != null && getTag(value) == '[object Symbol]')
}

数组扁平化

依赖于上面的isFlattenable函数,作为是否可扁平化的校验

/**
 * The base implementation of `flatten` with support for restricting flattening.
 *
 * @private
 * @param {Array} array 需要扁平化的数组
 * @param {number} depth 最大递归深度
 * @param {boolean} [predicate=isFlattenable] The function invoked per iteration.每次迭代调用的函数
 * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks. 限制数组值通过predicate类型校验
 * @param {Array} [result=[]] The initial result value.
 * @returns {Array} Returns the new flattened array.
 */
function baseFlatten(array, depth, predicate, isStrict, result) {
  // predicate 是否可扁平化校验,可扁平化的类型,默认数组,Arguments,是否实现了Symbol.isConcatSpreadable协议
  predicate || (predicate = isFlattenable)
  // 返回值,默认空数组
  result || (result = [])

  // array 为 null 或者 undefined时直接返回空数组
  if (array == null) {
    return result
  }

  for (const value of array) {
    // 递归深度大于0 并且能够被扁平化
    if (depth > 0 && predicate(value)) {
      // 大于 1 层,递归遍历
      if (depth > 1) {
        // Recursively flatten arrays (susceptible to call stack limits).
        baseFlatten(value, depth - 1, predicate, isStrict, result)
      } else {
        // 等于 1 层 直接 解构
        result.push(...value)
      }
    // 是否强制通过predicate 扁平化校验
    } else if (!isStrict) {
      result[result.length] = value
    }
  }
  return result
}

函数结果缓存

memoize

func需要执行的函数,resolver用于生成缓存的唯一key值,未穿默认使用函数的第一个实参作为cache key。

函数结果缓存在一个Map对象上

function memoize(func, resolver) {
  if (typeof func !== 'function' || (resolver != null && typeof resolver !== 'function')) {
    throw new TypeError('Expected a function')
  }
  const memoized = function(...args) {
    // 利用提供给 memoized 的参数生成 caceh key 或者使用 第一个实参作为 caceh key
    const key = resolver ? resolver.apply(this, args) : args[0]
    // 获取cache map
    const cache = memoized.cache

    // 如果有缓存,则返回对应的缓存
    if (cache.has(key)) {
      return cache.get(key)
    }
    // 否则,执行func
    const result = func.apply(this, args)
    // 并缓存在 当前 函数引用的cache 属性上
    memoized.cache = cache.set(key, result) || cache
    return result
  }
  // memoized 的 cache 默认使用 memoize.Cache 的 Map 对象 
  memoized.cache = new (memoize.Cache || Map)
  return memoized
}

memoize.Cache = Map

export default memoize

memoizeCapped

memoize函数的基础上,限制了最大缓存数量。达到最大缓存数量的限制时,清空缓存Map对象

const MAX_MEMOIZE_SIZE = 500
function memoizeCapped(func) {
  const result = memoize(func, (key) => {
    // 获取memoize函数上的cache Map对象
    const { cache } = result
    // 达到最大缓存数量限制, 清空缓存
    if (cache.size === MAX_MEMOIZE_SIZE) {
      cache.clear()
    }
    // 返回 cache key,默认为传入参数的第一个参数的值
    return key
  })

  return result
}

工具函数

stringToPath

将一个嵌套对象路径转化为数组,如[a].b.c 转化为[a, b, c]。主要借助replace方法实现

该函数借助了memoizeCapped函数对执行结果进行缓存,提高二次执行性能

// 46
const charCodeOfDot = '.'.charCodeAt(0)
// 匹配转译字符\ 或 \\
const reEscapeChar = /\\(\\)?/g
const rePropName = RegExp(
  // Match anything that isn't a dot or bracket.
  // 非. 或 非 括号
  '[^.[\\]]+' + '|' +
  // Or match property names within brackets.
  // \\[(?:([^"'][^[]*)|("')(?:(?!\2[^\\\\]|\\\\.)*?)\\2)]
  '\\[(?:' +
    // Match a non-string expression.
    // 任意非"or非'非[
    '([^"\'][^[]*)' + '|' +
    // Or match strings (supports escaping characters).
    '(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
  ')\\]'+ '|' +
  // Or match "" as the space between consecutive dots or empty brackets.
  '(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))'
  , 'g')
const stringToPath = memoizeCapped((string) => {
  const result = []
  // 以.开头
  if (string.charCodeAt(0) === charCodeOfDot) {
    result.push('')
  }
  string.replace(rePropName, (match, expression, quote, subString) => {
    // expression ([^"'][^[])
    // quote ["']
    // subString ((?:(?!\2)[^\\]|\\.)*?)
    let key = match
    if (quote) {
      key = subString.replace(reEscapeChar, '$1')
    }
    else if (expression) {
      key = expression.trim()
    }
    result.push(key)
  })
  return result
})

rePropName 转化为正则:

// 表达式1 非. [ ]
const rePropName1 = /[^.[\]]/
// 表达式2 非" 非' 非[
// 或 非贪婪匹配" 或 ' 未跟 " 或 '非\
// 或 非贪婪匹配\任意除换行会车以外的字符
// 匹配"或'
const rePropName2 = /\[(?:([^"'][^[])|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]/
// 表达式3 正向预查 . 或者 []  . 或者 [] 或者 结束符
const rePropName3 = /(?=(?:\.|\[\])(?:\.|\[\]|$))/

对应图如下:

image.png

image.png

image.png

castPath

把传入的对象键转化为数组。调用了isKeystringToPath方法


/**
 * Casts `value` to a path array if it's not one.
 * 如果value不是路径数组则将它转换为一个路径数组
 * 
 * @private
 * @param {*} value The value to inspect.
 * @param {Object} [object] The object to query keys on.
 * @returns {Array} Returns the cast property path array.
 */
function castPath(value, object) {
  // 数组直接返回
  if (Array.isArray(value)) {
    return value
  }
  // 否则如果是嵌套属性返回嵌套数组,不是嵌套属性则直接返回[对应的值]
  return isKey(value, object) ? [value] : stringToPath(value)
}

toKey

依赖于isSymbol函数

把传入的值转换为一个key值,主要处理-0的情况。${-0} === '0'

/** Used as references for various `Number` constants. */
const INFINITY = 1 / 0

/**
 * Converts `value` to a string key if it's not a string or symbol.
 * 把value转换成一个键
 * 
 * @private
 * @param {*} value The value to inspect.
 * @returns {string|symbol} Returns the key.
 */
function toKey(value) {
  // string 或 symbol类型,直接返回
  if (typeof value === 'string' || isSymbol(value)) {
    return value
  }
  // 否则, 处理 -0 情况 后返回
  const result = `${value}`
  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result
}

baseGet

获取对象上属性的值

/**
 * The base implementation of `get` without support for default values.
 * get方法的基本实现,不返回对象默认值
 *
 * @private
 * @param {Object} object The object to query.
 * @param {Array|string} path The path of the property to get.
 * @returns {*} Returns the resolved value.
 */
function baseGet(object, path) {
  // 获取路径数组,如[prop],嵌套属性[a, b ,c]
  path = castPath(path, object)

  let index = 0
  const length = path.length

  while (object != null && index < length) {
    // tokey处理-0情况,length > 1 的时候循环处理嵌套属性
    // 对于.开头的path参数,castPath返回的数组的第一项为''
    object = object[toKey(path[index++])]
  }
  return (index && index == length) ? object : undefined
}

get

相比较于baseGet函数,仅仅增加了一个形参defaultValue

function get(object, path, defaultValue) {
  const result = object == null ? undefined : baseGet(object, path)
  return result === undefined ? defaultValue : result
}

at函数

baseAt

依赖于get函数

/**
 * The base implementation of `at` without support for individual paths.
 * at 方法的基本实现,返回对应paths的值的数组
 * 
 * @private
 * @param {Object} object The object to iterate over.
 * @param {string[]} paths The property paths to pick.
 * @returns {Array} Returns the picked elements.
 */
function baseAt(object, paths) {
  let index = -1
  const length = paths.length
  const result = new Array(length)
  // object 为 null 或者 undefined时
  const skip = object == null

  // 下标0开始依次返回对应的值
  while (++index < length) {
    result[index] = skip ? undefined : get(object, paths[index])
  }
  return result
}

at

依赖于baseAt函数和baseFlatten函数

/**
 * Creates an array of values corresponding to `paths` of `object`.
 *
 * @since 1.0.0
 * @category Object
 * @param {Object} object The object to iterate over.
 * @param {...(string|string[])} [paths] The property paths to pick.
 * @returns {Array} Returns the picked values.
 * @example
 *
 * const object = { 'a': [{ 'b': { 'c': 3 } }, 4] }
 *
 * at(object, ['a[0].b.c', 'a[1]'])
 * // => [3, 4]
 */
// baseFlatten 返回一个字符串数组,该函数可以处理字符串,类数组,一维数组和多维数组
const at = (object, ...paths) => baseAt(object, baseFlatten(paths, 1))

参考资料

正则表达式不要背