Многие увидев #Задачка_28 — "о, так здесь же можно просто взять lodash.get". И были бы правы. Но сначала рассмотрим более креативные варианты решений
Решение на позицию тех. лида 🙂
Написать конвертор путей в формат json-pointer. А потом использовать библиотеку, которая умеет доставать значения с объекта в новом формате.
Сылка на формат json-pointer— https://datatracker.ietf.org/doc/html/rfc6901
Спасибо Юре Ткаченко, мы сами про это не подумали.
Ну а если вас рассматривают на позицию человека работающего руками, то ждут что-то вроде следующих вариантов.
Почти самое краткое решение от Максима
function getWithPath(obj, propertyPath){
try {
with (obj){
return eval(propertyPath);
}
} catch(e) {}
}
Но есть проблема — eval. Вернее даже сказать, что это роковая ошибка, выстрел в колено, шаг в бездну (нужное подчеркнуть). Как нибудь сделаем про это отдельную задачку, а пока просто не используйте eval в продакшене никогда.
Если знаете как решить еще короче – пишите в комменты.
Рекурсивное решение от Станислава
function getWithPath(obj, path) {
// some edge cases
if (obj === null || obj === undefined) return obj;
// empty path, need to return the value
if (path === '') return obj;
const [current, ...steps] = path.split('.');
if (current.includes('[')) {
// it feels like property is an array
const openBracketPos = current.indexOf('[');
const closeBracketPos = current.indexOf(']');
// is it one-dimensional array?
const key = current.substring(0, openBracketPos);
// need to check for correctness, e.g. key is a valid key, indexes bounds
// not implemented through
const idx = parseInt(current.substring(openBracketPos + 1, closeBracketPos));
// here should be NaN check
if (key) {
// take a value from current key and call itself with next steps
return getWithPath(obj[key][idx], current.substring(closeBracketPos + 1) + steps.join('.'));
} else {
// thats an multidimensional array, lets unwind it
return getWithPath(obj[idx], current.substring(closeBracketPos + 1) + steps.join('.'));
}
} else {
// this is a regular property
// take a value from current key and call itself with next steps
return getWithPath(obj[current], steps.join('.'));
}
}
Заменяем рекурсию на цикл. Смотрите сами:
const getWithPath = (object: Object, path: string) => {
return path.split('.').reduce((acc, currentPath) => {
if (!acc) return acc;
const property = currentPath.split('[')[0];
const indexes = currentPath.match(/(?<=\[).+?(?=])/g) ?? [];
return indexes.reduce((acc, i) => acc && acc[i], acc[property]);
}, object);
};
Обратите внимание, как круто достаём индексы массива с помощью регулярного выражения:
/(?<=\[).+?(?=])/g
Вариант очень хороший, его прислал Женя.
Давайте посмотрим алгоритм для идеального случая:
- Преобразуем строку path в масив путей (https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L3029)
path = castPath(path, object);
- Итеративно пройдемся по массиву путей, доставая значение вложеных объектов (https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L3034)
var index = 0, length = path.length; while (object != null && index < length) { object = object[toKey(path[index++])]; }
- Вернем значение, если оно есть. В противном случае — undefined (https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L3037)
return (index && index == length) ? object : undefined;
Как вы могли заметить, самое интересное — этап преобразования. Каким образом он реализован? В его основе лежит функция replace и магический regex.(https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L6740)
var result = [];
var rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
var reEscapeChar = /\\(\\)?/g;
string.replace(rePropName, function(match, number, quote, subString) {
result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match));
});
Если посмотреть внимательно, то можно многое узнать о методе replace. Например:
- replace первым параметром принимает не только строку, а также регулярные выражение
- вторым параметром может быть функция-преобразователь
- специальные строки типа "$&", "$`" и другие также могут выступать вторым параметром — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter
Но есть ли альтернативный вариант преобразование path? Самое время ещё раз передать привет подписчикам и привести их примеры:
- Саша решил сделать массив путей через split. Используя регулярное выражение и функцию filter:
path = path.split(/[.\[\]]/).filter(Boolean);
- Виталик заменил символы "[" , "]" на точки и сделал split:
path = path.replace(/[\[\]\.]+/g, ".").replace(/\.$/g, "").split('.')
Так и закончим разбор. Спасибо, что присылаете свои варианты решений