lodash源码解读之get

2,654 阅读4分钟

作用以及使用场景

lodash中的get方法就是获取一个对象的值,在es的可选链操作符还未出来的时候,又没有使用ts的情况下,一般获取对象的值就是:object.a.b.c...,如果在对象中没有这个某个属性,而又直接获取值的时候,就会抛出错误,影响程序执行,为了提高代码健壮性,会用if添加判断,或则使用&&,||等逻辑运算符,但是,在对象的层级比较深的情况下,代码可读性就会变差,所以,使用lodashget方法,就简明很多

用法

直接通过CDN引入lodash库或则通过node_modules的方式引入,使用方式很简单,直接看源码:

import baseGet from './.internal/baseGet.js'

/**
 * Gets the value at `path` of `object`. If the resolved value is
 * `undefined`, the `defaultValue` is returned in its place.
 *
 * @since 3.7.0
 * @category Object
 * @param {Object} object The object to query.
 * @param {Array|string} path The path of the property to get.
 * @param {*} [defaultValue] The value returned for `undefined` resolved values.
 * @returns {*} Returns the resolved value.
 * @see has, hasIn, set, unset
 * @example
 *
 * const object = { 'a': [{ 'b': { 'c': 3 } }] }
 *
 * get(object, 'a[0].b.c')
 * // => 3
 *
 * get(object, ['a', '0', 'b', 'c'])
 * // => 3
 *
 * get(object, 'a.b.c', 'default')
 * // => 'default'
 */
function get(object, path, defaultValue) {
  const result = object == null ? undefined : baseGet(object, path)
  return result === undefined ? defaultValue : result
}

export default get

从源码中不难看出,get方法接受三个参数,

  • object表示需要取值的对象
  • path就是取值的路径,可以是字符串,也可以是数组
  • defaultValue就是在取值失败时(obejct中不包含path路径中的值,或则objectnull时),返回的值 可以看到,当obejct == null时,会返回defaultValue的值,如果没传defaultValue,就是undefined
    obejct是正常对象时,会调用baseGet方法传入obejctpath,看下baseGet的代码:
import castPath from './castPath.js'
import toKey from './toKey.js'

/**
 * The base implementation of `get` without support for default values.
 *
 * @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) {
  path = castPath(path, object)

  let index = 0
  const length = path.length

  while (object != null && index < length) {
    object = object[toKey(path[index++])]
  }
  return (index && index == length) ? object : undefined
}

export default baseGet

castPath的作用就是将path转换成数组类型,比如,传入'a[0].b.c',转换之后就是['a','0','b','c'],具体的转换规则后面再解释
接着往下,就是循环取值了,在while循环中,有个陌生的方法,toKey,看下toKey的代码:

import isSymbol from '../isSymbol.js'

/** 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.
 *
 * @private
 * @param {*} value The value to inspect.
 * @returns {string|symbol} Returns the key.
 */
function toKey(value) {
  if (typeof value === 'string' || isSymbol(value)) {
    return value
  }
  const result = `${value}`
  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result
}

export default toKey

如果传入toKey的值(后面直接称value)为字符串或则满足isSymbol,就直接返回value的值,否则将value值变为字符串在返回,${value}使用字符串模版,就是将value转为字符串了,(result == '0' && (1 / value) == -INFINITY) ? '-0' : result这里的意义在于区别0-0产生的影响,比如上面的例子,path['a',0,'b','c']就能取出对应的值,但是如果为['a',-0,'b','c'],就无法取出object中的值

综上来看,toKey就是将key值转为字符串的一个方法

key转为字符串之后,通过object = object[toKey(path[index++])]将旧的object值覆盖,如果object中没有当前key属性,则不满足 object != null,循环结束,或则当index === path.length的时候,循环结束,此时的object就为需要取的值
return (index && index == length) ? object : undefined,在返回结果中,条件代表正常从object中取到值,就返回object(此时的object就是我们需要取的值),否则,取值失败,统一返回undefined
我们正常使用object.a[0].b.c取值,如果在object中没有a属性,那么object.a的值就为undefined,此时再使用undefined[0]取值,就会报错了,程序中断
现在来看下上面提到的castPath方法:

import isKey from './isKey.js'
import stringToPath from './stringToPath.js'

/**
 * Casts `value` to a path array if it's not one.
 *
 * @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)
}

export default castPath

前面提到,castPath方法是将字符串等路径表示方式转为数组,所以如果value是数组,就直接返回
如果满足isKey,就直接返回[value],否则就返回stringToPath(value),看下isKey中的代码:

import isSymbol from '../isSymbol.js'

/** Used to match property names within property paths. */
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/
const reIsPlainProp = /^\w*$/

/**
 * Checks if `value` is a property name and not a property path.
 *
 * @private
 * @param {*} value The value to check.
 * @param {Object} [object] The object to query keys on.
 * @returns {boolean} Returns `true` if `value` is a property name, else `false`.
 */
function isKey(value, object) {
  if (Array.isArray(value)) {
    return false
  }
  const type = typeof value
  if (type === 'number' || type === 'boolean' || value == null || isSymbol(value)) {
    return true
  }
  return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
    (object != null && value in Object(object))
}

export default isKey

isSymbol很简单,就是判断元素是都是Symbol类型,所以

  • 如果valuenumber、boolean、Symbol类型,或则value值为nullundefined就返回true
  • reIsDeepProp.test(value)正则匹配,会匹配到'a.b.c''a[0].b.c'这些类型
  • reIsPlainProp.test(value)正则匹配,只匹配数字、字母、下划线
  • object不为 null,并且object中存在value属性,也就是可通过object[value]取到值 综上所述:当valuenumber、boolean、Symbol类型,或值 == null或则字符串中不包含[],.或则为纯字母、数字、下划线的,或则object中存在value属性的,isKey都会返回true,其实也就是当path没有如a.b.ca[0]之类的,都认为是取得object的第一层
    如果patha[0].b.c之类的,是取得的object中层级比较深的呢,就会返回stringToPath(value)的值,看下stringToPath方法:
import memoizeCapped from './memoizeCapped.js'

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.
  '\\[(?:' +
    // Match a non-string expression.
    '([^"\'][^[]*)' + '|' +
    // Or match strings (supports escaping characters).
    '(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
  ')\\]'+ '|' +
  // Or match "" as the space between consecutive dots or empty brackets.
  '(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))'
  , 'g')

/**
 * Converts `string` to a property path array.
 *
 * @private
 * @param {string} string The string to convert.
 * @returns {Array} Returns the property path array.
 */
const stringToPath = memoizeCapped((string) => {
  const result = []
  if (string.charCodeAt(0) === charCodeOfDot) {
    result.push('')
  }
  string.replace(rePropName, (match, expression, quote, subString) => {
    let key = match
    if (quote) {
      key = subString.replace(reEscapeChar, '$1')
    }
    else if (expression) {
      key = expression.trim()
    }
    result.push(key)
  })
  return result
})

export default stringToPath

charCodeOfDot表示的是'.'Unicode 编码,也就是46,所以当path字符串的首位是.的时候,就表示对象的键是空字符串(往result中添加了一个空的字符串),rePropName生成的正则表达式为/[^.[\]]+|\[(?:([^"'][^[]*)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,其目的就是为了将path中的[]等字符去掉,将a[0].b.c这种类型的转为['a','0','b','c']类型的,最终得到数组,进入到baseGet中的while循环中
这里值得一提的就是字符串的replace方法的第二个参数是函数的情况,如果第二个参数是一个函数,它将在每个匹配结果上调用,它返回的字符串将作为替换文本。其接收四个参数:

  • 匹配该模式的字符串
  • 匹配该模式中某个圆括号子表达式的字符串,可能是0个或多个这样的参数
  • 整数,指定String中出现匹配结果的位置
  • string本身