Доступ к вложенным объектам JavaScript с помощью строкового ключа
У меня есть структура данных, как это:
var someObject = {
'part1' : {
'name': 'Part 1',
'size': '20',
'qty' : '50'
},
'part2' : {
'name': 'Part 2',
'size': '15',
'qty' : '60'
},
'part3' : [
{
'name': 'Part 3A',
'size': '10',
'qty' : '20'
}, {
'name': 'Part 3B',
'size': '5',
'qty' : '20'
}, {
'name': 'Part 3C',
'size': '7.5',
'qty' : '20'
}
]
};
И я хотел бы получить доступ к данным, используя эти переменные:
var part1name = "part1.name";
var part2quantity = "part2.qty";
var part3name1 = "part3[0].name";
part1name должно быть заполнено someObject.part1.name
Значение, которое является "Часть 1". То же самое с part2quantity, который заполнен 60.
Есть ли способ достичь этого с помощью чистого JavaScript или JQuery?
46 ответов
Я только что сделал это на основе некоторого похожего кода, который у меня уже был, похоже, он работает:
Object.byString = function(o, s) {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
s = s.replace(/^\./, ''); // strip a leading dot
var a = s.split('.');
for (var i = 0, n = a.length; i < n; ++i) {
var k = a[i];
if (k in o) {
o = o[k];
} else {
return;
}
}
return o;
}
Использование::
Object.byString(someObj, 'part3[0].name');
Смотрите рабочую демонстрацию на http://jsfiddle.net/alnitak/hEsys/
EDIT некоторые заметили, что этот код выдаст ошибку, если передать строку, где крайние левые индексы не соответствуют правильно вложенной записи в объекте. Это действительная проблема, но ИМХО лучше решать с try / catch
блокировать при вызове, вместо того, чтобы эта функция молча возвращала undefined
для неверного индекса.
Теперь это поддерживается с помощью lodash _.get(obj, property)
, Смотрите https://lodash.com/docs
Пример из документов:
var 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 resolve(path, obj=self, separator='.') {
var properties = Array.isArray(path) ? path : path.split(separator)
return properties.reduce((prev, curr) => prev && prev[curr], obj)
}
Пример использования:
// accessing property path on global scope
resolve("document.body.style.width")
// or
resolve("style.width", document.body)
// accessing array indexes
// (someObject has been defined in the question)
resolve("part3.0.size", someObject) // returns '10'
// accessing non-existent properties
// returns undefined when intermediate properties are not defined:
resolve('properties.that.do.not.exist', {hello:'world'})
// accessing properties with unusual keys by changing the separator
var obj = { object: { 'a.property.name.with.periods': 42 } }
resolve('object->a.property.name.with.periods', obj, '->') // returns 42
// accessing properties with unusual keys by passing a property name array
resolve(['object', 'a.property.name.with.periods'], obj) // returns 42
Ограничения:
- Не могу использовать скобки (
[]
) для индексов массива - хотя указание индексов массива между токеном-разделителем (например,.
) отлично работает, как показано выше.
ES6: только одна строка в Vanila JS (она возвращает ноль, если не находит вместо выдачи ошибки):
'path.string'.split('.').reduce((p,c)=>p&&p[c]||null, MyOBJ)
или пример:
'a.b.c'.split('.').reduce((p,c)=>p&&p[c]||null, {a:{b:{c:1}}})
Для готовой к использованию функции, которая также распознает false, 0 и отрицательное число и принимает значения по умолчанию в качестве параметра:
const resolvePath = (object, path, defaultValue) => path
.split('.')
.reduce((o, p) => o ? o[p] : defaultValue, object)
Пример для использования:
resolvePath(window,'document.body') => <body>
resolvePath(window,'document.body.xyz') => undefined
resolvePath(window,'document.body.xyz', null) => null
resolvePath(window,'document.body.xyz', 1) => 1
Бонус:
Чтобы установить путь (запрашивается @rob-gordon), вы можете использовать:
const setPath = (object, path, value) => path
.split('.')
.reduce((o,p) => o[p] = path.split('.').pop() === p ? value : o[p] || {}, object)
Пример:
let myVar = {}
setPath(myVar, 'a.b.c', 42) => 42
console.log(myVar) => {a: {b: {c: 42}}}
Доступ к массиву с помощью []:
const resolvePath = (object, path, defaultValue) => path
.split(/[\.\[\]\'\"]/)
.filter(p => p)
.reduce((o, p) => o ? o[p] : defaultValue, object)
Exemple
const myVar = {a:{b:[{c:1}]}}
resolvePath(myVar,'a.b[0].c') => 1
resolvePath(myVar,'a["b"][\'0\'].c') => 1
Вы должны разобрать строку самостоятельно:
function getProperty(obj, prop) {
var parts = prop.split('.');
if (Array.isArray(parts)) {
var last = parts.pop(),
l = parts.length,
i = 1,
current = parts[0];
while((obj = obj[current]) && i < l) {
current = parts[i];
i++;
}
if(obj) {
return obj[last];
}
} else {
throw 'parts is not valid array';
}
}
Для этого необходимо также определить индексы массива с точечной нотацией:
var part3name1 = "part3.0.name";
Это облегчает анализ.
Работает для массивов / массивов внутри объекта также. Защита от недопустимых значений.
/**
* Retrieve nested item from object/array
* @param {Object|Array} obj
* @param {String} path dot separated
* @param {*} def default value ( if result undefined )
* @returns {*}
*/
function path(obj, path, def){
var i, len;
for(i = 0,path = path.split('.'), len = path.length; i < len; i++){
if(!obj || typeof obj !== 'object') return def;
obj = obj[path[i]];
}
if(obj === undefined) return def;
return obj;
}
//////////////////////////
// TEST //
//////////////////////////
var arr = [true, {'sp ace': true}, true]
var obj = {
'sp ace': true,
arr: arr,
nested: {'dotted.str.ing': true},
arr3: arr
}
shouldThrow(`path(obj, "arr.0")`);
shouldBeDefined(`path(obj, "arr[0]")`);
shouldBeEqualToNumber(`path(obj, "arr.length")`, 3);
shouldBeTrue(`path(obj, "sp ace")`);
shouldBeEqualToString(`path(obj, "none.existed.prop", "fallback")`, "fallback");
shouldBeTrue(`path(obj, "nested['dotted.str.ing'])`);
<script src="https://cdn.rawgit.com/coderek/e7b30bac7634a50ad8fd/raw/174b6634c8f57aa8aac0716c5b7b2a7098e03584/js-test.js"></script>
Это, вероятно, никогда не увидит свет... но здесь это в любом случае.
- замещать
[]
Синтаксис скобки с.
- Разделить на
.
персонаж - Удалить пустые строки
- Найти путь (в противном случае
undefined
)
// "one liner" (ES6)
const deep_value = (obj, path) =>
path
.replace(/\[|\]\.?/g, '.')
.split('.')
.filter(s => s)
.reduce((acc, val) => acc && acc[val], obj);
// ... and that's it.
var someObject = {
'part1' : {
'name': 'Part 1',
'size': '20',
'qty' : '50'
},
'part2' : {
'name': 'Part 2',
'size': '15',
'qty' : '60'
},
'part3' : [
{
'name': 'Part 3A',
'size': '10',
'qty' : '20'
}
// ...
]
};
console.log(deep_value(someObject, "part1.name")); // Part 1
console.log(deep_value(someObject, "part2.qty")); // 60
console.log(deep_value(someObject, "part3[0].name")); // Part 3A
Используя eval:
var part1name = eval("someObject.part1.name");
обернуть, чтобы вернуть неопределенное при ошибке
function path(obj, path) {
try {
return eval("obj." + path);
} catch(e) {
return undefined;
}
}
http://jsfiddle.net/shanimal/b3xTw/
Пожалуйста, руководствуйтесь здравым смыслом и осторожностью при использовании силы пробуждения. Это немного похоже на легкую саблю, если вы включите ее, есть 90% -ный шанс, что вы отрежете конечность. Это не для всех.
Вы можете получить значение элемента глубокого объекта с точечной нотацией без какой-либо внешней библиотеки JavaScript с помощью простого трюка:
new Function('_', 'return _.' + path)(obj);
В вашем случае, чтобы получить значение part1.name
от someObject
просто делать:
new Function('_', 'return _.part1.name')(someObject);
Вот простая демонстрация скрипки: https://jsfiddle.net/harishanchu/oq5esowf/
Это один лайнер с lodash.
const deep = { l1: { l2: { l3: "Hello" } } };
const prop = "l1.l2.l3";
const val = _.reduce(prop.split('.'), function(result, value) { return result ? result[value] : undefined; }, deep);
// val === "Hello"
Или даже лучше...
const val = _.get(deep, prop);
Или версия ES6 с уменьшением...
const val = prop.split('.').reduce((r, val) => { return r ? r[val] : undefined; }, deep);
Я думаю, что вы просите об этом:
var part1name = someObject.part1.name;
var part2quantity = someObject.part2.qty;
var part3name1 = someObject.part3[0].name;
Вы могли бы просить об этом:
var part1name = someObject["part1"]["name"];
var part2quantity = someObject["part2"]["qty"];
var part3name1 = someObject["part3"][0]["name"];
Оба из которых будут работать
Или, может быть, вы просите об этом
var partName = "part1";
var nameStr = "name";
var part1name = someObject[partName][nameStr];
Наконец, вы могли бы попросить об этом
var partName = "part1.name";
var partBits = partName.split(".");
var part1name = someObject[partBits[0]][partBits[1]];
Вместо того, чтобы пытаться имитировать синтаксис JS, вам придется потратить кучу вычислений на синтаксический анализ или просто ошибиться / забыть такие вещи, как связка этих ответов (ключи с
.
s in, кто-нибудь?), просто используйте массив ключей.
Если вместо этого вам нужно использовать одну строку, просто JSONify ее.
Еще одним улучшением этого метода является то, что вы можете удалить / установить объект корневого уровня.
function resolve(obj, path) {
let root = obj = [obj];
path = [0, ...path];
while (path.length > 1)
obj = obj[path.shift()];
return [obj, path[0], root];
}
Object.get = (obj, path) => {
let [parent, key] = resolve(obj, path);
return parent[key];
};
Object.del = (obj, path) => {
let [parent, key, root] = resolve(obj, path);
delete parent[key];
return root[0];
};
Object.set = (obj, path, value) => {
let [parent, key, root] = resolve(obj, path);
parent[key] = value;
return root[0];
};
В
bob =
для /
.del(
не требуется, если ваш путь не может быть пустым (манипулирование корневым объектом).
Я доказываю, что не клонирую объект, используя
steve
сохранить ссылку на оригинал и проверить
bob == steve //true
после этого первого
.set(
На всякий случай, если кто-нибудь посетит этот вопрос в 2017 году или позже и ищет легкий для запоминания способ, вот подробный пост в блоге о доступе к вложенным объектам в JavaScript без обмана со стороны
Невозможно прочитать свойство 'foo' с неопределенной ошибкой
Доступ к вложенным объектам с помощью Array Reduce
Давайте возьмем этот пример структуры
const user = {
id: 101,
email: 'jack@dev.com',
personalInfo: {
name: 'Jack',
address: [{
line1: 'westwish st',
line2: 'washmasher',
city: 'wallas',
state: 'WX'
}]
}
}
Чтобы иметь доступ к вложенным массивам, вы можете написать свой собственный массив execute.
const getNestedObject = (nestedObj, pathArr) => {
return pathArr.reduce((obj, key) =>
(obj && obj[key] !== 'undefined') ? obj[key] : undefined, nestedObj);
}
// pass in your object structure as array elements
const name = getNestedObject(user, ['personalInfo', 'name']);
// to access nested array, just pass in array index as an element the path array.
const city = getNestedObject(user, ['personalInfo', 'address', 0, 'city']);
// this will return the city from the first address item.
Существует также превосходная обработка типов минимальной библиотеки типов, которая сделает все это за вас.
С typy ваш код будет выглядеть так
const city = t(user, 'personalInfo.address[0].city').safeObject;
Отказ от ответственности: я являюсь автором этого пакета.
Здесь я предлагаю больше способов, которые кажутся более быстрыми во многих отношениях:
Вариант 1: разделить строку на. или [или] или 'или ", переверните его, пропустите пустые элементы.
function getValue(path, origin) {
if (origin === void 0 || origin === null) origin = self ? self : this;
if (typeof path !== 'string') path = '' + path;
var parts = path.split(/\[|\]|\.|'|"/g).reverse(), name; // (why reverse? because it's usually faster to pop off the end of an array)
while (parts.length) { name=parts.pop(); if (name) origin=origin[name]; }
return origin;
}
Вариант 2 (самый быстрый, кроме eval
): Низкоуровневое сканирование символов (без регулярных выражений /split/etc, просто быстрое сканирование символов).Примечание: этот не поддерживает кавычки для индексов.
function getValue(path, origin) {
if (origin === void 0 || origin === null) origin = self ? self : this;
if (typeof path !== 'string') path = '' + path;
var c = '', pc, i = 0, n = path.length, name = '';
if (n) while (i<=n) ((c = path[i++]) == '.' || c == '[' || c == ']' || c == void 0) ? (name?(origin = origin[name], name = ''):(pc=='.'||pc=='['||pc==']'&&c==']'?i=n+2:void 0),pc=c) : name += c;
if (i==n+2) throw "Invalid path: "+path;
return origin;
} // (around 1,000,000+/- ops/sec)
Вариант 3: (новый: расширен вариант 2 для поддержки кавычек - немного медленнее, но все же быстро)
function getValue(path, origin) {
if (origin === void 0 || origin === null) origin = self ? self : this;
if (typeof path !== 'string') path = '' + path;
var c, pc, i = 0, n = path.length, name = '', q;
while (i<=n)
((c = path[i++]) == '.' || c == '[' || c == ']' || c == "'" || c == '"' || c == void 0) ? (c==q&&path[i]==']'?q='':q?name+=c:name?(origin?origin=origin[name]:i=n+2,name='') : (pc=='['&&(c=='"'||c=="'")?q=c:pc=='.'||pc=='['||pc==']'&&c==']'||pc=='"'||pc=="'"?i=n+2:void 0), pc=c) : name += c;
if (i==n+2 || name) throw "Invalid path: "+path;
return origin;
}
JSPerf: http://jsperf.com/ways-to-dereference-a-delimited-property-string/3
"eval (...)" все еще король (хотя производительность). Если у вас есть пути к свойствам непосредственно под вашим контролем, не должно быть никаких проблем с использованием 'eval' (особенно, если требуется скорость). Если вы перетаскиваете пути свойств "по проводу" ( на линии!? Lol:P), тогда да, используйте что-то еще, чтобы быть в безопасности. Только идиот сказал бы, что никогда не должен использовать "eval", поскольку есть веские причины, когда его использовать. Кроме того, "Он используется в анализаторе JSON Дуга Крокфорда". Если ввод безопасен, то проблем нет вообще. Используйте правильный инструмент для правильной работы, вот и все.
Подход Спейгга очень аккуратный и чистый, хотя я нашел этот ответ, когда искал решение для доступа к свойствам области видимости AngularJS $ по строковому пути, и с небольшой модификацией он выполняет свою работу:
$scope.resolve = function( path, obj ) {
return path.split('.').reduce( function( prev, curr ) {
return prev[curr];
}, obj || this );
}
Просто поместите эту функцию в свой корневой контроллер и используйте любую дочернюю область, например:
$scope.resolve( 'path.to.any.object.in.scope')
Если вам нужно решение, которое может правильно обнаруживать и сообщать подробную информацию о любой проблеме с анализом пути, я написал свое собственное решение для этого - значение пути к библиотеке .
const {resolveValue} = require('path-value');
resolveValue(someObject, 'part1.name'); //=> Part 1
resolveValue(someObject, 'part2.qty'); //=> 50
resolveValue(someObject, 'part3.0.name'); //=> Part 3A
Обратите внимание, что для индексов мы используем, а не
[0]
, потому что синтаксический анализ последнего добавляет потери производительности, в то время как
.0
работает непосредственно в JavaScript и поэтому работает очень быстро.
Однако также есть поддержка полного синтаксиса JavaScript ES5. Его просто нужно токенизировать:
const {resolveValue, tokenizePath} = require('path-value');
const path = tokenizePath('part3[0].name');
//=> ['part3', '0', 'name']
resolveValue(someObject, path); //=> Part 3A
/**
* Access a deep value inside a object
* Works by passing a path like "foo.bar", also works with nested arrays like "foo[0][1].baz"
* @author Victor B. https://gist.github.com/victornpb/4c7882c1b9d36292308e
* Unit tests: http://jsfiddle.net/Victornpb/0u1qygrh/
*/
function getDeepVal(obj, path) {
if (typeof obj === "undefined" || obj === null) return;
path = path.split(/[\.\[\]\"\']{1,2}/);
for (var i = 0, l = path.length; i < l; i++) {
if (path[i] === "") continue;
obj = obj[path[i]];
if (typeof obj === "undefined" || obj === null) return;
}
return obj;
}
Работает с
getDeepVal(obj,'foo.bar')
getDeepVal(obj,'foo.1.bar')
getDeepVal(obj,'foo[0].baz')
getDeepVal(obj,'foo[1][2]')
getDeepVal(obj,"foo['bar'].baz")
getDeepVal(obj,"foo['bar']['baz']")
getDeepVal(obj,"foo.bar.0.baz[1]['2']['w'].aaa[\"f\"].bb")
Я еще не нашел пакет, который выполнял бы все операции со строковым путем, поэтому я в итоге написал свой собственный маленький быстрый пакет, который поддерживает insert (), get () (с возвратом по умолчанию), set () и remove () операции.
Вы можете использовать точечные обозначения, скобки, числовые индексы, свойства строковых чисел и ключи с несловесными символами. Простое использование ниже:
> var jsocrud = require('jsocrud');
...
// Get (Read) ---
> var obj = {
> foo: [
> {
> 'key w/ non-word chars': 'bar'
> }
> ]
> };
undefined
> jsocrud.get(obj, '.foo[0]["key w/ non-word chars"]');
'bar'
Простая функция, допускающая либо строку, либо путь к массиву.
function get(obj, path) {
if(typeof path === 'string') path = path.split('.');
if(path.length === 0) return obj;
return get(obj[path[0]], path.slice(1));
}
const obj = {a: {b: {c: 'foo'}}};
console.log(get(obj, 'a.b.c')); //foo
ИЛИ ЖЕ
console.log(get(obj, ['a', 'b', 'c'])); //foo
Я занимаюсь разработкой интернет-магазина с React. Я попытался изменить значения в скопированном объекте состояния, чтобы обновить исходное состояние с ним при отправке. Приведенные выше примеры не работают для меня, потому что большинство из них изменяют структуру копируемого объекта. Я нашел рабочий пример функции для доступа и изменения значений свойств глубоко вложенного объекта: https://lowrey.me/create-an-object-by-path-in-javascript-2/ Вот это:
const createPath = (obj, path, value = null) => {
path = typeof path === 'string' ? path.split('.') : path;
let current = obj;
while (path.length > 1) {
const [head, ...tail] = path;
path = tail;
if (current[head] === undefined) {
current[head] = {};
}
current = current[head];
}
current[path[0]] = value;
return obj;
};
Есть npm
Модуль сейчас для этого: https://github.com/erictrinh/safe-access
Пример использования:
var access = require('safe-access');
access(very, 'nested.property.and.array[0]');
Вы можете использовать ramda
библиотека.
Обучение ramda
также помогает легко работать с неизменяемыми объектами.
var obj = {
a:{
b: {
c:[100,101,{
d: 1000
}]
}
}
};
var lens = R.lensPath('a.b.c.2.d'.split('.'));
var result = R.view(lens, obj);
Хотя уменьшение это хорошо, я удивлен, что никто не использовал forEach:
function valueForKeyPath(obj, path){
const keys = path.split('.');
keys.forEach((key)=> obj = obj[key]);
return obj;
};
Это можно упростить, разделив логику на три отдельные функции:
const isVal = a => a != null; // everything except undefined + null
const prop = prop => obj => {
if (isVal(obj)) {
const value = obj[prop];
if (isVal(value)) return value;
else return undefined;
} else return undefined;
};
const path = paths => obj => {
const pathList = typeof paths === 'string' ? paths.split('.') : paths;
return pathList.reduce((value, key) => prop(key)(value), obj);
};
Этот вариант поддерживает:
- передача аргумента массива или строки
- иметь дело с
undefined
значения во время вызова и выполнения - тестирование каждой функции независимо
- использование каждой функции независимо
На основании ответа Альнитака.
Я завернул полифилл в чек и свел функцию к единственной цепочке сокращения.
if (Object.byPath === undefined) {
Object.byPath = (obj, path) => path
.replace(/\[(\w+)\]/g, '.$1')
.replace(/^\./, '')
.split(/\./g)
.reduce((ref, key) => key in ref ? ref[key] : ref, obj)
}
const data = {
foo: {
bar: [{
baz: 1
}]
}
}
console.log(Object.byPath(data, 'foo.bar[0].baz'))
Если вам нужно получить доступ к другому вложенному ключу, не зная его во время кодирования (это будет тривиально для их обращения), вы можете использовать средство доступа к записи массива:
var part1name = someObject['part1']['name'];
var part2quantity = someObject['part2']['qty'];
var part3name1 = someObject['part3'][0]['name'];
Они эквивалентны средству доступа к точечной нотации и могут изменяться во время выполнения, например:
var part = 'part1';
var property = 'name';
var part1name = someObject[part][property];
эквивалентно
var part1name = someObject['part1']['name'];
или же
var part1name = someObject.part1.name;
Я надеюсь, что это ответить на ваш вопрос...
РЕДАКТИРОВАТЬ
Я не буду использовать строку для поддержки своего рода запроса xpath для доступа к значению объекта. Так как вам нужно вызвать функцию для анализа запроса и получения значения, я бы пошел по другому пути (не:
var part1name = function(){ return this.part1.name; }
var part2quantity = function() { return this['part2']['qty']; }
var part3name1 = function() { return this.part3[0]['name'];}
// usage: part1name.apply(someObject);
или, если вас не устраивает метод применения
var part1name = function(obj){ return obj.part1.name; }
var part2quantity = function(obj) { return obj['part2']['qty']; }
var part3name1 = function(obj) { return obj.part3[0]['name'];}
// usage: part1name(someObject);
Функции короче, понятнее, интерпретатор проверяет их на наличие синтаксических ошибок и так далее.
Кстати, я чувствую, что простого задания, сделанного в нужное время, будет достаточно...
Я просмотрел все остальные ответы, решил добавить улучшения в более читаемый код:
function getObjectValByString(obj, str) {
if (typeof obj === "string") return obj;
const fields = str.split(".");
return getObjectValByString(obj[fields[0]], fields.slice(1).join("."));}
вот фрагмент кода:
Расширение Мохамад Хамудей 'Ответ заполнит недостающие ключи
function Object_Manager(obj, Path, value, Action, strict)
{
try
{
if(Array.isArray(Path) == false)
{
Path = [Path];
}
let level = 0;
var Return_Value;
Path.reduce((a, b)=>{
console.log(level,':',a, '|||',b)
if (!strict){
if (!(b in a)) a[b] = {}
}
level++;
if (level === Path.length)
{
if(Action === 'Set')
{
a[b] = value;
return value;
}
else if(Action === 'Get')
{
Return_Value = a[b];
}
else if(Action === 'Unset')
{
delete a[b];
}
}
else
{
return a[b];
}
}, obj);
return Return_Value;
}
catch(err)
{
console.error(err);
return obj;
}
}
пример
obja = {
"a": {
"b":"nom"
}
}
// Set
path = "c.b" // Path does not exist
Object_Manager(obja,path.split('.'), 'test_new_val', 'Set', false);
// Expected Output: Object { a: Object { b: "nom" }, c: Object { b: "test_new_value" } }
Основываясь на предыдущем ответе, я создал функцию, которая также может обрабатывать скобки. Но нет точек внутри них из-за раскола.
function get(obj, str) {
return str.split(/\.|\[/g).map(function(crumb) {
return crumb.replace(/\]$/, '').trim().replace(/^(["'])((?:(?!\1)[^\\]|\\.)*?)\1$/, (match, quote, str) => str.replace(/\\(\\)?/g, "$1"));
}).reduce(function(obj, prop) {
return obj ? obj[prop] : undefined;
}, obj);
}