作用以及使用场景
lodash
中的get
方法就是获取一个对象的值,在es
的可选链操作符还未出来的时候,又没有使用ts
的情况下,一般获取对象的值就是:object.a.b.c...
,如果在对象中没有这个某个属性,而又直接获取值的时候,就会抛出错误,影响程序执行,为了提高代码健壮性,会用if
添加判断,或则使用&&,||
等逻辑运算符,但是,在对象的层级比较深的情况下,代码可读性就会变差,所以,使用lodash
的get
方法,就简明很多
用法
直接通过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
路径中的值,或则object
为null
时),返回的值 可以看到,当obejct == null
时,会返回defaultValue
的值,如果没传defaultValue
,就是undefined
当obejct
是正常对象时,会调用baseGet
方法传入obejct
和path
,看下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
类型,所以
- 如果
value
是number、boolean、Symbol
类型,或则value
值为null
或undefined
就返回true reIsDeepProp.test(value)
正则匹配,会匹配到'a.b.c'
,'a[0].b.c'
这些类型reIsPlainProp.test(value)
正则匹配,只匹配数字、字母、下划线object
不为null
,并且object
中存在value
属性,也就是可通过object[value]
取到值 综上所述:当value
为number、boolean、Symbol
类型,或值== null
或则字符串中不包含[]
,.
或则为纯字母、数字、下划线的,或则object
中存在value
属性的,isKey
都会返回true,其实也就是当path没有如a.b.c
和a[0]
之类的,都认为是取得object
的第一层
如果path
是a[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
本身