Komaruloh
Komaruloh

Reputation: 7041

Accessing nested JavaScript objects and arrays by string path

I have a data structure like this :

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'
        }
    ]
};

And I would like to access the data using these variable :

var part1name = "part1.name";
var part2quantity = "part2.qty";
var part3name1 = "part3[0].name";

part1name should be filled with someObject.part1.name 's value, which is "Part 1". Same thing with part2quantity which filled with 60.

Is there anyway to achieve this with either pure javascript or JQuery?

Upvotes: 692

Views: 385019

Answers (30)

Adriano Spadoni
Adriano Spadoni

Reputation: 4800

ES6: Only one line in Vanila JS (it return null if don't find instead of giving error):

'path.string'.split('.').reduce((p,c)=>p&&p[c]||null, MyOBJ)

Or example:

'a.b.c'.split('.').reduce((p,c)=>p&&p[c]||null, {a:{b:{c:1}}})

With Optional chaining operator:

'a.b.c'.split('.').reduce((p,c)=>p?.[c], {a:{b:{c:1}}})

For a ready to use function that also recognises falsy values and accept default values as parameter:

const resolvePath = (object, path, defaultValue) => path
   .split('.')
   .reduce((o, p) => o ? o[p] : defaultValue, object)

Example to use:

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

Bonus:

To set a path (Requested by @rob-gordon) you can use:

const setPath = (object, path, value) => path
   .split('.')
   .reduce((o,p,i) => o[p] = path.split('.').length === ++i ? value : o[p] || {}, object)

Example:

let myVar = {}
setPath(myVar, 'a.b.c', 42) => 42
console.log(myVar) => {a: {b: {c: 42}}}

Access array with []:

const resolvePath = (object, path, defaultValue) => path
   .split(/[\.\[\]\'\"]/)
   .filter(p => p)
   .reduce((o, p) => o ? o[p] : defaultValue, object)

Example:

const myVar = {a:{b:[{c:1}]}}
resolvePath(myVar,'a.b[0].c') => 1
resolvePath(myVar,'a["b"][\'0\'].c') => 1

Upvotes: 258

Dinesh Pandiyan
Dinesh Pandiyan

Reputation: 6299

Just in case, anyone's visiting this question in 2017 or later and looking for an easy-to-remember way, here's an elaborate blog post on Accessing Nested Objects in JavaScript without being bamboozled by

Cannot read property 'foo' of undefined error

Access Nested Objects Using Array Reduce

Let's take this example structure

const user = {
    id: 101,
    email: '[email protected]',
    personalInfo: {
        name: 'Jack',
        address: [{
            line1: 'westwish st',
            line2: 'washmasher',
            city: 'wallas',
            state: 'WX'
        }]
    }
}

To be able to access nested arrays, you can write your own array reduce util.

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.

There is also an excellent type handling minimal library typy that does all this for you.

With typy, your code will look like this

const city = t(user, 'personalInfo.address[0].city').safeObject;

Disclaimer: I am the author of this package.

Upvotes: 10

Aral Roca
Aral Roca

Reputation: 5929

Another solution:

export function getNestedFieldByStringKey(obj, path) {
  const squareBracketsRgx = /\[(\w|'|")*\]/g
  const squareBracketsToDot = (sb: string) => `.${sb.replace(/\[|\]|'|"/g, '')}`
  const parts = path
    .replace(squareBracketsRgx, squareBracketsToDot)
    .split('.')

  return parts.reduce((o, part) => o?.[part], obj)
}

And these are some tests:

describe('getNestedFieldByStringKey', () => {
    it('should return the nested field using "." as separator', () => {
      const input = {
        some: {
          example: {
            nested: true
          }
        }
      }

      expect(getNestedFieldByStringKey(input, 'some.example.nested')).toBe(true)
    })

    it('should return the nested field using "." and "[]" as separator', () => {
      const input = {
        some: {
          example: {
            nested: [{ test: true }]
          }
        }
      }

      expect(getNestedFieldByStringKey(input, 'some["example"].nested[0].test')).toBe(true)
    })

    it('should return undefined if does not exist', () => {
      const input = {}

      expect(getNestedFieldByStringKey(input, 'some["example"].nested[0].test')).toBe(undefined)
    })
})

Upvotes: 1

adjwilli
adjwilli

Reputation: 9688

After reading through the other answers, I believe the most performant and concise way of replacing _.get() and _.set() is by making a module with the following:

let rgxBracketToDot;

export function sanitizePath (path) {
    path = path || [];
    return Array.isArray(path) ? path : path.replace(rgxBracketToDot || (rgxBracketToDot = /\[(\w+)\]/g), '.$1').split('.');
}

export function get (obj, path) {
    if (!obj || typeof obj !== 'object') {
        return;
    }
    return sanitizePath(path).reduce((acc, val) => acc && acc[val], obj);
}

export function set (obj, path, value) {
    const [current,...rest] = sanitizePath(path);
    rest.length >= 1 ? set(obj[current] = obj[current] || {}, rest, value) : obj[current]= value;
    return obj;
}

To maintain full compatibility with lodash, two additional .replace() calls could be included in sanitizePath() to remove leading and trailing dots (.):

path = path.replace(/^\./, '');
path = path.replace(/\.$/, '');

This should be done in a similar way to rgxBracketToDot so that the regex is only set once.

If you have full control over the path arguments, you could also make the code more performant by only using array paths and removing sanitizeString() altogether.

Upvotes: 0

speigg
speigg

Reputation: 2898

This is the solution I use:

function resolve(path, obj=self, separator='.') {
    var properties = Array.isArray(path) ? path : path.split(separator)
    return properties.reduce((prev, curr) => prev?.[curr], obj)
}

Example usage:

// 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

Limitations:

  • Can't use brackets ([]) for array indices—though specifying array indices between the separator token (e.g., .) works fine as shown above.

Upvotes: 274

Giulio
Giulio

Reputation: 524

DotObject = obj => new Proxy(obj, {
  get: function(o,k) {
    const m = k.match(/(.+?)\.(.+)/)
    return m ? this.get(o[m[1]], m[2]) : o[k]
  }
})

const test = DotObject({a: {b: {c: 'wow'}}})
console.log(test['a.b.c'])

Upvotes: 2

Reza Attar
Reza Attar

Reputation: 618

I've looked on all the other answers, decided to add improvements into more readable code:

function getObjectValByString(obj, str) {
if (typeof obj === "string") return obj;

const fields = str.split(".");

return getObjectValByString(obj[fields[0]], fields.slice(1).join("."));}

heres a code snippet:

let someObject = {
    partner: {
        id: "AIM",
        person: {
            name: "ANT",
            an: { name: "ESM" },
        },
    },
};

function getObjectValByString(obj, str) {
    if (typeof obj === "string") return obj;

    const fields = str.split(".");

    return getObjectValByString(obj[fields[0]], fields.slice(1).join("."));
}

const result = getObjectValByString(someObject, "partner.person.an.name");
console.log({
    result,
});

Upvotes: 1

Harish Ambady
Harish Ambady

Reputation: 13151

You can manage to obtain the value of a deep object member with dot notation without any external JavaScript library with the following simple trick:

function objectGet(obj, path) { return new Function('_', 'return _.' + path)(obj); };

In your case to obtain value of part1.name from someObject just do:

objectGet(someObject, 'part1.name');

Here is a simple fiddle demo: https://jsfiddle.net/harishanchu/oq5esowf/

Upvotes: 27

Alex Mckay
Alex Mckay

Reputation: 3706

This can be simplified by splitting the logic into three separate functions:

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);
};

//usage:
const myObject = { foo: { bar: { baz: 'taco' } } };
const result = path('foo.bar')(myObject);
//results => { baz: 'taco' }

This variation supports:

  • passing an array or string argument
  • dealing with undefined values during invocation and execution
  • testing each function independently
  • using each function independently

Upvotes: 2

timebandit
timebandit

Reputation: 838

My solution is based on that given by @AdrianoSpadoni and addresses a need to clone the object

function generateData(object: any, path: string, value: any): object {
  const clone = JSON.parse(JSON.stringify(object));
  path
    .split(".")
    .reduce(
    (o, p, i) => (o[p] = path.split(".").length === ++i ? value : o[p] || {}),
  clone
);
  return clone;
}

Upvotes: 1

Hashbrown
Hashbrown

Reputation: 13023

Instead of trying to emulate JS syntax which you will have to spend a bunch of compute parsing, or just get wrong/forget things like a bunch of these answers (keys with .s in, anyone?), just use an array of keys.

var part1name     = Object.get(someObject, ['part1', 'name']);
var part2quantity = Object.get(someObject, ['part2', 'qty']);
var part3name1    = Object.get(someObject, ['part3', 0, 'name']);

answer

If you need to use a single string instead, simply JSONify it.
Another improvement in this method is that you can delete/set the root level object.

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];
};

Demo of other features:
demonstration

The bob = for .set(/.del( isn't necessary unless your path might be empty (manipulating the root object).
I prove that I don't clone the object by using steve to keep a reference to the original and checking bob == steve //true after that first .set(

Upvotes: 10

vitaly-t
vitaly-t

Reputation: 25930

If you want a solution that can properly detect and report details of any issue with the path parsing, I wrote my own solution to this - library path-value.

const {resolveValue} = require('path-value');

resolveValue(someObject, 'part1.name'); //=> Part 1
resolveValue(someObject, 'part2.qty'); //=> 50
resolveValue(someObject, 'part3.0.name'); //=> Part 3A

Note that for indexes we use .0, and not [0], because parsing the latter adds a performance penalty, while .0 works directly in JavaScript, and is thus very fast.

However, full ES5 JavaScript syntax is also supported, it just needs to be tokenized first:

const {resolveValue, tokenizePath} = require('path-value');

const path = tokenizePath('part3[0].name'); //=> ['part3', '0', 'name']

resolveValue(someObject, path); //=> Part 3A

Upvotes: 5

vincent
vincent

Reputation: 2181

Using object-scan this becomes a one liner. However more importantly this solution considers performance:

  • input traversed once during search (even if multiple keys are queried)
  • parsing only happens once on init (in case multiple objects are queried)
  • allow for extended syntax using *

// const objectScan = require('object-scan');

const 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' }] };

const get = (haystack, needle) => objectScan([needle], { rtn: 'value', abort: true })(haystack);

console.log(get(someObject, 'part1.name'));
// => Part 1
console.log(get(someObject, 'part2.qty'));
// => 60
console.log(get(someObject, 'part3[0].name'));
// => Part 3A

const getAll = (haystack, ...needles) => objectScan(needles, { reverse: false, rtn: 'entry', joined: true })(haystack);

console.log(getAll(someObject, 'part1.name', 'part2.qty', 'part3[0].name'));
/* =>
[ [ 'part1.name', 'Part 1' ],
  [ 'part2.qty', '60' ],
  [ 'part3[0].name', 'Part 3A' ] ]
 */

console.log(getAll(someObject, 'part1.*'));
/* =>
[ [ 'part1.name', 'Part 1' ],
  [ 'part1.size', '20' ],
  [ 'part1.qty', '50' ] ]
 */
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>

Disclaimer: I'm the author of object-scan

Upvotes: 1

Nick Grealy
Nick Grealy

Reputation: 25942

This will probably never see the light of day... but here it is anyway.

  1. Replace [] bracket syntax with .
  2. Split on . character
  3. Remove blank strings
  4. Find the path (otherwise undefined)

(For finding a path to an object, use this pathTo solution.)

// "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'
        }
        // ...
    ],
    'pa[rt3' : [
        {
            '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
console.log(deep_value(someObject, "part3[0].....name"));        // Part 3A - invalid blank paths removed
console.log(deep_value(someObject, "pa[rt3[0].name"));           // undefined - name does not support square brackets

Upvotes: 33

Ben Aston
Ben Aston

Reputation: 55769

Note that the following will not work for all valid unicode property names (but neither will any of the other answers as far as I can tell).

const PATTERN = /[\^|\[|\.]([$|\w]+)/gu

function propValue(o, s) {
    const names = []
    for(let [, name] of [...s.matchAll(PATTERN)]) 
        names.push(name)
    return names.reduce((p, propName) => {
        if(!p.hasOwnProperty(propName)) 
            throw 'invalid property name'
        return p[propName]
    }, o)
}

let path = 'myObject.1._property2[0][0].$property3'
let o = {
    1: {
        _property2: [
            [{
                $property3: 'Hello World'
            }]
        ]
    }
}
console.log(propValue(o, path)) // 'Hello World'

Upvotes: 1

Mr. Polywhirl
Mr. Polywhirl

Reputation: 48713

Based on Alnitak's answer.

I wrapped the polyfill in a check, and reduced the function to a single chained reduction.

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'))

Upvotes: 2

nesinervink
nesinervink

Reputation: 420

AngularJS

Speigg's approach is very neat and clean, though I found this reply while searching for the solution of accessing AngularJS $scope properties by string path and with a little modification it does the job:

$scope.resolve = function( path, obj ) {
    return path.split('.').reduce( function( prev, curr ) {
        return prev[curr];
    }, obj || this );
}

Just place this function in your root controller and use it any child scope like this:

$scope.resolve( 'path.to.any.object.in.scope')

Upvotes: 7

georgeawg
georgeawg

Reputation: 48968

AngularJS has $scope.$eval

With AngularJS, one can use the $scope.$eval method to access nested objects:

$scope.someObject = someObject;
console.log( $scope.$eval("someObject.part3[0].name") ); //Part 3A

For more information, see

The DEMO

angular.module("app",[])
.run(function($rootScope) {
     $rootScope.someObject = {
         'part2' : {
              'name': 'Part 2',
              'size': '15',
              'qty' : '60'
         },
         'part3' : [{
              'name': 'Part 3A',
              'size': '10',
              'qty' : '20'
         },{
              name: 'Part 3B'           
         }]
     };
     console.log(
         "part3[0].name =",
         $rootScope.$eval("someObject.part3[0].name")
    );
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app"
</body>

Upvotes: 1

Ghominejad
Ghominejad

Reputation: 1798

You can use ramda library.

Learning ramda also helps you to work with immutable objects easily.


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);


https://codepen.io/ghominejad/pen/BayJZOQ

Upvotes: 2

jumpjack
jumpjack

Reputation: 990

Starting from @Alnitak answer I built this source, which downloads an actual .JSON file and processes it, printing to console explanatory strings for each step, and more details in case of wrong key passed:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
  <script>
function retrieveURL(url) {
        var client = new XMLHttpRequest();
        prefix = "https://cors-anywhere.herokuapp.com/"
        client.open('GET', prefix + url);
        client.responseType = 'text';
        client.onload = function() {
            response = client.response; // Load remote response.
            console.log("Response received.");
            parsedJSON  = JSON.parse(response);
            console.log(parsedJSON);
            console.log(JSONitemByPath(parsedJSON,"geometry[6].obs[3].latituade"));
            return response;
        };
        try {
            client.send();
        } catch(e) {
            console.log("NETWORK ERROR!");
            console.log(e);
        }
}



function JSONitemByPath(o, s) {
    structure = "";
    originalString = s;
    console.log("Received string: ", s);
    s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
    console.log("Converted to   : ", s);
    s = s.replace(/^\./, '');           // strip a leading dot
    var a = s.split('.');

    console.log("Single keys to parse: ",a);

    for (var i = 0, n = a.length; i < n; ++i) {
        var k = a[i];
        if (k in o) {
            o = o[k];
            console.log("object." + structure +  a[i], o);
            structure +=  a[i] + ".";
        } else {
            console.log("ERROR: wrong path passed: ", originalString);
            console.log("       Last working level: ", structure.substr(0,structure.length-1));
            console.log("       Contents: ", o);
            console.log("       Available/passed key: ");
            Object.keys(o).forEach((prop)=> console.log("       "+prop +"/" + k));
            return;
        }
    }
    return o;
}


function main() {
    rawJSON = retrieveURL("http://haya2now.jp/data/data.json");
}

</script>
  </head>
  <body onload="main()">
  </body>
</html>

Output example:

Response received.
json-querier.html:17 {geometry: Array(7), error: Array(0), status: {…}}
json-querier.html:34 Received string:  geometry[6].obs[3].latituade
json-querier.html:36 Converted to   :  geometry.6.obs.3.latituade
json-querier.html:40 Single keys to parse:  (5) ["geometry", "6", "obs", "3", "latituade"]
json-querier.html:46 object.geometry (7) [{…}, {…}, {…}, {…}, {…}, {…}, {…}]
json-querier.html:46 object.geometry.6 {hayabusa2: {…}, earth: {…}, obs: Array(6), TT: 2458816.04973593, ryugu: {…}, …}
json-querier.html:46 object.geometry.6.obs (6) [{…}, {…}, {…}, {…}, {…}, {…}]
json-querier.html:46 object.geometry.6.obs.3 {longitude: 148.98, hayabusa2: {…}, sun: {…}, name: "DSS-43", latitude: -35.4, …}
json-querier.html:49 ERROR: wrong path passed:  geometry[6].obs[3].latituade
json-querier.html:50        Last working level:  geometry.6.obs.3
json-querier.html:51        Contents:  {longitude: 148.98, hayabusa2: {…}, sun: {…}, name: "DSS-43", latitude: -35.4, …}
json-querier.html:52        Available/passed key: 
json-querier.html:53        longitude/latituade
json-querier.html:53        hayabusa2/latituade
json-querier.html:53        sun/latituade
json-querier.html:53        name/latituade
json-querier.html:53        latitude/latituade
json-querier.html:53        altitude/latituade
json-querier.html:18 undefined

Upvotes: 0

ThatGuyRob
ThatGuyRob

Reputation: 126

React example - Using lodash

This may not be the most efficient way, from a performance perspective, but if your app is some monolith it sure as heck will save you some time. Especially, when you are tightly coupling your state data format to an API back-end.

   import set from "lodash/set";  // More efficient import

    class UserProfile extends Component {

      constructor(props){
        super(props);

        this.state = {
          user: {
            account: {
              id: "",
              email: "",
              first_name: ""
            }
          }
        }
      }

       /**
       * Updates the state based on the form input
       * 
       * @param {FormUpdate} event 
       */
      userAccountFormHook(event) {
        // https://lodash.com/docs#get
        // https://lodash.com/docs#set
        const { name, value } = event.target;
        let current_state = this.state
        set(current_state, name, value)  // Magic happens here
        this.setState(current_state);
      }

    render() {
        return (
          <CustomFormInput
            label: "First Name"
            type: "text"
            placeholder: "First Name"
            name: "user.account.first_name"
            onChange: {this.userAccountFormHook}
            value: {this.state.user.account.first_name}

          />
      )
  }
}

Upvotes: 0

SBUK-Tech
SBUK-Tech

Reputation: 1337

Extension of Mohamad Hamouday' Answer will fill in missing keys

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;
    }
}

Example


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" } }

Upvotes: 1

Dm Mh
Dm Mh

Reputation: 840

I'm developing online-shop with React. I tried to change values in copied state object to update original state with it on submit. Examples above haven't worked for me, because most of them mutate structure of copied object. I found working example of the function for accessing and changing values of the deep nested object properties: https://lowrey.me/create-an-object-by-path-in-javascript-2/ Here it is:

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;
};

Upvotes: 2

Mohamad Hamouday
Mohamad Hamouday

Reputation: 2783

Inspired by @webjay's answer: https://stackoverflow.com/a/46008856/4110122

I made this function which can you use it to Get/ Set/ Unset any value in object

function Object_Manager(obj, Path, value, Action) 
{
    try
    {
        if(Array.isArray(Path) == false)
        {
            Path = [Path];
        }

        let level = 0;
        var Return_Value;
        Path.reduce((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;
    }
}

To use it:

 // Set
 Object_Manager(Obj,[Level1,Level2,Level3],New_Value, 'Set');

 // Get
 Object_Manager(Obj,[Level1,Level2,Level3],'', 'Get');

 // Unset
 Object_Manager(Obj,[Level1,Level2,Level3],'', 'Unset');

Upvotes: 1

Aakash
Aakash

Reputation: 23825

Working with Underscore's property or propertyOf:

var test = {
  foo: {
    bar: {
      baz: 'hello'
    }
  }
}
var string = 'foo.bar.baz';


// document.write(_.propertyOf(test)(string.split('.')))

document.write(_.property(string.split('.'))(test));
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>

Good Luck...

Upvotes: 0

Aobo Cheng
Aobo Cheng

Reputation: 4528

// (IE9+) Two steps

var pathString = "[0]['property'].others[3].next['final']";
var obj = [{
  property: {
    others: [1, 2, 3, {
      next: {
        final: "SUCCESS"
      }
    }]
  }
}];

// Turn string to path array
var pathArray = pathString
    .replace(/\[["']?([\w]+)["']?\]/g,".$1")
    .split(".")
    .splice(1);

// Add object prototype method
Object.prototype.path = function (path) {
  try {
    return [this].concat(path).reduce(function (f, l) {
      return f[l];
    });
  } catch (e) {
    console.error(e);
  }
};

// usage
console.log(obj.path(pathArray));
console.log(obj.path([0,"doesNotExist"]));

Upvotes: 1

Flavien Volken
Flavien Volken

Reputation: 21349

While reduce is good, I am surprised no one used forEach:

function valueForKeyPath(obj, path){
        const keys = path.split('.');
        keys.forEach((key)=> obj = obj[key]);
        return obj;
    };

Test

Upvotes: 2

James
James

Reputation: 2841

It's a one liner with 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"

Or even better...

const val = _.get(deep, prop);

Or ES6 version w/ reduce...

const val = prop.split('.').reduce((r, val) => { return r ? r[val] : undefined; }, deep);

Plunkr

Upvotes: 15

Vincent
Vincent

Reputation: 2404

Based on a previous answer, I have created a function that can also handle brackets. But no dots inside them due to the split.

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);
}

Upvotes: 1

Jodo
Jodo

Reputation: 4773

Instead of a string an array can be used adressing nested objects and arrays e.g.: ["my_field", "another_field", 0, "last_field", 10]

Here is an example that would change a field based on this array representation. I am using something like that in react.js for controlled input fields that change the state of nested structures.

let state = {
        test: "test_value",
        nested: {
            level1: "level1 value"
        },
        arr: [1, 2, 3],
        nested_arr: {
            arr: ["buh", "bah", "foo"]
        }
    }

function handleChange(value, fields) {
    let update_field = state;
    for(var i = 0; i < fields.length - 1; i++){
        update_field = update_field[fields[i]];
    }
    update_field[fields[fields.length-1]] = value;
}

handleChange("update", ["test"]);
handleChange("update_nested", ["nested","level1"]);
handleChange(100, ["arr",0]);
handleChange('changed_foo', ["nested_arr", "arr", 3]);
console.log(state);

Upvotes: 0

Related Questions