简体   繁体   中英

Access Javascript nested objects safely

I have json based data structure with objects containing nested objects. In order to access a particular data element I have been chaining references to object properties together. For example:

var a = b.c.d;

If b or bc is undefined, this will fail with an error. However, I want to get a value if it exists otherwise just undefined. What is the best way to do this without having to check that every value in the chain exists?

I would like to keep this method as general as possible so I don't have to add huge numbers of helper methods like:

var a = b.getD();

or

var a = helpers.getDFromB(b);

I also want to try to avoid try/catch constructs as this isn't an error so using try/catch seems misplaced. Is that reasonable?

Any ideas?

Standard approach:

var a = b && b.c && b.c.d && b.c.d.e;

is quite fast but not too elegant (especially with longer property names).

Using functions to traverse JavaScipt object properties is neither efficient nor elegant.

Try this instead:

try { var a = b.c.d.e; } catch(e){}

in case you are certain that a was not previously used or

try { var a = b.c.d.e; } catch(e){ a = undefined; }

in case you may have assigned it before.

This is probably even faster that the first option.

ECMAScript2020 , and in Node v14, has the optional chaining operator (I've seen it also called safe navigation operator), which would allow your example to be written as:

var a = b?.c?.d;

From the MDN docs :

The optional chaining operator (?.) permits reading the value of a property located deep within a chain of connected objects without having to expressly validate that each reference in the chain is valid. The ?. operator functions similarly to the . chaining operator, except that instead of causing an error if a reference is nullish (null or undefined), the expression short-circuits with a return value of undefined. When used with function calls, it returns undefined if the given function does not exist.

You can create a general method that access an element based on an array of property names that is interpreted as a path through the properties:

function getValue(data, path) {
    var i, len = path.length;
    for (i = 0; typeof data === 'object' && i < len; ++i) {
        data = data[path[i]];
    }
    return data;
}

Then you could call it with:

var a = getValue(b, ["c", "d"]);

This is an old question and now with es6 features, this problem can be solved more easily.

const idx = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);

Thanks to @sharifsbeat for this solution .

ES6 has optional chaining which can be used as follows:

 const object = { foo: {bar: 'baz'} }; // not found, undefined console.log(object?.foo?.['nested']?.missing?.prop) // not found, object as default value console.log(object?.foo?.['nested']?.missing?.prop || {}) // found, "baz" console.log(object?.foo?.bar)

This approach requires the variable "object" to be defined and to be an object.

Alternatively, you could define your own utility, here's an example which implements recursion:

 const traverseObject = (object, propertyName, defaultValue) => { if (Array.isArray(propertyName)) { return propertyName.reduce((o, p) => traverseObject(o, p, defaultValue), object); } const objectSafe = object || {}; return objectSafe[propertyName] || defaultValue; }; // not found, undefined console.log(traverseObject({}, 'foo')); // not found, object as default value console.log(traverseObject(null, ['foo', 'bar'], {})); // found "baz" console.log(traverseObject({foo: {bar:'baz'}}, ['foo','bar']));

probably it's may be simple:

let a = { a1: 11, b1: 12, c1: { d1: 13, e1: { g1: 14 }}}
console.log((a || {}).a2); => undefined
console.log(((a || {}).c1 || {}).d1) => 13

and so on.

The answers here are good bare-metal solutions. However, if you just want to use a package that is tried and true, I recommend using lodash.

With ES6 you can run the following

import _ from 'lodash'

var myDeepObject = {...}

value = _.get(myDeepObject, 'maybe.these.path.exist', 'Default value if not exists')

const getValue = (obj, property, defaultValue) => (
  property.split('.').reduce((item, key) => {
    if (item && typeof item === 'object' && key in item) {
      return item[key];
    }
    return defaultValue;
  }, obj)
)

const object = { 'a': { 'b': { 'c': 3 } } };

getValue(object, 'a.b.c'); // 3
getValue(object, 'a.b.x'); // undefined
getValue(object, 'a.b.x', 'default'); // 'default'
getValue(object, 'a.x.c'); // undefined

I will just paste the function that I use in almost all project as utility for this type of situation.

public static is(fn: Function, dv: any) {
    try {
        if (fn()) {
                return fn()
            } else {
                return dv
            }
        } catch (e) {
            return dv
        }
    }

So first argument is callback and second is the default value if it fails to extract the data due to some error.

I call it at all places as follows:

var a = is(()=> a.b.c, null);

// The code for the regex isn't great, 
// but it suffices for most use cases.

/**
 * Gets the value at `path` of `object`.
 * If the resolved value is `undefined`,
 * or the property does not exist (set param has: true),
 * the `defaultValue` is returned in its place.
 *
 * @param {Object} object The object to query.
 * @param {Array|string} path The path of the property to get.
 * @param {*} [def] The value returned for `undefined` resolved values.
 * @param {boolean} [has] Return property instead of default value if key exists.
 * @returns {*} Returns the resolved value.
 * @example
 *
 * var object = { 'a': [{ 'b': { 'c': 3 } }], b: {'c-[d.e]': 1}, c: { d: undefined, e: 0 } };
 *
 * dotGet(object, 'a[0].b.c');
 * // => 3
 * 
 * dotGet(object, ['a', '0', 'b', 'c']);
 * // => 3
 *
 * dotGet(object, ['b', 'c-[d.e]']);
 * // => 1
 *
 * dotGet(object, 'c.d', 'default value');
 * // => 'default value'
 *
 * dotGet(object, 'c.d', 'default value', true);
 * // => undefined
 *
 * dotGet(object, 'c.d.e', 'default value');
 * // => 'default value'
 *
 * dotGet(object, 'c.d.e', 'default value', true);
 * // => 'default value'
 *
 * dotGet(object, 'c.e') || 5; // non-true default value
 * // => 5 
 * 
 */
var dotGet = function (obj, path, def, has) {
    return (typeof path === 'string' ? path.split(/[\.\[\]\'\"]/) : path)
    .filter(function (p) { return 0 === p ? true : p; })
    .reduce(function (o, p) {
        return typeof o === 'object' ? ((
            has ? o.hasOwnProperty(p) : o[p] !== undefined
        ) ? o[p] : def) : def;
    }, obj);
}

If you would like to have a dynamic access with irregular number of properties at hand, in ES6 you might easily do as follows;

 function getNestedValue(o,...a){ var val = o; for (var prop of a) val = typeof val === "object" && val !== null && val[prop] !== void 0 ? val[prop] : undefined; return val; } var obj = {a:{foo:{bar:null}}}; console.log(getNestedValue(obj,"a","foo","bar")); console.log(getNestedValue(obj,"a","hop","baz"));

Gets the value at path of object . If the resolved value is undefined , the defaultValue is returned in its place.

In ES6 we can get nested property from an Object like below code snippet .

 const myObject = { a: { b: { c: { d: 'test' } } }, c: { d: 'Test 2' } }, isObject = obj => obj && typeof obj === 'object', hasKey = (obj, key) => key in obj; function nestedObj(obj, property, callback) { return property.split('.').reduce((item, key) => { if (isObject(item) && hasKey(item, key)) { return item[key]; } return typeof callback != undefined ? callback : undefined; }, obj); } console.log(nestedObj(myObject, 'abcd')); //return test console.log(nestedObj(myObject, 'abcde')); //return undefined console.log(nestedObj(myObject, 'c.d')); //return Test 2 console.log(nestedObj(myObject, 'd.d', false)); //return false console.log(nestedObj(myObject, 'a.b')); //return {"c": {"d": "test"}}

An old question, and now days we have Typescript projects so often that this question seems irrelevant, but I got here searching for the same thing, so I made a simple function to do it. Your thoughts about not using try/catch is too strict for my taste, after all the seek for undefined.x will cause an error anyway. So with all that, this is my method.

function getSafe (obj, valuePath) {
    try { return eval("obj." + valuePath); } 
    catch (err) { return null; }
}

To use this we have to pass the object. I tried to avoid that, but there was not other way to get scope into it from another function (there is a whole bunch of questions about this in here). And a small test set to see what we get:

let outsideObject = {
    html: {
        pageOne: {
            pageTitle: 'Lorem Ipsum!'
        }
    }
};
function testme() {  
    let insideObject = { a: { b: 22 } };
    return {
        b: getSafe(insideObject, "a.b"),       // gives: 22
        e: getSafe(insideObject, "a.b.c.d.e"), // gives: null
        pageTitle: getSafe(outsideObject, "html.pageOne.pageTitle"),     // gives: Lorem Ipsum!
        notThere: getSafe(outsideObject, "html.pageOne.pageTitle.style") // gives: undefined
    }
}
testme(); 

UPDATE: Regarding the use of eval I think that eval is a tool to use carefully and not the devil itself. In this method, the user does not interfere with eval since it is the developer that is looking for a property by its name.

If you care about syntax, here's a cleaner version of Hosar's answer:

function safeAccess(path, object) {
  if (object) {
    return path.reduce(
      (accumulator, currentValue) => (accumulator && accumulator[currentValue] ? accumulator[currentValue] : null),
      object,
    );
  } else {
    return null;
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM