What my function does:
Safely sets object properties with dot notation strings in JavaScript.
That is to say, it allows setting a nested property on an object using a string such as "a.b.c".
For example: set(obj, "a.b.c", value)
would be equivalent to a.b.c = value
Some notes on intended behavior
- When setting a property at a given path, if any portion of that path doesn't exist, it should be created
- When creating a path portion, if the next key in the path is NOT an integer, the current portion should be created as an object
- When creating a path portion, if the next key in the path is an integer, the current portion should be created as an array
Why?
I'd like to be able to set deeply nested properties on an object in an environment where I can't be sure that all the properties in the path will always exist and I don't want to deal with the logic of checking for and setting each key manually in more than one place. This allows me to quickly set the needed value without caring if the keys were present previously or not.
/**
* Set the value for the given object for the given path
* where the path can be a nested key represented with dot notation
*
* @param {object} obj The object on which to set the given value
* @param {string} path The dot notation path to the nested property where the value should be set
* @param {mixed} value The value that should be set
* @return {mixed}
*
*/
function set(obj, path, value) {
// protect against being something unexpected
obj = typeof obj === 'object' ? obj : {};
// split the path into and array if its not one already
var keys = Array.isArray(path) ? path : path.split('.');
// keep up with our current place in the object
// starting at the root object and drilling down
var curStep = obj;
// loop over the path parts one at a time
// but, dont iterate the last part,
for (var i = 0; i < keys.length - 1; i++) {
// get the current path part
var key = keys[i];
// if nothing exists for this key, make it an empty object or array
if (!curStep[key] && !Object.prototype.hasOwnProperty.call(curStep, key)){
// get the next key in the path, if its numeric, make this property an empty array
// otherwise, make it an empty object
var nextKey = keys[i+1];
var useArray = /^\+?(0|[1-9]\d*)$/.test(nextKey);
curStep[key] = useArray ? [] : {};
}
// update curStep to point to the new level
curStep = curStep[key];
}
// set the final key to our value
var finalStep = keys[keys.length - 1];
curStep[finalStep] = value;
};
/** Usage **/
console.log('setting non existant a.b.c to "some value":');
var test = {};
set(test, 'a.b.c', 'some value');
console.log(test);
console.log('updating a.b.c to "some new value":');
set(test, 'a.b.c', 'some new value');
console.log(test);
console.log('setting non existant a.b.0 to "some value":');
var test = {};
set(test, 'a.b.0', 'some value');
console.log(test);
console.log('updating a.b.0 to "some new value":');
set(test, 'a.b.0', 'some new value');
console.log(test);
-
4\$\begingroup\$ Just in case you're not aware of it, Dottie may have considered some of the issues raised below - github.com/mickhansen/dottie.js \$\endgroup\$iabw– iabw2018年05月14日 15:03:53 +00:00Commented May 14, 2018 at 15:03
2 Answers 2
Dangerous
There are so many caveats in this function I would really not allow it in any code base because of potencial unexpected , or misunderstood behaviour. Its just waiting to cause problems in completely unrelated code.
Some points.
Some variables should be
const
they arekeys
,key
,useArray
,nextKey
,finalStep
Don't see why you create a new variable
curStep
(with very odd name) to hold the current object. Just reuseobj
Avoid the name
set
as it is used as a JavaScript token eg create a setter{ set val(blah) {...} }
Maybe the name could beassignToPath
null
is also of type"object"
so it will pay to extend the test on the first line to include thenull
Would be best to throwYou currently return
undefined
. Maybe it would be more helpful to return the object you assigned the new property to.Why use the long version
!Object.prototype.hasOwnProperty.call(curStep, key)
?? when!curStep.hasOwnProperty(key)
will do the same.If a property is expressly set to
undefined
your function fails.
const test = { a: undefined }
assignToPath(test, "a.b", "foo" ); //throws but would expect test.a.b = "foo"
Objects can be locked via various
Object
functionsObject.freeze
,Object.seal
,Object.preventExtensions
,Object.defineProperties
andObject.defineProperty
Your function ignores these settings.If a property exists but is not an
Object
orArray
you still attempt to add a property to it.
const test = { a: "blah" }
assignToPath(test, "a.b", "foo" ); // fails
- This is what I would consider a low level function and as such would be one of the few that need to throw errors rather than fail silently. It should throw at anything that is not as expected. For example if the object to assign a property to is an array, or an object
const test = { a: [] }
assignToPath(test, "a.b", "foo" ); // Maybe this should throw
const test = { a: {} }
assignToPath(test, "a.1", "foo" ); // This should throw as it is unclear what the result should be.
// the case against the above
const test = {}
assignToPath(test, "a.1", "foo" ); // creates an array
Suggestions only.
Adding a settings argument would allow for better control of the behaviour when things get a little ambiguous.
assignToPath({a:[]}, "a.b", "bar", {arrayHaveProps : true});
assignToPath({a:{}}, "a.1", "bar", {allowObjIndex : true});
You can shorten the code if you use a while loop and shift the path as you go.
// simple example of using `while` and `shift` rather than a `for` loop
var obj = {a:{b:{c:"a"}}};
const path = "a.b.c".split(".");
while(path.length > 1){
obj = obj[path.shift()];
}
obj[path.shift()] = "b";
-
1\$\begingroup\$ I guess
set({}, "hasOwnProperty", null)
is a reason to useObject.prototype.hasOwnProperty
\$\endgroup\$sineemore– sineemore2018年05月14日 08:00:31 +00:00Commented May 14, 2018 at 8:00 -
1\$\begingroup\$ or even
Object.create(null).hasOwnProperty()
\$\endgroup\$sineemore– sineemore2018年05月14日 08:02:50 +00:00Commented May 14, 2018 at 8:02 -
\$\begingroup\$ @sineemore Your examples are not reason to use the indirect call. How about
Object.prototype.hasOwnProperty =()=> {}
Now what??? \$\endgroup\$Blindman67– Blindman672018年05月14日 09:13:47 +00:00Commented May 14, 2018 at 9:13 -
\$\begingroup\$ Those examples are reason to use the indirect call, because they come up all the time in the real world. Nobody overwrites
Object.prototype.hasOwnProperty
on purpose while expecting things to work. \$\endgroup\$Ry-– Ry-2020年04月20日 06:58:41 +00:00Commented Apr 20, 2020 at 6:58
obj = typeof obj === 'object' ? obj : {};
When obj
argument points to something other than object
you will create one. Like in set("foo", "a.b.c", "Yay!")
. I guess you want to return it at function end or it will be lost.
Other notes:
/^\+?(0|[1-9]\d*)$/
allows+
in integer key inputs- you may use
isNaN(+nextKey)
instead of regular expression, but beware float or signed numbers finalStep
is a bit misleading name. You may usekey = keys[keys.length - 1]
instead.