Given a JavaScript object,
var obj = { a: { b: '1', c: '2' } }
and a string
"a.b"
how can I convert the string to dot notation so I can go
var val = obj.a.b
If the string was just 'a', I could use obj[a]. But this is more complex. I imagine there is some straightforward method, but it escapes me at present.
recent note: While I'm flattered that this answer has gotten many upvotes, I am also somewhat horrified. If one needs to convert dot-notation strings like "x.a.b.c" into references, it could (maybe) be a sign that there is something very wrong going on (unless maybe you're performing some strange deserialization).
That is to say, novices who find their way to this answer must ask themselves the question "why am I doing this?"
It is of course generally fine to do this if your use case is small and you will not run into performance issues, AND you won't need to build upon your abstraction to make it more complicated later. In fact, if this will reduce code complexity and keep things simple, you should probably go ahead and do what OP is asking for. However, if that's not the case, consider if any of these apply:
case 1: As the primary method of working with your data (e.g. as your app's default form of passing objects around and dereferencing them). Like asking "how can I look up a function or variable name from a string".
This is bad programming practice (unnecessary metaprogramming specifically, and kind of violates function side-effect-free coding style, and will have performance hits). Novices who find themselves in this case, should instead consider working with array representations, e.g. ['x','a','b','c'], or even something more direct/simple/straightforward if possible: like not losing track of the references themselves in the first place (most ideal if it's only client-side or only server-side), etc. (A pre-existing unique id would be inelegant to add, but could be used if the spec otherwise requires its existence regardless.)
case 2: Working with serialized data, or data that will be displayed to the user. Like using a date as a string "1999-12-30" rather than a Date object (which can cause timezone bugs or added serialization complexity if not careful). Or you know what you're doing.
This is maybe fine. Be careful that there are no dot strings "." in your sanitized input fragments.
If you find yourself using this answer all the time and converting back and forth between string and array, you may be in the bad case, and should consider an alternative.
Here's an elegant one-liner that's 10x shorter than the other solutions:
function index(obj,i) {return obj[i]}
'a.b.etc'.split('.').reduce(index, obj)
[edit] Or in ECMAScript 6:
'a.b.etc'.split('.').reduce((o,i)=> o[i], obj)
(Not that I think eval always bad like others suggest it is (though it usually is), nevertheless those people will be pleased that this method doesn't use eval. The above will find obj.a.b.etc given obj and the string "a.b.etc".)
In response to those who still are afraid of using reduce despite it being in the ECMA-262 standard (5th edition), here is a two-line recursive implementation:
function multiIndex(obj,is) { // obj,['1','2','3'] -> ((obj['1'])['2'])['3']
return is.length ? multiIndex(obj[is[0]],is.slice(1)) : obj
}
function pathIndex(obj,is) { // obj,'1.2.3' -> multiIndex(obj,['1','2','3'])
return multiIndex(obj,is.split('.'))
}
pathIndex('a.b.etc')
Depending on the optimizations the JS compiler is doing, you may want to make sure any nested functions are not re-defined on every call via the usual methods (placing them in a closure, object, or global namespace).
edit:
To answer an interesting question in the comments:
how would you turn this into a setter as well? Not only returning the values by path, but also setting them if a new value is sent into the function? – Swader Jun 28 at 21:42
(sidenote: sadly can't return an object with a Setter, as that would violate the calling convention; commenter seems to instead be referring to a general setter-style function with side-effects like index(obj,"a.b.etc", value) doing obj.a.b.etc = value.)
The reduce style is not really suitable to that, but we can modify the recursive implementation:
function index(obj,is, value) {
if (typeof is == 'string')
return index(obj,is.split('.'), value);
else if (is.length==1 && value!==undefined)
return obj[is[0]] = value;
else if (is.length==0)
return obj;
else
return index(obj[is[0]],is.slice(1), value);
}
Demo:
> obj = {a:{b:{etc:5}}}
> index(obj,'a.b.etc')
5
> index(obj,['a','b','etc']) #works with both strings and lists
5
> index(obj,'a.b.etc', 123) #setter-mode - third argument (possibly poor form)
123
> index(obj,'a.b.etc')
123
...though personally I'd recommend making a separate function setIndex(...). I would like to end on a side-note that the original poser of the question could (should?) be working with arrays of indices (which they can get from .split), rather than strings; though there's usually nothing wrong with a convenience function.
A commenter asked:
what about arrays? something like "a.b[4].c.d[1][2][3]" ? –AlexS
Javascript is a very weird language; in general objects can only have strings as their property keys, so for example if x was a generic object like x={}, then x[1] would become x["1"]... you read that right... yup...
Javascript Arrays (which are themselves instances of Object) specifically encourage integer keys, even though you could do something like x=[]; x["puppy"]=5;.
But in general (and there are exceptions), x["somestring"]===x.somestring (when it's allowed; you can't do x.123).
(Keep in mind that whatever JS compiler you're using might choose, maybe, to compile these down to saner representations if it can prove it would not violate the spec.)
So the answer to your question would depend on whether you're assuming those objects only accept integers (due to a restriction in your problem domain), or not. Let's assume not. Then a valid expression is a concatenation of a base identifier plus some .identifiers plus some ["stringindex"]s.
Let us ignore for a moment that we can of course do other things legitimately in the grammar like identifier[0xFA7C25DD].asdf[f(4)?.[5]+k][false][null][undefined][NaN]; integers are not (that) 'special'.
Commenter's statement would then be equivalent to a["b"][4]["c"]["d"][1][2][3], though we should probably also support a.b["c\"validjsstringliteral"][3]. You'd have to check the ecmascript grammar section on string literals to see how to parse a valid string literal. Technically you'd also want to check (unlike in my first answer) that a is a valid javascript identifier.
A simple answer to your question though, if your strings don't contain commas or brackets, would be just be to match length 1+ sequences of characters not in the set , or [ or ]:
> "abc[4].c.def[1][2][\"gh\"]".match(/[^\]\[.]+/g)
// ^^^ ^ ^ ^^^ ^ ^ ^^^^^
["abc", "4", "c", "def", "1", "2", ""gh""]
If your strings don't contain escape characters or " characters, and because IdentifierNames are a sublanguage of StringLiterals (I think???) you could first convert your dots to []:
> var R=[], demoString="abc[4].c.def[1][2][\"gh\"]";
> for(var match,matcher=/^([^\.\[]+)|\.([^\.\[]+)|\["([^"]+)"\]|\[(\d+)\]/g;
match=matcher.exec(demoString); ) {
R.push(Array.from(match).slice(1).filter(x=> x!==undefined)[0]);
// extremely bad code because js regexes are weird, don't use this
}
> R
["abc", "4", "c", "def", "1", "2", "gh"]
Of course, always be careful and never trust your data. Some bad ways to do this that might work for some use cases also include:
// hackish/wrongish; preprocess your string into "a.b.4.c.d.1.2.3", e.g.:
> yourstring.replace(/]/g,"").replace(/\[/g,".").split(".")
"a.b.4.c.d.1.2.3" //use code from before
Special 2018 edit:
Let's go full-circle and do the most inefficient, horribly-overmetaprogrammed solution we can come up with... in the interest of syntactical purityhamfistery. With ES6 Proxy objects!... Let's also define some properties which (imho are fine and wonderful but) may break improperly-written libraries. You should perhaps be wary of using this if you care about performance, sanity (yours or others'), your job, etc.
// [1,2,3][-1]==3 (or just use .slice(-1)[0])
if (![1][-1])
Object.defineProperty(Array.prototype, -1, {get() {return this[this.length-1]}}); //credit to caub
// WARNING: THIS XTREME™ RADICAL METHOD IS VERY INEFFICIENT,
// ESPECIALLY IF INDEXING INTO MULTIPLE OBJECTS,
// because you are constantly creating wrapper objects on-the-fly and,
// even worse, going through Proxy i.e. runtime ~reflection, which prevents
// compiler optimization
// Proxy handler to override obj[*]/obj.* and obj[*]=...
var hyperIndexProxyHandler = {
get: function(obj,key, proxy) {
return key.split('.').reduce((o,i)=> o[i], obj);
},
set: function(obj,key,value, proxy) {
var keys = key.split('.');
var beforeLast = keys.slice(0,-1).reduce((o,i)=> o[i], obj);
beforeLast[keys[-1]] = value;
},
has: function(obj,key) {
//etc
}
};
function hyperIndexOf(target) {
return new Proxy(target, hyperIndexProxyHandler);
}
Demo:
var obj = {a:{b:{c:1, d:2}}};
console.log("obj is:", JSON.stringify(obj));
var objHyper = hyperIndexOf(obj);
console.log("(proxy override get) objHyper['a.b.c'] is:", objHyper['a.b.c']);
objHyper['a.b.c'] = 3;
console.log("(proxy override set) objHyper['a.b.c']=3, now obj is:", JSON.stringify(obj));
console.log("(behind the scenes) objHyper is:", objHyper);
if (!({}).H)
Object.defineProperties(Object.prototype, {
H: {
get: function() {
return hyperIndexOf(this); // TODO:cache as a non-enumerable property for efficiency?
}
}
});
console.log("(shortcut) obj.H['a.b.c']=4");
obj.H['a.b.c'] = 4;
console.log("(shortcut) obj.H['a.b.c'] is obj['a']['b']['c'] is", obj.H['a.b.c']);
Output:
obj is: {"a":{"b":{"c":1,"d":2}}}
(proxy override get) objHyper['a.b.c'] is: 1
(proxy override set) objHyper['a.b.c']=3, now obj is: {"a":{"b":{"c":3,"d":2}}}
(behind the scenes) objHyper is: Proxy {a: {…}}
(shortcut) obj.H['a.b.c']=4
(shortcut) obj.H['a.b.c'] is obj['a']['b']['c'] is: 4
inefficient idea: You can modify the above to dispatch based on the input argument; either use the .match(/[^\]\[.]+/g) method to support obj['keys'].like[3]['this'], or if instanceof Array, then just accept an Array as input like keys = ['a','b','c']; obj.H[keys].
Per suggestion that maybe you want to handle undefined indices in a 'softer' NaN-style manner (e.g. index({a:{b:{c:...}}}, 'a.x.c') return undefined rather than uncaught TypeError)...:
This makes sense from the perspective of "we should return undefined rather than throw an error" in the 1-dimensional index situation ({})['e.g.']==undefined, so "we should return undefined rather than throw an error" in the N-dimensional situation.
This does not make sense from the perspective that we are doing x['a']['x']['c'], which would fail with a TypeError in the above example.
That said, you'd make this work by replacing your reducing function with either:
(o,i)=> o===undefined?undefined:o[i], or
(o,i)=> (o||{})[i].
(You can make this more efficient by using a for loop and breaking/returning whenever the subresult you'd next index into is undefined, or using a try-catch if you expect such failures to be sufficiently rare.)
If you can use Lodash, there is a function, which does exactly that:
_.get(object, path, [defaultValue])
var val = _.get(obj, "a.b");
You could use lodash.get
After installing (npm i lodash.get), use it like this:
const get = require('lodash.get');
const myObj = {
user: {
firstName: 'Stacky',
lastName: 'Overflowy',
list: ['zero', 'one', 'two']
},
id: 123
};
console.log(get(myObj, 'user.firstName')); // outputs Stacky
console.log(get(myObj, 'id')); // outputs 123
console.log(get(myObj, 'user.list[1]')); // outputs one
// You can also update values
get(myObj, 'user').firstName = 'John';
A little more involved example with recursion.
function recompose(obj, string) {
var parts = string.split('.');
var newObj = obj[parts[0]];
if (parts[1]) {
parts.splice(0, 1);
var newString = parts.join('.');
return recompose(newObj, newString);
}
return newObj;
}
var obj = { a: { b: '1', c: '2', d:{a:{b:'blah'}}}};
console.log(recompose(obj, 'a.d.a.b')); //blah
2021
You don't need to pull in another dependency every time you wish for new capabilities in your program. Modern JS is very capable and the optional-chaining operator ?. is now widely supported and makes this kind of task easy as heck.
With a single line of code we can write get that takes an input object, t and string path. It works for object and arrays of any nesting level -
const get = (t, path) =>
path.split(".").reduce((r, k) => r?.[k], t)
const mydata =
{ a: { b: [ 0, { c: { d: [ "hello", "world" ] } } ] } }
console.log(get(mydata, "a.b.1.c.d.0"))
console.log(get(mydata, "a.b.1.c.d.1"))
console.log(get(mydata, "a.b.x.y.z"))
"hello"
"world"
undefined
I suggest to split the path and iterate it and reduce the object you have. This proposal works with a default value for missing properties.
const getValue = (object, keys) => keys.split('.').reduce((o, k) => (o || {})[k], object);
console.log(getValue({ a: { b: '1', c: '2' } }, 'a.b'));
console.log(getValue({ a: { b: '1', c: '2' } }, 'foo.bar.baz'));
Many years since the original post.
Now there is a great library called 'object-path'.
https://github.com/mariocasciaro/object-path
Available on NPM and BOWER
https://www.npmjs.com/package/object-path
It's as easy as:
objectPath.get(obj, "a.c.1"); //returns "f"
objectPath.set(obj, "a.j.0.f", "m");
And works for deeply nested properties and arrays.
If you expect to dereference the same path many times, building a function for each dot notation path actually has the best performance by far (expanding on the perf tests James Wilkins linked to in comments above).
var path = 'a.b.x';
var getter = new Function("obj", "return obj." + path + ";");
getter(obj);
Using the Function constructor has some of the same drawbacks as eval() in terms of security and worst-case performance, but IMO it's a badly underused tool for cases where you need a combination of extreme dynamism and high performance. I use this methodology to build array filter functions and call them inside an AngularJS digest loop. My profiles consistently show the array.filter() step taking less than 1ms to dereference and filter about 2000 complex objects, using dynamically-defined paths 3-4 levels deep.
A similar methodology could be used to create setter functions, of course:
var setter = new Function("obj", "newval", "obj." + path + " = newval;");
setter(obj, "some new val");
Other proposals are a little cryptic, so I thought I'd contribute:
Object.prop = function(obj, prop, val){
var props = prop.split('.')
, final = props.pop(), p
while(p = props.shift()){
if (typeof obj[p] === 'undefined')
return undefined;
obj = obj[p]
}
return val ? (obj[final] = val) : obj[final]
}
var obj = { a: { b: '1', c: '2' } }
// get
console.log(Object.prop(obj, 'a.c')) // -> 2
// set
Object.prop(obj, 'a.c', function(){})
console.log(obj) // -> { a: { b: '1', c: [Function] } }
var a = { b: { c: 9 } };
function value(layer, path, value) {
var i = 0,
path = path.split('.');
for (; i < path.length; i++)
if (value != null && i + 1 === path.length)
layer[path[i]] = value;
layer = layer[path[i]];
return layer;
};
value(a, 'b.c'); // 9
value(a, 'b.c', 4);
value(a, 'b.c'); // 4
This is a lot of code when compared to the much simpler eval way of doing it, but like Simon Willison says, you should never use eval.
Also, JSFiddle.
You can use the library available at npm, which simplifies this process. https://www.npmjs.com/package/dot-object
var dot = require('dot-object');
var obj = {
some: {
nested: {
value: 'Hi there!'
}
}
};
var val = dot.pick('some.nested.value', obj);
console.log(val);
// Result: Hi there!
Note if you're already using Lodash you can use the property or get functions:
var obj = { a: { b: '1', c: '2' } };
_.property('a.b')(obj); // => 1
_.get(obj, 'a.b'); // => 1
Underscore.js also has a property function, but it doesn't support dot notation.
I have extended the elegant answer by ninjagecko so that the function handles both dotted and/or array style references, and so that an empty string causes the parent object to be returned.
Here you go:
string_to_ref = function (object, reference) {
function arr_deref(o, ref, i) { return !ref ? o : (o[ref.slice(0, i ? -1 : ref.length)]) }
function dot_deref(o, ref) { return ref.split('[').reduce(arr_deref, o); }
return !reference ? object : reference.split('.').reduce(dot_deref, object);
};
See my working jsFiddle example here: http://jsfiddle.net/sc0ttyd/q7zyd/
You can obtain value of an object member by dot notation with a single line of code:
new Function('_', 'return _.' + path)(obj);
In you case:
var obj = { a: { b: '1', c: '2' } }
var val = new Function('_', 'return _.a.b')(obj);
To make it simple you may write a function like this:
function objGet(obj, path){
return new Function('_', 'return _.' + path)(obj);
}
Explanation:
The Function constructor creates a new Function object. In JavaScript every function is actually a Function object. Syntax to create a function explicitly with Function constructor is:
new Function ([arg1[, arg2[, ...argN]],] functionBody)
where arguments(arg1 to argN) must be a string that corresponds to a valid javaScript identifier and functionBody is a string containing the javaScript statements comprising the function definition.
In our case we take the advantage of string function body to retrieve object member with dot notation.
Hope it helps.
var find = function(root, path) {
var segments = path.split('.'),
cursor = root,
target;
for (var i = 0; i < segments.length; ++i) {
target = cursor[segments[i]];
if (typeof target == "undefined") return void 0;
cursor = target;
}
return cursor;
};
var obj = { a: { b: '1', c: '2' } }
find(obj, "a.b"); // 1
var set = function (root, path, value) {
var segments = path.split('.'),
cursor = root,
target;
for (var i = 0; i < segments.length - 1; ++i) {
cursor = cursor[segments[i]] || { };
}
cursor[segments[segments.length - 1]] = value;
};
set(obj, "a.k", function () { console.log("hello world"); });
find(obj, "a.k")(); // hello world
Use this function:
function dotToObject(data) {
function index(parent, key, value) {
const [mainKey, ...children] = key.split(".");
parent[mainKey] = parent[mainKey] || {};
if (children.length === 1) {
parent[mainKey][children[0]] = value;
} else {
index(parent[mainKey], children.join("."), value);
}
}
const result = Object.entries(data).reduce((acc, [key, value]) => {
if (key.includes(".")) {
index(acc, key, value);
} else {
acc[key] = value;
}
return acc;
}, {});
return result;
}
module.exports = { dotToObject };
Ex:
const user = {
id: 1,
name: 'My name',
'address.zipCode': '123',
'address.name': 'Some name',
'address.something.id': 1,
}
const mappedUser = dotToObject(user)
console.log(JSON.stringify(mappedUser, null, 2))
Output:
{
"id": 1,
"name": "My name",
"address": {
"zipCode": "123",
"name": "Some name",
"something": {
"id": 1
}
}
}
using Array Reduce function will get/set based on path provided.
I tested it with a.b.c and a.b.2.c {a:{b:[0,1,{c:7}]}} and its works for both getting key or mutating object to set value
function setOrGet(obj, path=[], newValue){
const l = typeof path === 'string' ? path.split('.') : path;
return l.reduce((carry,item, idx)=>{
const leaf = carry[item];
// is this last item in path ? cool lets set/get value
if( l.length-idx===1) {
// mutate object if newValue is set;
carry[item] = newValue===undefined ? leaf : newValue;
// return value if its a get/object if it was a set
return newValue===undefined ? leaf : obj ;
}
carry[item] = leaf || {}; // mutate if key not an object;
return carry[item]; // return object ref: to continue reduction;
}, obj)
}
console.log(
setOrGet({a: {b:1}},'a.b') === 1 ||
'Test Case: Direct read failed'
)
console.log(
setOrGet({a: {b:1}},'a.c',22).a.c===22 ||
'Test Case: Direct set failed'
)
console.log(
setOrGet({a: {b:[1,2]}},'a.b.1',22).a.b[1]===22 ||
'Test Case: Direct set on array failed'
)
console.log(
setOrGet({a: {b:{c: {e:1} }}},'a.b.c.e',22).a.b.c. e===22 ||
'Test Case: deep get failed'
)
// failed !. Thats your homework :)
console.log(
setOrGet({a: {b:{c: {e:[1,2,3,4,5]} }}},'a.b.c.e.3 ',22)
)
my personal recommendation.
do not use such a thing unless there is no other way!
i saw many examples people use it for translations for example from json; so you see function like locale('app.homepage.welcome') . this is just bad. if you already have data in an object/json; and you know path.. then just use it directly example locale().app.homepage.welcome by changing you function to return object you get typesafe, with autocomplete, less prone to typo's ..
I copied the following from Ricardo Tomasi's answer and modified to also create sub-objects that don't yet exist as necessary. It's a little less efficient (more ifs and creating of empty objects), but should be pretty good.
Also, it'll allow us to do Object.prop(obj, 'a.b', false) where we couldn't before. Unfortunately, it still won't let us assign undefined...Not sure how to go about that one yet.
/**
* Object.prop()
*
* Allows dot-notation access to object properties for both getting and setting.
*
* #param {Object} obj The object we're getting from or setting
* #param {string} prop The dot-notated string defining the property location
* #param {mixed} val For setting only; the value to set
*/
Object.prop = function(obj, prop, val){
var props = prop.split('.'),
final = props.pop(),
p;
for (var i = 0; i < props.length; i++) {
p = props[i];
if (typeof obj[p] === 'undefined') {
// If we're setting
if (typeof val !== 'undefined') {
// If we're not at the end of the props, keep adding new empty objects
if (i != props.length)
obj[p] = {};
}
else
return undefined;
}
obj = obj[p]
}
return typeof val !== "undefined" ? (obj[final] = val) : obj[final]
}
Few years later, I found this that handles scope and array. e.g. a['b']["c"].d.etc
function getScopedObj(scope, str) {
let obj=scope, arr;
try {
arr = str.split(/[\[\]\.]/) // split by [,],.
.filter(el => el) // filter out empty one
.map(el => el.replace(/^['"]+|['"]+$/g, '')); // remove string quotation
arr.forEach(el => obj = obj[el])
} catch(e) {
obj = undefined;
}
return obj;
}
window.a = {b: {c: {d: {etc: 'success'}}}}
getScopedObj(window, `a.b.c.d.etc`) // success
getScopedObj(window, `a['b']["c"].d.etc`) // success
getScopedObj(window, `a['INVALID']["c"].d.etc`) // undefined
If you wish to convert any object that contains dot notation keys into an arrayed version of those keys you can use this.
This will convert something like
{
name: 'Andy',
brothers.0: 'Bob'
brothers.1: 'Steve'
brothers.2: 'Jack'
sisters.0: 'Sally'
}
to
{
name: 'Andy',
brothers: ['Bob', 'Steve', 'Jack']
sisters: ['Sally']
}
convertDotNotationToArray(objectWithDotNotation) {
Object.entries(objectWithDotNotation).forEach(([key, val]) => {
// Is the key of dot notation
if (key.includes('.')) {
const [name, index] = key.split('.');
// If you have not created an array version, create one
if (!objectWithDotNotation[name]) {
objectWithDotNotation[name] = new Array();
}
// Save the value in the newly created array at the specific index
objectWithDotNotation[name][index] = val;
// Delete the current dot notation key val
delete objectWithDotNotation[key];
}
});
}
If you want to convert a string dot notation into an object, I've made a handy little helper than can turn a string like a.b.c.d with a value of e with dotPathToObject("a.b.c.d", "value") returning this:
{
"a": {
"b": {
"c": {
"d": "value"
}
}
}
}
https://gist.github.com/ahallora/9731d73efb15bd3d3db647efa3389c12
Solution:
function deepFind(key, data){
return key.split('.').reduce((ob,i)=> ob?.[i], data)
}
Usage:
const obj = {
company: "Pet Shop",
person: {
name: "John"
},
animal: {
name: "Lucky"
}
}
const company = deepFind("company", obj)
const personName = deepFind("person.name", obj)
const animalName = deepFind("animal.name", obj)
Here is my implementation
Implementation 1
Object.prototype.access = function() {
var ele = this[arguments[0]];
if(arguments.length === 1) return ele;
return ele.access.apply(ele, [].slice.call(arguments, 1));
}
Implementation 2 (using array reduce instead of slice)
Object.prototype.access = function() {
var self = this;
return [].reduce.call(arguments,function(prev,cur) {
return prev[cur];
}, self);
}
Examples:
var myobj = {'a':{'b':{'c':{'d':'abcd','e':[11,22,33]}}}};
myobj.access('a','b','c'); // returns: {'d':'abcd', e:[0,1,2,3]}
myobj.a.b.access('c','d'); // returns: 'abcd'
myobj.access('a','b','c','e',0); // returns: 11
it can also handle objects inside arrays as for
var myobj2 = {'a': {'b':[{'c':'ab0c'},{'d':'ab1d'}]}}
myobj2.access('a','b','1','d'); // returns: 'ab1d'
I used this code in my project
const getValue = (obj, arrPath) => (
arrPath.reduce((x, y) => {
if (y in x) return x[y]
return {}
}, obj)
)
Usage:
const obj = { id: { user: { local: 104 } } }
const path = [ 'id', 'user', 'local' ]
getValue(obj, path) // return 104
Using object-scan seems a bit overkill, but you can simply do
// const objectScan = require('object-scan');
const get = (obj, p) => objectScan([p], { abort: true, rtn: 'value' })(obj);
const obj = { a: { b: '1', c: '2' } };
console.log(get(obj, 'a.b'));
// => 1
console.log(get(obj, '*.c'));
// => 2
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan
There are a lot more advanced examples in the readme.
This is one of those cases, where you ask 10 developers and you get 10 answers.
Below is my [simplified] solution for OP, using dynamic programming.
The idea is that you would pass an existing DTO object that you wish to UPDATE. This makes the method most useful in the case where you have a form with several input elements having name attributes set with dot (fluent) syntax.
Example use:
<input type="text" name="person.contact.firstName" />
Code snippet:
const setFluently = (obj, path, value) => {
if (typeof path === "string") {
return setFluently(obj, path.split("."), value);
}
if (path.length <= 1) {
obj[path[0]] = value;
return obj;
}
const key = path[0];
obj[key] = setFluently(obj[key] ? obj[key] : {}, path.slice(1), value);
return obj;
};
const origObj = {
a: {
b: "1",
c: "2"
}
};
setFluently(origObj, "a.b", "3");
setFluently(origObj, "a.c", "4");
console.log(JSON.stringify(origObj, null, 3));
function at(obj, path, val = undefined) {
// If path is an Array,
if (Array.isArray(path)) {
// it returns the mapped array for each result of the path
return path.map((path) => at(obj, path, val));
}
// Uniting several RegExps into one
const rx = new RegExp(
[
/(?:^(?:\.\s*)?([_a-zA-Z][_a-zA-Z0-9]*))/,
/(?:^\[\s*(\d+)\s*\])/,
/(?:^\[\s*'([^']*(?:\\'[^']*)*)'\s*\])/,
/(?:^\[\s*"([^"]*(?:\\"[^"]*)*)"\s*\])/,
/(?:^\[\s*`([^`]*(?:\\`[^`]*)*)`\s*\])/,
]
.map((r) => r.source)
.join("|")
);
let rm;
while (rm = rx.exec(path.trim())) {
// Matched resource
let [rf, rp] = rm.filter(Boolean);
// If no one matches found,
if (!rm[1] && !rm[2]) {
// it will replace escape-chars
rp = rp.replace(/\\(.)/g, "$1");
}
// If the new value is set,
if ("undefined" != typeof val && path.length == rf.length) {
// assign a value to the object property and return it
return (obj[rp] = val);
}
// Going one step deeper
obj = obj[rp];
// Removing a step from the path
path = path.substr(rf.length).trim();
}
if (path) {
throw new SyntaxError();
}
return obj;
}
// Test object schema
let o = { a: { b: [ [ { c: { d: { '"e"': { f: { g: "xxx" } } } } } ] ] } };
// Print source object
console.log(JSON.stringify(o));
// Set value
console.log(at(o, '.a["b"][0][0].c[`d`]["\\"e\\""][\'f\']["g"]', "zzz"));
// Get value
console.log(at(o, '.a["b"][0][0].c[`d`]["\\"e\\""][\'f\']["g"]'));
// Print result object
console.log(JSON.stringify(o));
Here is my code without using eval. It’s easy to understand too.
function value(obj, props) {
if (!props)
return obj;
var propsArr = props.split('.');
var prop = propsArr.splice(0, 1);
return value(obj[prop], propsArr.join('.'));
}
var obj = { a: { b: '1', c: '2', d:{a:{b:'blah'}}}};
console.log(value(obj, 'a.d.a.b')); // Returns blah
Yes, extending base prototypes is not usually good idea but, if you keep all extensions in one place, they might be useful.
So, here is my way to do this.
Object.defineProperty(Object.prototype, "getNestedProperty", {
value : function (propertyName) {
var result = this;
var arr = propertyName.split(".");
while (arr.length && result) {
result = result[arr.shift()];
}
return result;
},
enumerable: false
});
Now you will be able to get nested property everywhere without importing module with function or copy/pasting function.
Example:
{a:{b:11}}.getNestedProperty('a.b'); // Returns 11
The Next.js extension broke Mongoose in my project. Also I've read that it might break jQuery. So, never do it in the Next.js way:
Object.prototype.getNestedProperty = function (propertyName) {
var result = this;
var arr = propertyName.split(".");
while (arr.length && result) {
result = result[arr.shift()];
}
return result;
};
This is my extended solution proposed by ninjagecko.
For me, simple string notation was not enough, so the below version supports things like:
index(obj, 'data.accounts[0].address[0].postcode');
/**
* Get object by index
* #supported
* - arrays supported
* - array indexes supported
* #not-supported
* - multiple arrays
* #issues:
* index(myAccount, 'accounts[0].address[0].id') - works fine
* index(myAccount, 'accounts[].address[0].id') - doesnt work
* #Example:
* index(obj, 'data.accounts[].id') => returns array of id's
* index(obj, 'data.accounts[0].id') => returns id of 0 element from array
* index(obj, 'data.accounts[0].addresses.list[0].id') => error
* #param obj
* #param path
* #returns {any}
*/
var index = function(obj, path, isArray?, arrIndex?){
// is an array
if(typeof isArray === 'undefined') isArray = false;
// array index,
// if null, will take all indexes
if(typeof arrIndex === 'undefined') arrIndex = null;
var _arrIndex = null;
var reduceArrayTag = function(i, subArrIndex){
return i.replace(/(\[)([\d]{0,})(\])/, (i) => {
var tmp = i.match(/(\[)([\d]{0,})(\])/);
isArray = true;
if(subArrIndex){
_arrIndex = (tmp[2] !== '') ? tmp[2] : null;
}else{
arrIndex = (tmp[2] !== '') ? tmp[2] : null;
}
return '';
});
}
function byIndex(obj, i) {
// if is an array
if(isArray){
isArray = false;
i = reduceArrayTag(i, true);
// if array index is null,
// return an array of with values from every index
if(!arrIndex){
var arrValues = [];
_.forEach(obj, (el) => {
arrValues.push(index(el, i, isArray, arrIndex));
})
return arrValues;
}
// if array index is specified
var value = obj[arrIndex][i];
if(isArray){
arrIndex = _arrIndex;
}else{
arrIndex = null;
}
return value;
}else{
// remove [] from notation,
// if [] has been removed, check the index of array
i = reduceArrayTag(i, false);
return obj[i]
}
}
// reduce with the byIndex method
return path.split('.').reduce(byIndex, obj)
}
In other languages like Python 2 and Python 3, you can define and assign values to a tuple variable, and retrieve their values like this:
tuple = ("Bob", 24)
name, age = tuple
print(name) #name evaluates to Bob
print(age) #age evaluates to 24
Is there anything similar in JavaScript? Or do I just have to do it the ugly way with an array:
tuple = ["Bob", 24]
name = tuple[0] //name Evaluates to Bob
age = tuple[1] //age Evaluates to 24
Is there a better way to simulate Python tuples in JavaScript 5?
Update: See the answer regarding ES6, which should be favored over CoffeeScript for new projects.
Javascript 1.7 added destructuring assignment which allows you to do essentially what you are after.
function getTuple(){
return ["Bob", 24];
}
var [a, b] = getTuple();
// a === "bob" , b === 24 are both true
You have to do it the ugly way. If you really want something like this, you can check out CoffeeScript, which has that and a whole lot of other features that make it look more like python (sorry for making it sound like an advertisement, but I really like it.)
You can do something similar:
var tuple = Object.freeze({ name:'Bob', age:14 })
and then refer to name and age as attributes
tuple.name
tuple.age
This "tuple" feature it is called destructuring in EcmaScript2015 and is soon to be supported by up to date browsers. For the time being, only Firefox and Chrome support it.
But hey, you can use a transpiler.
The code would look as nice as python:
let tuple = ["Bob", 24]
let [name, age] = tuple
console.log(name)
console.log(age)
A frozen array behaves identically to a python tuple:
const tuple = Object.freeze(["Bob", 24]);
let [name, age]; = tuple
console.debug(name); // "Bob"
console.debug(age); // 24
Be fancy and define a class
class Tuple extends Array {
constructor(...items) {
super(...items);
Object.freeze(this);
}
}
let tuple = new Tuple("Jim", 35);
let [name, age] = tuple;
console.debug(name); // Jim
console.debug(age); // 35
tuple = ["Bob", 24]; // no effect
console.debug(name); // Jim
console.debug(age); // 25
Works today in all the latest browsers.
Tuples aren't supported in JavaScript
If you're looking for an immutable list, Object.freeze() can be used to make an array immutable.
The Object.freeze() method freezes an object: that is, prevents new properties from being added to it; prevents existing properties from being removed; and prevents existing properties, or their enumerability, configurability, or writability, from being changed. In essence the object is made effectively immutable. The method returns the object being frozen.
Source: Mozilla Developer Network - Object.freeze()
Assign an array as usual but lock it using 'Object.freeze()
> tuple = Object.freeze(['Bob', 24]);
[ 'Bob', 24 ]
Use the values as you would a regular array (python multi-assignment is not supported)
> name = tuple[0]
'Bob'
> age = tuple[1]
24
Attempt to assign a new value
> tuple[0] = 'Steve'
'Steve'
But the value is not changed
> console.log(tuple)
[ 'Bob', 24 ]
Unfortunately you can't use that tuple assignment syntax in (ECMA|Java)Script.
EDIT: Someone linked to Mozilla/JS 1.7 - this wouldn't work cross-browser but if that is not required then there's your answer.
This is not intended to be actually used in real life, just an interesting exercise. See Why is using the JavaScript eval function a bad idea? for details.
This is the closest you can get without resorting to vendor-specific extensions:
myArray = [1,2,3];
eval(set('a,b,c = myArray'));
Helper function:
function set(code) {
var vars=code.split('=')[0].trim().split(',');
var array=code.split('=')[1].trim();
return 'var '+vars.map(function(x,i){return x+'='+array+'['+i+']'}).join(',');
}
Proof that it works in arbitrary scope:
(function(){
myArray = [4,5,6];
eval(set('x,y,z = myArray'));
console.log(y); // prints 5
})()
eval is not supported in Safari.
As an update to The Minister's answer, you can now do this with es2015:
function Tuple(...args) {
args.forEach((val, idx) =>
Object.defineProperty(this, "item"+idx, { get: () => val })
)
}
var t = new Tuple("a", 123)
console.log(t.item0) // "a"
t.item0 = "b"
console.log(t.item0) // "a"
https://jsbin.com/fubaluwimo/edit?js,console
You can have a tuple type in Javascript as well. Just define it with higher order functions (the academic term is Church encoding):
const Tuple = (...args) => {
const Tuple = f => f(...args);
return Object.freeze(Object.assign(Tuple, args));
};
const get1 = tx => tx((x, y) => x);
const get2 = tx => tx((x, y) => y);
const bimap = f => g => tx => tx((x, y) => Tuple(f(x), g(y)));
const toArray = tx => tx((...args) => args);
// aux functions
const inc = x => x + 1;
const toUpperCase = x => x.toUpperCase();
// mock data
const pair = Tuple(1, "a");
// application
console.assert(get1(pair) === 1);
console.assert(get2(pair) === "a");
const {0:x, 1:y} = pair;
console.log(x, y); // 1 a
console.log(toArray(bimap(inc) (toUpperCase) (pair))); // [2, "A"]
const map = new Map([Tuple(1, "a"), Tuple(2, "b")]);
console.log(map.get(1), map.get(2)); // a b
Please note that Tuple isn't used as a normal constructor. The solution doesn't rely on the prototype system at all, but solely on higher order functions.
What are the advantages of tuples over Arrays used like tuples? Church encoded tuples are immutable by design and thus prevent side effects caused by mutations. This helps to build more robust applications. Additionally, it is easier to reason about code that distinguishes between Arrays as a collection type (e.g. [a]) and tuples as related data of various types (e.g. (a, b)).
Here is a simple Javascript Tuple implementation:
var Tuple = (function () {
function Tuple(Item1, Item2) {
var item1 = Item1;
var item2 = Item2;
Object.defineProperty(this, "Item1", {
get: function() { return item1 }
});
Object.defineProperty(this, "Item2", {
get: function() { return item2 }
});
}
return Tuple;
})();
var tuple = new Tuple("Bob", 25); // Instantiation of a new Tuple
var name = tuple.Item1; // Assignment. name will be "Bob"
tuple.Item1 = "Kirk"; // Will not set it. It's immutable.
This is a 2-tuple, however, you could modify my example to support 3,4,5,6 etc. tuples.
I made a tuple implementation that works quite well. This solution allows for array destructuring, as well as basic type-cheking.
const Tuple = (function() {
function Tuple() {
// Tuple needs at least one element
if (arguments.length < 1) {
throw new Error('Tuple needs at least one element');
}
const args = { ...arguments };
// Define a length property (equal to the number of arguments provided)
Object.defineProperty(this, 'length', {
value: arguments.length,
writable: false
});
// Assign values to enumerable properties
for (let i in args) {
Object.defineProperty(this, i, {
enumerable: true,
get() {
return args[+i];
},
// Checking if the type of the provided value matches that of the existing value
set(value) {
if (typeof value !== typeof args[+i]) {
throw new Error('Cannot assign ' + typeof value + ' on ' + typeof args[+i]);
}
args[+i] = value;
}
});
}
// Implementing iteration with Symbol.iterator (allows for array destructuring as well for...of loops)
this[Symbol.iterator] = function() {
const tuple = this;
return {
current: 0,
last: tuple.length - 1,
next() {
if (this.current <= this.last) {
let val = { done: false, value: tuple[this.current] };
this.current++;
return val;
} else {
return { done: true };
}
}
};
};
// Sealing the object to make sure no more values can be added to tuple
Object.seal(this);
}
// check if provided object is a tuple
Tuple.isTuple = function(obj) {
return obj instanceof Tuple;
};
// Misc. for making the tuple more readable when printing to the console
Tuple.prototype.toString = function() {
const copyThis = { ...this };
const values = Object.values(copyThis);
return `(${values.join(', ')})`;
};
// conctat two instances of Tuple
Tuple.concat = function(obj1, obj2) {
if (!Tuple.isTuple(obj1) || !Tuple.isTuple(obj2)) {
throw new Error('Cannot concat Tuple with ' + typeof (obj1 || obj2));
}
const obj1Copy = { ...obj1 };
const obj2Copy = { ...obj2 };
const obj1Items = Object.values(obj1Copy);
const obj2Items = Object.values(obj2Copy);
return new Tuple(...obj1Items, ...obj2Items);
};
return Tuple;
})();
const SNAKE_COLOR = new Tuple(0, 220, 10);
const [red, green, blue] = SNAKE_COLOR;
console.log(green); // => 220