1

Is there a more functional way to create an object in JavaScript programatically without assigning each key individually?

For example, given this array (imagine it comes from an outside data source):

let arr = ['a=1', 'b=2', 'c=3'];

What is an easy way to convert this to an object like so?

let expectedResult = { a: '1', b: '2', c: '3'};

It's clunky to assign a new object and loop over the elements with a for or foreach. It would be nice if there were something akin to map that could yield such a final result.

Imagine you could do this:

arr
  .map(item => new KeyValuePair(itemKey, itemValue)) // magically get itemKey/itemValue
  .toObjectFromKeyValuePairs();

That'd be it right there. But of course there's no such function built in.

ErikE
  • 48,881
  • 23
  • 151
  • 196

4 Answers4

3

If you're looking for a more functional approach to the code, you could use a library such as Lodash which makes code more succinct.

You could use _.fromPairs to convert pairs of data in arrays to key-value pairs of an object.

const convert = arr => _(arr)
    .map(s => _.split(s, '=', 2))
    .fromPairs()
    .value();
console.log(convert(['a=1', 'b=2', 'c=3']));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
4castle
  • 32,613
  • 11
  • 69
  • 106
  • +1 - Major endorsement for Lodash. Makes life in js land FAR easier, defensively protects against type issues, awesome useful functions, and much more... – random_user_name Jan 19 '17 at 00:32
  • Although I'd also encourage you to refactor `arr.map` to `_.map` and `s.split` to `_.split`, for strong "defensiveness" in your example... – random_user_name Jan 19 '17 at 00:33
  • @cale_b I agree. It took me a little bit to understand what you meant by "defensiveness" – 4castle Jan 19 '17 at 00:59
  • Note that querystrings such as `?a` or `?a=b=c` may not work with your code. – ErikE Jan 19 '17 at 01:01
  • @ErikE What would the desired output be for those inputs? – 4castle Jan 19 '17 at 01:05
  • That would be `{ a: '' }` and `{ a: 'b=c'}` as @some guessed correctly! – ErikE Jan 19 '17 at 01:10
  • @ErikE For that I would change the `map` function to `s => s.match(/(\w+)=(.*)/).slice(1)` – 4castle Jan 19 '17 at 01:16
  • I think using a RegEx might be a little heavy. – ErikE Jan 19 '17 at 01:17
  • @ErikE If that's an actual requirement of your question, you may want to be more specific in your question of what your priorities are and what the actual inputs are. As I said, a `for` loop would be the efficient solution. – 4castle Jan 19 '17 at 01:19
  • It's not a big deal, I just was pointing out for any readers using your solution that they need to think about parsing a value containing the delimiter. Regarding efficiency, there's efficient performance and efficient (concise or succinct) code. – ErikE Jan 19 '17 at 01:21
  • @ErikE, your original question mentioned nothing about querystrings or other delimiters. You asked for `['a=1', 'b=2', 'c=3'];` There's no extra `?` mark or `&` signs. You also didn't define which delimiter is important. For `a=b=c` without more specification it would be just as valid for it to become `{'a=b': 'c'}`. A perfect example of that is file extensions where only the last period maters. – gman Jan 19 '17 at 03:42
  • @gman You're right, I didn't mention querystrings, and I awarded the answer based on assessment without them. Sometimes people make better guesses than others, and in my experience this tends to correlate with greater experience and intelligence. It's just too darn bad that some people guessed worse. In general, while I appreciate LoDash and like libraries like that, I prefer a solution that doesn't use it. At this point, I don't really know what your criticism is. Did I pick the wrong answer? Do you feel I didn't reward someone who should have been rewarded? I upvoted this answer. – ErikE Jan 19 '17 at 03:47
  • @gman P.S. Thanks for the down vote on my question! It's a fine question as is, and I considered but declined editing it to throw in the weird cases. I was more interested in the functional aspect of the answer than the string-parsing part. Of the early, best answers, some's answer hit the spot for me. What are you worried about? – ErikE Jan 19 '17 at 03:49
  • You asked one question but then go about complaining about answers that don't cover cases that you didn't specify. Given your example of `['a=1', 'b=2', 'c=3']` it would be just as valid to error if the value is not an number or to truncate anything after a second `=` the same way `parseInt('123abc')` will truncate any extra stuff at the end. If you don't specify what you want people can't read your mind. If you want certain cases handed put those in your example. Also you mentioned `efficiently`. Nothing is efficient about parsing strings. Just put `{a:'1',b:'2',c:'3'}` in your code – gman Jan 19 '17 at 04:00
  • @gman The only person whining and complaining at great length is you. I never really wanted anyone to split strings for me. But since this post did, I wanted to point out for future readers that one must be careful about delimiters in the rest of the string. What could possibly be your problem with that?!? Please, take a chill pill. I voted the guy up right away... what possible beef can you have besides some weird internal state coming from being irrational and emotional? – ErikE Jan 19 '17 at 07:53
  • You asked a poor question, then berated every single answer for not handling cases you never asked to cover. But I'm the one whining. Ok. – gman Jan 19 '17 at 08:05
2

You could use reduce, split and slice:

var arr = ['a=1', 'b=2', 'c=3'];

var out = arr.reduce(
  function (output, input) {
    if (typeof input === 'string') {
      var key = input.split('=',1)[0];
      output[key] = input.slice( key.length + 1 );
    }
    return output;
  },
  {}
);

I use the second argument of split to make it stop after the first = found. Then using slice on the input (treating it as an array of characters) allows the value to contain the = separator as in the case of a=b=c.

By using slice, the value will always be a string, even if it is an empty one. If you want to have null values you could change the line to:

output[key || null] = input.slice( key.length + 1 ) || null;

The type check for string is present since split throws error on null and undefined.

If you wanted to parse the current page's query string for example, you could do it using the above technique just like this:

function getQueryStringParams() {
  var reEncodedSpace = /\+/g;
  return location.search.length > 1 // returns false if length is too short
    && location.search.slice( 1 ).split( '&' ).reduce(
      ( output, input ) => {
        if ( input.length ) {
          if ( output === false ) output = {};
          input = input.replace( reEncodedSpace, ' ' ); //transport decode
          let key = input.split( '=', 1 )[ 0 ]; // Get first section as string
          let value = decodeURIComponent( input.slice( key.length + 1) ); // rest is value
          key = decodeURIComponent( key ); // transport decode

          // The standard supports multiple values per key.
          // Using 'hasOwnProperty' to detect if key is pressent in output,
          // and using it from Object.prototype instead of the output object
          // to prevent a key of 'hasOwnProperty' to break the code.
          if ( Object.prototype.hasOwnProperty.call( output, key ) ) {
            if ( Array.isArray( output[ key ] ) ) {
              // Third or more values: add value to array
              output[ key ].push( value );
            } else {
              // Second value of key: convert to array.
              output[ key ] = [ output[ key ], value ];
            }
          } else {
            // First value of key: set value as string.
            output[ key ] = value;
          }
        }
        return output;
      },
      false
    );
}

The function returns false if the search is empty.

some
  • 48,070
  • 14
  • 77
  • 93
  • After getting the hint about reduce from Jaromanda X, I figured out something similar to this. Though, I see no reason to split AND slice, when you could split once and use [1]. – ErikE Jan 19 '17 at 00:44
  • @ErikE Well, if your input is 'a=b=c' you get the wrong result if you just use split. – some Jan 19 '17 at 00:45
  • I looked at using `var pieces = input.split('=');` then use `pieces[0]` and `pieces[1]` — it ends up with the ugly `output[pieces[0]]` but then doesn't have to call `input.slice(...)` ... thoughts? – Stephen P Jan 19 '17 at 00:46
  • Using `.split('=', 2)` would eliminate the need for `slice` while still being safe. – 4castle Jan 19 '17 at 00:50
  • If the input is "a=b=c" you get `{a:'b'}` if you only use split (even if you use `split('=',2)`). With slice, as I have written it, you get `{a:'b=c'}` – some Jan 19 '17 at 00:52
  • I had not thought about the case of `a=b=c`. Thanks! – ErikE Jan 19 '17 at 00:56
  • @4castle, nope. If the input is "a=b=c", you get `{a:'b'}` if you only use split, even if you use `split('=',2)`. I don't know about you, but I expect the result to be `{a:'b=c'}`, and that you get when you use both split and slice.. – some Jan 19 '17 at 00:57
  • Hope you don't mind the extra real-world exmaple code at the bottom... Feel free to remove it. – ErikE Jan 19 '17 at 01:11
  • @ErikE I don't mind, but the code your provided doesn't work (`key.slice(key.length + 1)`), and you create a DOM-object to get the search part, while it is available on the location object. I improved the code and fixed the bugs :) – some Jan 19 '17 at 01:32
  • Hey, thanks! I was going to get to refining it in a bit. Yeah, definite typo there with `key.slice`... – ErikE Jan 19 '17 at 01:33
  • Is `slice(1)` better than or somehow more idiomatic than `substr(1)`??? – ErikE Jan 19 '17 at 01:35
  • There is actually `substr`, `substring` and `slice`, all with a little bit different behavior and I don't remember which is which of `substr` or `substring` (one came with Netscape and one with MSIE if I remember correctly). The nice thing with the newer `slice` is that is behaves the same on all platforms, and the `slice` on arrays have the same semantics. – some Jan 19 '17 at 01:48
  • `substr` and `substring` do the same when you use only the first parameter. For the second parameter, one of them wants the length of the final string, and one wants the offset of the original string (I don't remember which, I always have to check). But with one parameter it doesn't matter. All browsers worth supporting support both `substr` and `substring`. – ErikE Jan 19 '17 at 01:52
  • It has not always been like that. `slice` is more generic since it also works on arrays. – some Jan 19 '17 at 02:06
  • @ErikE Oops, forgot to fix that in the function. – some Jan 19 '17 at 02:08
  • Funny how I like this function better than **any** of [the answers to this question](http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript) or [this one](http://stackoverflow.com/questions/2907482/how-to-get-the-query-string-by-javascript). And no problem about the fix, I was the one who put in a bad implementation to begin with. – ErikE Jan 19 '17 at 02:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/133504/discussion-between-some-and-erike). – some Jan 19 '17 at 02:27
0

If you're willing to spare having one additional line for declaration, this could work for you. Although using a library like lodash or underscore, as mentioned in other answers would certainly help:

var arr = ['a=1', 'b=2', 'c=3'];
var expectedResult = {};
arr.map(function(value) { 
    var kv = value.split("="); 
    expectedResult[kv[0]] = kv[1]; 
    return value    
})
random_user_name
  • 25,694
  • 7
  • 76
  • 115
Dondrey Taylor
  • 290
  • 3
  • 10
  • I found out that reduce works pretty nicely since you can pass in a seed accumulator value as the second parameter (though then you have to return the accumulator at the end of the function). – ErikE Jan 19 '17 at 00:36
  • 1
    Also, please note that a querystring such as `?a` will break with your code. – ErikE Jan 19 '17 at 00:36
  • As will a querystring such as `?a=b=c`, where the key of `a` has a value of `b=c`. – ErikE Jan 19 '17 at 01:00
  • @ErikE ah yes, you're totally right, the example I provided doesn't account for any of those scenarios. – Dondrey Taylor Jan 19 '17 at 23:23
0

Try The Below Code.

let arr = ['a=1', 'b=2', 'c=3'];
let b=arr.toString();
b='{"'+(b.split('=').join('":"').split(',').join('","'))+'"}';
b=$.parseJSON(b);
console.log(b);

You will get the required output.

Aravind
  • 1,145
  • 6
  • 18
  • 44
  • 1
    Converting to Json is not a good idea, because the inputs could have quote marks or curly braces in them. Splitting on '=' can lose values such as `a=b=c`. – ErikE Jan 19 '17 at 01:00