# 一、题目
实现 lodash 中的 get 函数【难度⭐,对标百度 T4,阿里 P5,腾讯T2.2】
// var object = { 'a': [{ 'b': { 'c': 3 } }] };
// get(object, 'a[0].b.c'); // 3
// get(object, ['a', '0', 'b', 'c']); // 3
const get = (data, path, defaultValue = void 0)
=> {
// todo
}
# 二、题目情景与分析
情景 | 效果 |
---|---|
object.a.b.c | ❌ |
_get(object,'a[0].b.c',null) | ✅ |
object?.a?.b?.c | ✅ |
# 三、手写
const get = (data, path, defaultValue = 0)
=> {
// 'a[0].b.c' ==> 'a.0.b.c' ==> ['a', '0', 'b', 'c']
const regPath = path.replace(/\[(\d+)\]/g, '.$1').split('.');
let result = data;
for(const path of regPath){
result = Object(result)[path];
if(result == null){
return defaultValue;
}
}
return result;
}
- 考察点1:处理数组情况,使用正则表达式将数组元素
[num]
替换成.num
- 考察点2:正则表达式书写,
- 考察点3:处理 data 未定义状况,之所以要包一层Object,因为null 与 undefined 取属性会报错,所以使用 Object 包装一下,另外可以借助可选链操作简化
result = result?.[path]
# 四、lodash源码分析 - 环境准备
A modern JavaScript utility library delivering modularity, performance & extras.
lodash
是一个一致性、模块化、高性能的JavaScript
实用工具库
lodash
版本v4.0.0
通过
github1s
网页可以 查看 (opens new window)lodash - get
源码调试测试用例可以
clone
到本地
git clone https://github.com/lodash/lodash.git
cd axios
npm install
npm run test
# 五、lodash源码分析 - 结构分析
这是一张 get
依赖引用路径图,相对复杂一些,按照功能划分,大致包括 tokey
模块、 castPath
模块,分别用于检验 key
值是否合法、计算 path
。整体流程和我们手写的相差不大,主要是功能方面做了扩展,比如不仅可以实现 a[0]
还可以实现 a[0][0]
这样的操作,并且深度过大时会开启 Map
缓存,下面挑其中的重点讲一下。
# 六、lodash源码分析 - 函数研读
# 1. castPath 模块
如果值不是数组,则将其强制转换路径数组
import isKey from './isKey.js'
import stringToPath from './stringToPath.js'
/**
* @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
isKey
判断path
是不是当前对象的key
- stringToPath 如果传入的 path 不是当前对象的 key,就调用该方法把字符串转成对应的数组
# 2. isKey 模块
检查'value'是否是属性名而不是属性路径
import isSymbol from '../isSymbol.js'
/** 用于匹配属性路径中的属性名称 **/
const reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/
const reIsPlainProp = /^\w*$/
/**
* @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
reIsDeepProp
用于界定分隔内容,比如会匹配到a.b[1].c[2]
中的.
、[1]
、[2]
reIsPlainProp
用于匹配0-n
个字符,目的是匹配属性名
# 3. stringToPath 模块
import memoizeCapped from './memoizeCapped.js'
const charCodeOfDot = '.'.charCodeAt(0)
const reEscapeChar = /\\(\\)?/g
const rePropName = RegExp(
// 匹配任何不是点或括号的内容
'[^.[\\]]+' + '|' +
// 或者在括号内匹配属性名称
'\\[(?:' +
// 匹配非字符串表达式
'([^"\'][^[]*)' + '|' +
// 或匹配字符串(支持转义字符)
'(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
')\\]'+ '|' +
// 或将""匹配为连续点或空括号之间的间距
'(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))'
, 'g')
/**
* 将'string'转换为属性路径数组
* @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
- 关键是看懂
rePropName
这个正则表达式 [^.[\\]]+
匹配任何不是点或括号的内容,其中\\]
用于对]
转义,可以理解为[^.]+
或[^[\\]]+
\\[(?:
匹配[(
其中?:
表示非捕获性分组(不保存子组)(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2
,这个比较复杂了,其中(?!)
表示Case-Insensitive
即不区分大小写模式,\2
表示分组引用,比如(1)(2)(3)\2
表示引用第二个分组(2)
🐶