Тест на наличие вложенного ключа объекта JavaScript
если у меня есть ссылка на объект:
var test = {};
что потенциально (но не сразу) будет иметь вложенные объекты, что-то вроде:
{level1: {level2: {level3: "level3"}}};
каков наилучший способ проверить наличие ключей в наиболее глубоко вложенных объектах?
alert(test.level1);
доходность undefined
, а alert(test.level1.level2.level3);
не удается.
в настоящее время я делаю что-то вроде этого:
if(test.level1 && test.level1.level2 && test.level1.level2.level3) {
alert(test.level1.level2.level3);
}
но мне было интересно, если есть лучший способ.
30 ответов:
вы должны сделать это шаг за шагом, если вы не хотите, чтобы
TypeError
, потому что если один из членовnull
илиundefined
, и вы пытаетесь получить доступ к члену исключение будет выдано.можно просто
catch
исключение, или сделать функцию для проверки существования нескольких уровней, что-то вроде этого:function checkNested(obj /*, level1, level2, ... levelN*/) { var args = Array.prototype.slice.call(arguments, 1); for (var i = 0; i < args.length; i++) { if (!obj || !obj.hasOwnProperty(args[i])) { return false; } obj = obj[args[i]]; } return true; } var test = {level1:{level2:{level3:'level3'}} }; checkNested(test, 'level1', 'level2', 'level3'); // true checkNested(test, 'level1', 'level2', 'foo'); // false
вот шаблон I снято с Оливера Стила:
var level3 = (((test || {}).level1 || {}).level2 || {}).level3; alert( level3 );
на самом деле вся эта статья-это обсуждение того, как вы можете сделать это в javascript. Он решает использовать приведенный выше синтаксис (который не так уж трудно прочитать, как только вы привыкнете к нему) в качестве идиомы.
обновление
похоже на лодаш добавил
_.get
для всех ваших вложенных потребностей в получении свойств._.get(countries, 'greece.sparta.playwright')
предыдущий ответ
лодашь пользователи могут пользоваться лодашь.контриб С пару методов, которые смягчают эту проблему.
getPath
подпись:
_.getPath(obj:Object, ks:String|Array)
возвращает значение на любой глубине во вложенном объекте на основе пути, описанного ключи даны. Ключи могут быть даны в виде массива или в виде разделенных точкой строки. Возвращает
undefined
если путь не может быть достигнут.var countries = { greece: { athens: { playwright: "Sophocles" } } } }; _.getPath(countries, "greece.athens.playwright"); // => "Sophocles" _.getPath(countries, "greece.sparta.playwright"); // => undefined _.getPath(countries, ["greece", "athens", "playwright"]); // => "Sophocles" _.getPath(countries, ["greece", "sparta", "playwright"]); // => undefined
я сделал тесты производительности (спасибо cdMinix для добавления lodash) по некоторым предложениям, предложенным к этому вопросу с результатами, перечисленными ниже.
отказ от ответственности #1 превращение строк в ссылки является ненужным метапрограммированием и, вероятно, лучше избегать. Не теряйте из виду ваши ссылки для начала. Прочитайте больше из этого ответа на аналогичный вопрос.
отказ от ответственности #2 здесь речь идет о миллионах операций в миллисекунду. Очень маловероятно, что любой из них будет делать большой разницы в большинстве случаев. Выберите то, что имеет наибольший смысл, зная ограничения каждого из них. Для меня я бы пошел с чем-то вроде
reduce
из удобства.объект Wrap (Оливер Стил) – 34 % – быстрый
var r1 = (((test || {}).level1 || {}).level2 || {}).level3; var r2 = (((test || {}).level1 || {}).level2 || {}).foo;
Оригинал решение (предложенное в вопросе) – 45%
var r1 = test.level1 && test.level1.level2 && test.level1.level2.level3; var r2 = test.level1 && test.level1.level2 && test.level1.level2.foo;
checkNested – 50%
function checkNested(obj) { for (var i = 1; i < arguments.length; i++) { if (!obj.hasOwnProperty(arguments[i])) { return false; } obj = obj[arguments[i]]; } return true; }
get_if_exist – 52%
function get_if_exist(str) { try { return eval(str) } catch(e) { return undefined } }
validChain – 54%
function validChain( object, ...keys ) { return keys.reduce( ( a, b ) => ( a || { } )[ b ], object ) !== undefined; }
objHasKeys – 63%
function objHasKeys(obj, keys) { var next = keys.shift(); return obj[next] && (! keys.length || objHasKeys(obj[next], keys)); }
nestedPropertyExists – 69%
function nestedPropertyExists(obj, props) { var prop = props.shift(); return prop === undefined ? true : obj.hasOwnProperty(prop) ? nestedPropertyExists(obj[prop], props) : false; }
_.получить – 72%
deeptest – 86%
function deeptest(target, s){ s= s.split('.') var obj= target[s.shift()]; while(obj && s.length) obj= obj[s.shift()]; return obj; }
грустные клоуны – 100% – медленный
var o = function(obj) { return obj || {} }; var r1 = o(o(o(o(test).level1).level2).level3); var r2 = o(o(o(o(test).level1).level2).foo);
вы можете прочитать свойство объекта на любой глубине, если вы обрабатываете имя как строку:
't.level1.level2.level3'
.window.t={level1:{level2:{level3: 'level3'}}}; function deeptest(s){ s= s.split('.') var obj= window[s.shift()]; while(obj && s.length) obj= obj[s.shift()]; return obj; } alert(deeptest('t.level1.level2.level3') || 'Undefined');
возвращает
undefined
если любой из сегментовundefined
.
var a; a = { b: { c: 'd' } }; function isset (fn) { var value; try { value = fn(); } catch (e) { value = undefined; } finally { return value !== undefined; } }; // ES5 console.log( isset(function () { return a.b.c; }), isset(function () { return a.b.c.d.e.f; }) );
если вы кодируете в среде ES6 (или используете 6to5), то вы можете воспользоваться функции стрелочку синтаксис:
// ES6 using the arrow function console.log( isset(() => a.b.c), isset(() => a.b.c.d.e.f) );
Что касается производительности, нет никакого штрафа за использование
try..catch
блок, если свойство имеет значение. Если свойство не задано, это влияет на производительность.рассмотрим просто с помощью
_.has
:var object = { 'a': { 'b': { 'c': 3 } } }; _.has(object, 'a'); // → true _.has(object, 'a.b.c'); // → true _.has(object, ['a', 'b', 'c']); // → true
я попробовал рекурсивный подход:
function objHasKeys(obj, keys) { var next = keys.shift(); return obj[next] && (! keys.length || objHasKeys(obj[next], keys)); }
The
! keys.length ||
выбивает рекурсию, чтобы она не запускала функцию без ключей, оставшихся для тестирования. Тесты:obj = { path: { to: { the: { goodKey: "hello" } } } } console.log(objHasKeys(obj, ['path', 'to', 'the', 'goodKey'])); // true console.log(objHasKeys(obj, ['path', 'to', 'the', 'badKey'])); // undefined
Я использую его для печати дружественного html-представления группы объектов с неизвестными ключами / значениями, например:
var biosName = objHasKeys(myObj, 'MachineInfo:BiosInfo:Name'.split(':')) ? myObj.MachineInfo.BiosInfo.Name : 'unknown';
ES6 ответ, тщательно протестирован :)
const propExists = (obj, path) => { return !!path.split('.').reduce((obj, prop) => { return obj && obj[prop] ? obj[prop] : undefined; }, obj) }
читайте:Codepen с полным тестовым покрытием
один простой способ заключается в следующем:
try { alert(test.level1.level2.level3); } catch(e) { alert("undefined"); // this is optional to put any output here }
The
try/catch
ловит случаи, когда любой из объектов более высокого уровня, таких как test, test.уровень 1, Тест.уровень 1.Уровень 2 не определен.
Я думаю, что следующий скрипт дает более читаемое представление.
объявить функцию:
var o = function(obj) { return obj || {};};
тогда используйте его так:
if (o(o(o(o(test).level1).level2).level3) { }
я называю это "техника грустного клоуна", потому что он использует знак o(
EDIT:
вот версия TypeScript
Он дает проверки типа во время компиляции (а также intellisense, если вы используете такой инструмент, как Visual Studio)
export function o<T>(someObject: T, defaultValue: T = {} as T) : T { if (typeof someObject === 'undefined' || someObject === null) return defaultValue; else return someObject; }
использование:
o(o(o(o(test).level1).level2).level3
но на этот раз IntelliSense работает!
плюс, вы можете установить значение по умолчанию:
o(o(o(o(o(test).level1).level2).level3, "none")
Я не видел ни одного примера кто-то использует Прокси
так что я придумал свой собственный. Самое замечательное в этом то, что вам не нужно интерполировать строки. Вы можете на самом деле вернуть цепочку-в состоянии
объектфункция и сделать некоторые магические вещи с ним. Вы даже можете вызывать функции и получать индексы массива для проверки глубоких объектовfunction resolve(target) { var noop = () => {} // We us a noop function so we can call methods also return new Proxy(noop, { get(noop, key) { // return end result if key is _result return key === '_result' ? target : resolve( // resolve with target value or undefined target === undefined ? undefined : target[key] ) }, // if we want to test a function then we can do so alos thanks to using noop // instead of using target in our proxy apply(noop, that, args) { return resolve(typeof target === 'function' ? target.apply(that, args) : undefined) }, }) } // some modified examples from the accepted answer var test = {level1: {level2:() => ({level3:'level3'})}} var test1 = {key1: {key2: ['item0']}} // You need to get _result in the end to get the final result console.log(resolve(test).level1.level2().level3._result) console.log(resolve(test).level1.level2().level3.level4.level5._result) console.log(resolve(test1).key1.key2[0]._result) console.log(resolve(test1)[0].key._result) // don't exist
приведенный выше код отлично работает для синхронных вещей. Но как бы вы проверьте что-то асинхронное, как этот вызов ajax? Как вы это проверяете? что делать, если ответ не json, когда он возвращает ошибку http 500?
window.fetch('https://httpbin.org/get') .then(function(response) { return response.json() }) .then(function(json) { console.log(json.headers['User-Agent']) })
конечно, вы можете использовать async/await, чтобы избавиться от некоторых обратных вызовов. Но что, если вы могли бы сделать это еще более волшебно? что-то, что выглядит так:
fetch('https://httpbin.org/get').json().headers['User-Agent']
вы, наверное, задаетесь вопросом, где все обещание &
.then
цепи... это может быть блокировкой для всего, что вы знаете... но, используя тот же метод прокси-с обещайте, что вы действительно можете проверить глубоко вложенный сложный путь для его существования, никогда не написав ни одной функцииfunction resolve(target) { return new Proxy(() => {}, { get(noop, key) { return key === 'then' ? target.then.bind(target) : resolve( Promise.resolve(target).then(target => { if (typeof target[key] === 'function') return target[key].bind(target) return target[key] }) ) }, apply(noop, that, args) { return resolve(target.then(result => { return result.apply(that, args) })) }, }) } // this feels very much synchronous but are still non blocking :) resolve(window) // this will chain a noop function until you call then() .fetch('https://httpbin.org/get') .json() .headers['User-Agent'] .then(console.log, console.warn) // you get a warning if it doesn't exist // You could use this method also for the first test object // also, but it would have to call .then() in the end // Another example resolve(window) .fetch('https://httpbin.org/get?items=4&items=2') .json() .args .items // nice that you can map an array item without even having it ready .map(n => ~~n * 4) .then(console.log, console.warn) // you get a warning if it doesn't exist
более короткая, ES5 версия отличного ответа @CMS:
// Check the obj has the keys in the order mentioned. Used for checking JSON results. var checkObjHasKeys = function(obj, keys) { var success = true; keys.forEach( function(key) { if ( ! obj.hasOwnProperty(key)) { success = false; } obj = obj[key]; }) return success; }
с аналогичным тестом:
var test = { level1:{level2:{level3:'result'}}}; utils.checkObjHasKeys(test, ['level1', 'level2', 'level3']); // true utils.checkObjHasKeys(test, ['level1', 'level2', 'foo']); // false
на основе ответ, Я придумал эту общую функцию с помощью
ES2015
что бы решить проблемуfunction validChain( object, ...keys ) { return keys.reduce( ( a, b ) => ( a || { } )[ b ], object ) !== undefined; } var test = { first: { second: { third: "This is not the key your are looking for" } } } if ( validChain( test, "first", "second", "third" ) ) { console.log( test.first.second.third ); }
вы также можете использовать tc39 дополнительное предложение цепочки вместе с babel 7 - tc39-proposal-optional-chaining
код будет выглядеть так:
const test = test?.level1?.level2?.level3; if (test) alert(test);
ответ, данный CMS, отлично работает со следующей модификацией для нулевых проверок, а также
function checkNested(obj /*, level1, level2, ... levelN*/) { var args = Array.prototype.slice.call(arguments), obj = args.shift(); for (var i = 0; i < args.length; i++) { if (obj == null || !obj.hasOwnProperty(args[i]) ) { return false; } obj = obj[args[i]]; } return true; }
были разработаны следующие варианты, начиная с ответ. Одно и то же дерево для обоих :
var o = { a: { b: { c: 1 } } };
остановить поиск, когда значение undefined
var u = undefined; o.a ? o.a.b ? o.a.b.c : u : u // 1 o.x ? o.x.y ? o.x.y.z : u : u // undefined (o = o.a) ? (o = o.b) ? o.c : u : u // 1
обеспечить каждый уровень по одному
var $ = function (empty) { return function (node) { return node || empty; }; }({}); $($(o.a).b).c // 1 $($(o.x).y).z // undefined
Я знаю, этот вопрос старый, но я хотел бы предложить расширение, добавив ко всем объектам. Я знаю, что люди склонны хмуриться при использовании прототипа объекта для расширенной функциональности объекта, но я не нахожу ничего проще, чем делать это. Кроме того, теперь это разрешено с "объект".defineProperty метод.
Object.defineProperty( Object.prototype, "has", { value: function( needle ) { var obj = this; var needles = needle.split( "." ); for( var i = 0; i<needles.length; i++ ) { if( !obj.hasOwnProperty(needles[i])) { return false; } obj = obj[needles[i]]; } return true; }});
теперь, чтобы проверить любое свойство в любом объекте, вы можете просто сделать:
if( obj.has("some.deep.nested.object.somewhere") )
вот jsfiddle чтобы проверить он выходит, и в частности он включает в себя некоторый jQuery, который ломается, если вы изменяете объект.прототип непосредственно из-за свойства становится перечислимым. Это должно хорошо работать с библиотеками сторонних производителей.
Я думаю, что это небольшое улучшение (становится 1-liner):
alert( test.level1 && test.level1.level2 && test.level1.level2.level3 )
это работает, потому что оператор && возвращает конечный операнд, который он оценил (и это короткое замыкание).
это работает со всеми объектами и массивами :)
ex:
if( obj._has( "something.['deep']['under'][1][0].item" ) ) { //do something }
Это моя улучшенная версия ответа
Я однако других как имя свойства, потому что оно может конфликтовать с существующим имеет свойство (например: карты)
Object.defineProperty( Object.prototype, "_has", { value: function( needle ) { var obj = this; var needles = needle.split( "." ); var needles_full=[]; var needles_square; for( var i = 0; i<needles.length; i++ ) { needles_square = needles[i].split( "[" ); if(needles_square.length>1){ for( var j = 0; j<needles_square.length; j++ ) { if(needles_square[j].length){ needles_full.push(needles_square[j]); } } }else{ needles_full.push(needles[i]); } } for( var i = 0; i<needles_full.length; i++ ) { var res = needles_full[i].match(/^((\d+)|"(.+)"|'(.+)')\]$/); if (res != null) { for (var j = 0; j < res.length; j++) { if (res[j] != undefined) { needles_full[i] = res[j]; } } } if( typeof obj[needles_full[i]]=='undefined') { return false; } obj = obj[needles_full[i]]; } return true; }});
здесь скрипка
вот мой взгляд на это - большинство из этих решений учитывать регистр вложенного массива, как в:
obj = { "l1":"something", "l2":[{k:0},{k:1}], "l3":{ "subL":"hello" } }
возможно, я захочу проверить
obj.l2[0].k
С помощью функции ниже, вы можете сделать
deeptest('l2[0].k',obj)
функция вернет true, если объект существует, false в противном случае
function deeptest(keyPath, testObj) { var obj; keyPath = keyPath.split('.') var cKey = keyPath.shift(); function get(pObj, pKey) { var bracketStart, bracketEnd, o; bracketStart = pKey.indexOf("["); if (bracketStart > -1) { //check for nested arrays bracketEnd = pKey.indexOf("]"); var arrIndex = pKey.substr(bracketStart + 1, bracketEnd - bracketStart - 1); pKey = pKey.substr(0, bracketStart); var n = pObj[pKey]; o = n? n[arrIndex] : undefined; } else { o = pObj[pKey]; } return o; } obj = get(testObj, cKey); while (obj && keyPath.length) { obj = get(obj, keyPath.shift()); } return typeof(obj) !== 'undefined'; } var obj = { "l1":"level1", "arr1":[ {"k":0}, {"k":1}, {"k":2} ], "sub": { "a":"letter A", "b":"letter B" } }; console.log("l1: " + deeptest("l1",obj)); console.log("arr1[0]: " + deeptest("arr1[0]",obj)); console.log("arr1[1].k: " + deeptest("arr1[1].k",obj)); console.log("arr1[1].j: " + deeptest("arr1[1].j",obj)); console.log("arr1[3]: " + deeptest("arr1[3]",obj)); console.log("arr2: " + deeptest("arr2",obj));
теперь мы можем использовать
reduce
для перебора вложенных ключей:// @params o<object> // @params path<string> expects 'obj.prop1.prop2.prop3' // returns: obj[path] value or 'false' if prop doesn't exist const objPropIfExists = o => path => { const levels = path.split('.'); const res = (levels.length > 0) ? levels.reduce((a, c) => a[c] || 0, o) : o[path]; return (!!res) ? res : false } const obj = { name: 'Name', sys: { country: 'AU' }, main: { temp: '34', temp_min: '13' }, visibility: '35%' } const exists = objPropIfExists(obj)('main.temp') const doesntExist = objPropIfExists(obj)('main.temp.foo.bar.baz') console.log(exists, doesntExist)
Я подумал, что добавлю еще один, который я придумал сегодня. Причина, по которой я горжусь этим решением, заключается в том, что он избегает вложенных скобок, которые используются во многих решениях, таких как объект Wrap (Оливер Стил):
(в этом примере я использую символ подчеркивания в качестве переменной переменную, но любое имя переменной будет работать)
//the 'test' object var test = {level1: {level2: {level3: 'level3'}}}; let _ = test; if ((_=_.level1) && (_=_.level2) && (_=_.level3)) { let level3 = _; //do stuff with level3 }
//you could also use 'stacked' if statements. This helps if your object goes very deep. //(formatted without nesting or curly braces except the last one) let _ = test; if (_=_.level1) if (_=_.level2) if (_=_.level3) { let level3 = _; //do stuff with level3 } //or you can indent: if (_=_.level1) if (_=_.level2) if (_=_.level3) { let level3 = _; //do stuff with level3 }
есть функция здесь на thecodeabode (safeRead) который будет делать это безопасным способом... то есть
safeRead(test, 'level1', 'level2', 'level3');
если какое-либо свойство имеет значение null или не определено, возвращается пустая строка
на основе предыдущий комментарий, вот еще одна версия, где главный объект не может быть определен как:
// Supposing that our property is at first.second.third.property: var property = (((typeof first !== 'undefined' ? first : {}).second || {}).third || {}).property;
Я написал свою собственную функцию, которая принимает нужный путь, и имеет хорошую и плохую функцию обратного вызова.
function checkForPathInObject(object, path, callbackGood, callbackBad){ var pathParts = path.split("."); var currentObjectPath = object; // Test every step to see if it exists in object for(var i=0; i<(pathParts.length); i++){ var currentPathPart = pathParts[i]; if(!currentObjectPath.hasOwnProperty(pathParts[i])){ if(callbackBad){ callbackBad(); } return false; } else { currentObjectPath = currentObjectPath[pathParts[i]]; } } // call full path in callback callbackGood(); }
использование:
var testObject = { level1:{ level2:{ level3:{ } } } }; checkForPathInObject(testObject, "level1.level2.level3", function(){alert("good!")}, function(){alert("bad!")}); // good checkForPathInObject(testObject, "level1.level2.level3.levelNotThere", function(){alert("good!")}, function(){alert("bad!")}); //bad
//Just in case is not supported or not included by your framework //*************************************************** Array.prototype.some = function(fn, thisObj) { var scope = thisObj || window; for ( var i=0, j=this.length; i < j; ++i ) { if ( fn.call(scope, this[i], i, this) ) { return true; } } return false; }; //**************************************************** function isSet (object, string) { if (!object) return false; var childs = string.split('.'); if (childs.length > 0 ) { return !childs.some(function (item) { if (item in object) { object = object[item]; return false; } else return true; }); } else if (string in object) { return true; } else return false; } var object = { data: { item: { sub_item: { bla: { here : { iam: true } } } } } }; console.log(isSet(object,'data.item')); // true console.log(isSet(object,'x')); // false console.log(isSet(object,'data.sub_item')); // false console.log(isSet(object,'data.item')); // true console.log(isSet(object,'data.item.sub_item.bla.here.iam')); // true
Я искал значение, которое будет возвращено, если свойство существует, поэтому я изменил ответ CMS выше. Вот что я придумал:
function getNestedProperty(obj, key) { // Get property array from key string var properties = key.split("."); // Iterate through properties, returning undefined if object is null or property doesn't exist for (var i = 0; i < properties.length; i++) { if (!obj || !obj.hasOwnProperty(properties[i])) { return; } obj = obj[properties[i]]; } // Nested property found, so return the value return obj; } Usage: getNestedProperty(test, "level1.level2.level3") // "level3" getNestedProperty(test, "level1.level2.foo") // undefined
у меня была та же проблема, и я хотел посмотреть, смогу ли я придумать свое собственное решение. Это принимает путь, который вы хотите проверить в виде строки.
function checkPathForTruthy(obj, path) { if (/\[[a-zA-Z_]/.test(path)) { console.log("Cannot resolve variables in property accessors"); return false; } path = path.replace(/\[/g, "."); path = path.replace(/]|'|"/g, ""); path = path.split("."); var steps = 0; var lastRef = obj; var exists = path.every(key => { var currentItem = lastRef[path[steps]]; if (currentItem) { lastRef = currentItem; steps++; return true; } else { return false; } }); return exists; }
вот фрагмент с некоторыми журналирования и тестовых случаев:
console.clear(); var testCases = [ ["data.Messages[0].Code", true], ["data.Messages[1].Code", true], ["data.Messages[0]['Code']", true], ['data.Messages[0]["Code"]', true], ["data[Messages][0]['Code']", false], ["data['Messages'][0]['Code']", true] ]; var path = "data.Messages[0].Code"; var obj = { data: { Messages: [{ Code: "0" }, { Code: "1" }] } } function checkPathForTruthy(obj, path) { if (/\[[a-zA-Z_]/.test(path)) { console.log("Cannot resolve variables in property accessors"); return false; } path = path.replace(/\[/g, "."); path = path.replace(/]|'|"/g, ""); path = path.split("."); var steps = 0; var lastRef = obj; var logOutput = []; var exists = path.every(key => { var currentItem = lastRef[path[steps]]; if (currentItem) { logOutput.push(currentItem); lastRef = currentItem; steps++; return true; } else { return false; } }); console.log(exists, logOutput); return exists; } testCases.forEach(testCase => { if (checkPathForTruthy(obj, testCase[0]) === testCase[1]) { console.log("Passed: " + testCase[0]); } else { console.log("Failed: " + testCase[0] + " expected " + testCase[1]); } });
небольшое изменение в ответ разрешить вложенные массивы в пути
var has = function (obj, key) { return key.split(".").every(function (x) { if (typeof obj != "object" || obj === null || !x in obj) return false; if (obj.constructor === Array) obj = obj[0]; obj = obj[x]; return true; }); }
Проверьте связанный ответ для использования :)