1

Is there a way to bulk set associative array keys and values in JavaScript, similar to PHP's shorthand array declaration below?

$array = [
  'foo' => 'val1',
  'bar' => 'val2',
  'baz' => 'val3'
];

The only way I can think of to declare associative arrays in JS is something like this:

var newArray = new Array();
newArray['foo'] = 'val1';
newArray['bar'] = 'val2';
newArray['baz'] = 'val3';

Or something like this:

var newArray = new Array(),
  keys = ['foo', 'bar', 'baz'],
  vals = ['val1', 'val2', 'val3'];

for(var i = 0; i < keys.length; i++) {
  newArray[ keys[i] ] = vals[i];
}

That last one can cause a mess in certain cases, but I wanted to list it since it's doable.

If there isn't a way like that top example, then that's fine. It would've been nice. I tried using a JS object literal already with jQuery like this:

var myObject = {
  foo: $('foo'),
  bar: $(this.foo).find('bar'),
  baz: $(this.bar).data('baz')
}

Unfortunately it seems that JS object literals don't allow evaluating code to set values for their properties. If I turned those properties into methods that returned values instead of permanently storing the values/references in the properties, then they would have to run every time they're called; I really don't want to do that if possible.

Thanks in advance.

JSn1nj4
  • 87
  • 12
  • Actually it would need to be: `baz: myObject.foo.find('baz')` – Malk Mar 16 '15 at 22:14
  • @Malk Ah, you're right. I didn't realize it was self-referencing. – Stryner Mar 16 '15 at 22:15
  • @Malk: That wouldn't work either. When the property initializers within the object initializer above are run, no value has been assigned to `myObject` yet, so it's `undefined`. – T.J. Crowder Mar 17 '15 at 07:34

2 Answers2

1

PHP's "associative arrays" — an ordered set of name/value pairs — are a fairly rare data structure, particularly rare to find as a feature of the language rather than as a library class. JavaScript doesn't have them — yet. ES6 will introduce better iteration semantics to the language generally, and a standard library Map type that takes advantage of those semantics so that when iterated, it visits entries in key insertion order; more below.

Before ES6

Until ES6, you can use objects, as you seem to know, like so:

var obj = {
  foo: 'val1',
  bar: 'val2',
  baz: 'val3'
};

...but the properties have no order. If order is important, you'd have to track the order separately, perhaps by having the keys in an array:

var keys = ['foo', 'bar', 'baz'];
var obj = {
  foo: 'val1',
  bar: 'val2',
  baz: 'val3'
};
var i;
for (i = 0; i < keys.length; ++i) {
    console.log(obj[keys[i]]);
}

This is less delicate than parallel arrays, because you only have to worry about the order of one array.

As of ES6

ES6's Map defines that when iterated, the map's entries are visited in the order in which the keys were inserted. You can also initialize a Map instance by using an array of key/value arrays, like so:

var map = new Map([
    ["foo", "val1"],
    ["bar", "val2"],
    ["baz", "val3"]
]);

So that will be the equivalent form when it's widely-supported.

More about Map in this article (and the draft spec).


Unfortunately it seems that JS object literals don't allow evaluating code to set values for their properties

Yes, they do. In your example, the foo property will be a jQuery object with a set of elements matching the CSS selector foo (a set which is probably empty, as there is no foo element).

Example:

var obj = {
    foo: 6 * 7
};
console.log(obj.foo); // 42

You can't (yet) use an expression for the property name in a property initializer like that, just the value. In ES6, you'll be able to use an expression for the property name as well:

// As of ES6
var obj = {
    ['f' + 'o' + 'o']: 6 * 7
};
console.log(obj.foo); // 42

Meanwhile, in ES5 we can do it, but not in the initializer:

// Now
var obj = {};
obj['f' + 'o' + 'o'] = 6 * 7;
console.log(obj.foo); // 42

I tried using a JS object literal already with jQuery like this:

var myObject = {
  foo: $('foo'),
  bar: $(this.foo).find('bar'),
  baz: $(this.bar).data('baz')
}

There are a couple of problems there:

  1. $('foo') looks for elements with the tag foo — there is no foo HTML tag. Perhaps you meant #foo or .foo (or perhaps foo was just meant as a stand-in for div or span or whatever).

  2. Your foo property's value is already a jQuery object, no need to wrap it in another $() call.

  3. this has no special meaning within an object initializer, it's the same as this outside the object initializer, not a reference to the object being initialized. Malk's suggestion would also not work, because as of when the properties are being initialized, myObject hasn't had any value assigned to it yet. Here's how your overall expression above is evaluated:

    1. Prior to any step-by-step code in the scope, a variable called myObject is created with the value undefined.

    2. When the expression is reached in the step-by-step execution of the code, start the assignment expression by evaluating the right-hand side (the bit after the =).

    3. To start the object initializer expression, a new object is created but not stored anywhere.

    4. Each property initializer in the object initializer is processed in source code order (yes, that's guaranteed by the spec), like so:

      1. Evaluate the expression defining its property.

      2. Create the property on the object using the given key and the evaluated value.

    5. When the object initializer is complete, the resulting value (the object reference) is assigned to myObject.

To refer to properties of the object from the expressions defining other properties of the object, you have to do a series of statements rather than a single statement:

var myObject = {
  foo: $('foo') // or $('#foo') or $('.foo') or whatever it is
};
myObject.bar = myObject.foo.find('bar');
myObject.baz = myObject.bar.data('baz');

Alternately, you could do something convoluted with temporary variables:

var foo, bar, myObject = {
  foo: (foo = $('foo')),
  bar: (bar = foo.find('bar')),
  baz: bar.data('baz')
};

...because the result of an assignment expression is the value that was assigned. (No enduring connection between the properties and the variables is maintained.) But then you have those temp variables lying around. You could wrap the entire thing in a function so the temp vars are only around temporarily:

var myObject = (function() {
  var foo, bar;
  return {
    foo: (foo = $('foo')),
    bar: (bar = foo.find('bar')),
    baz: bar.data('baz')
  };
})();

...but that's getting pretty convoluted. :-)


Re your comment below:

are there any limits on what kind of expressions can be evaluated into property values?

No, the expression processing for the values of property initializers is exactly the same as expression handling elsewhere in the scope containing the object initializer that the property initializer is part of.

Here's an example grabbing some DOM elements via jQuery:

var myObject = {
  foo: $('.foo')
};
myObject.bar = myObject.foo.find('.bar');
myObject.baz = myObject.bar.data('baz');

myObject.foo.css("color", "green");
myObject.bar.css("color", "blue");
$("<div>").html(myObject.baz).appendTo(document.body); // "I'm the data"
<div class="foo">
  This is a .foo
  <div class="bar" data-baz="I'm the data">
    This is a .bar inside the .foo
  </div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

Side note: I wouldn't use .data if all you're doing is accessing data-* attributes; I'd use attr instead. data sets up a data cache for the object, initializes that cache from all data-* attributes on the object, and then works from the cache, not the attributes. Overkill if you're just reading an attribute value.

Or using my convoluted function:

var myObject = (function() {
  var foo, bar;
  return {
    foo: (foo = $('.foo')),
    bar: (bar = foo.find('.bar')),
    baz: bar.data('baz')
  };
})();

myObject.foo.css("color", "green");
myObject.bar.css("color", "blue");
$("<div>").html(myObject.baz).appendTo(document.body); // "I'm the data"
<div class="foo">
  This is a .foo
  <div class="bar" data-baz="I'm the data">
    This is a .bar inside the .foo
  </div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • I'm not sure associative arrays or maps qualify as rare, and the mainstream languages (scripting languages anyway) that didn't have them have been adding them (e.g. JavaScript, Ruby). – JMM Mar 16 '15 at 22:19
  • @JMM: The rarity isn't maps, it's **ordered** maps. Those are and remain a "fairly rare" data structure, most maps do not apply ordering to keys. Some do, of course, such as Java's `SortedMap` (which is really just a map with a list of keys, like the suggestion above). – T.J. Crowder Mar 16 '15 at 22:20
  • Yes, ordered maps is what I was referring to. If I'm not mistaken they're in ES next and Ruby added them "recently". It's an incredibly useful data structure, so it's sad that there was any rarity to their availability. – JMM Mar 16 '15 at 22:27
  • @JMM: The `Map` being added to ES6 is primarily around getting rid of the problem with using objects as maps, making them directly iterable, etc. I don't think they're sorted (but they can be subclassed, so...). I've rarely had any need for a sorted map, in 25 years of professional development. It's definitely come up at least once, possibly twice, probably not three times. :-) – T.J. Crowder Mar 16 '15 at 22:30
  • @TJCrowder I believe the `Map` in ES next is ordered. "I've rarely had any need for a sorted map..." -- to each his own. I've used them frequently in PHP and looking forward to getting them in JS. – JMM Mar 16 '15 at 22:34
  • @JMM: Can you point me to something on that? I haven't read a lot on the maps yet, and of course the spec is obtuse to the degree of being unintelligible. (We used to think the ES5 spec was bad. More fools us.) Re using them in JS: The one time I know I remember needing them was in JS; took less than an hour to implement. – T.J. Crowder Mar 16 '15 at 22:38
  • [This article](http://www.nczonline.net/blog/2012/10/09/ecmascript-6-collections-part-2-maps/) claims they're ordered, but gives no citation. But it's Nicholas Zakas, who's usually very clued into JS stuff. – T.J. Crowder Mar 16 '15 at 22:40
  • Ah hah - [§23.1.3.5](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-map.prototype.foreach): *"...`forEach` calls* callbackfn *once for each key/value pair present in the map object, in key insertion order."* – T.J. Crowder Mar 16 '15 at 22:43
  • Not that it proves it, but the [MDN page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) currently says so. "and of course the spec is obtuse to the degree of being unintelligible." Of course :) I admit I haven't pored over the current version of that to decipher what it says about it. I'll take a look at your citation. "in JS; took less than an hour to implement." Perhaps we can agree that it's far better for it to be baked into the language though. – JMM Mar 16 '15 at 22:46
  • @JMM: Thanks for pointing me at the info that the new `Map` is sorted, I've been able to improve the answer as a result, and learned something useful. Much appreciated! I'm not sure I do agree they're better as a language feature, but it's useful to have them in the standard library(ies) associated with a language, which is basically what's happening with JS as it did with Java with `SortedMap`. Then we don't have to write it, and we can easily read others' code using it. :-) – T.J. Crowder Mar 16 '15 at 22:50
  • @TJCrowder No problem, that's great. "*I'm not sure I do agree they're better as a language feature, but it's useful to have them in the standard library(ies) associated with a language, which is basically what's happening with JS*" With JS it's close to being the same thing. I actually would like syntactic sugar for it, but it's still a big step forward that there's at least a built-in class for it. – JMM Mar 16 '15 at 22:57
  • @T.J.Crowder are there any limits on what kind of expressions can be evaluated into property values? if I put my object into a jQuery anonymous function like this `$(function(){ ... });` shouldn't the properties that "reference" DOM elements have a reference to those DOM elements? The ones that refer to DOM elements keep coming up undefined (even though I test the same code in the JS console to be sure it works), and the one that refers to a DOM element's data doesn't get it because the property that should refer to the DOM element itself is `undefined`. – JSn1nj4 Mar 16 '15 at 23:26
  • @s0rT4_w3Bn1Nj4: No, no limits, it's the same expression eval as everywhere else. There must be either a timing issue (the elements don't exist as of when you try to get them, although in your comment you showed a `ready` handler, so...) or there's some misunderstanding when you're retrieving them. – T.J. Crowder Mar 17 '15 at 07:33
  • @s0rT4_w3Bn1Nj4: I've added to the end of the answer to address your attempted object literal from the question, and your question above in more detail. – T.J. Crowder Mar 17 '15 at 07:56
  • 1
    I think I know how I'm gonna handle this now. Thanks for all the help. Much appreciated. – JSn1nj4 Mar 17 '15 at 14:03
0

If you're just trying to declare a Javascript object with a bunch of property/value pairs, you can do that like this:

var myData = {
    foo: 'val1',
    bar: 'val2',
    baz: 'val3'
};

Which you can then access as:

console.log(myData.foo);   // `val1`    
console.log(myData.bar);   // `val2`
console.log(myData.baz);   // `val3`

Note, Javascript does not have a data type called an "associative array". Most Javascript developers use an object with property names to solve similar problems, but Javascript objects do not maintain an ordered set of properties - they are unordered.


You can't do something like this:

var myObject = {
  foo: $('foo'),
  bar: $(this.foo).find('bar'),
  baz: $(this.bar).data('baz')
}

Because this does not point to the object itself from within the declaration. Javascript just doesn't work that way. You would have to either add those properties later or turn them into functions or use a different structure. You will also have to make absolutely sure that the DOM is completely ready at the time of static declaration.

What might make sense is something like this:

var myObject = {
    init: function() {
        this.foo = $('foo');
        this.bar = this.foo.find('bar');
        this.baz = this.foo.find('baz');
    }
}

// then, sometime when you know the DOM is ready
myObject.init();

Or, if you want to do it in just one function call at the time the DOM is ready, you can skip the static declaration and just make one function that you declare and execute when you know the DOM is ready:

var myObject = (function() {
    var obj = {};
    obj.foo = $('foo');
    obj.bar = obj.foo.find('bar');
    obj.baz = obj.foo.find('baz');
    return obj;
})();
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Flat values are fine. The trouble I'm having has to do with object properties that reference DOM elements using jQuery, like the myObject example I put in. – JSn1nj4 Mar 16 '15 at 23:30
  • So `this` doesn't refer to the the object in an object literal... Ok. Not what I expected, but I guess I can deal with that... Will it work if I replace `this` with `myObject`? – JSn1nj4 Mar 16 '15 at 23:53
  • @s0rT4_w3Bn1Nj4 - no, it will not work if you replace `this` with `myObject` because `myObject` does not exist until AFTER the end of the object literal declaration so it cannot be used in the middle of the declaration. Yes, this is sometimes annoying, but it is the way Javascript is. – jfriend00 Mar 16 '15 at 23:57
  • Wow... That really bites. Ok, thanks for explaining. I'll probably just have to declare the object literal first. Thanks! – JSn1nj4 Mar 17 '15 at 13:58
  • Now that I reread your answer, I have to ask something: JS doesn't have an associative array? So syntax like this `myArray['key']` doesn't refer to an array, just an object with properties? – JSn1nj4 Mar 17 '15 at 18:48
  • @s0rT4_w3Bn1Nj4 - jQuery has objects and arrays. An array is derived from an object. Objects have unordered properties that can be accessed with either the dot syntax as in `obj.prop` or the bracket syntax as in `obj["prop"]`. Arrays have ordered properties that can be accessed via numeric index as in `arr[3]`. Because arrays are also objects, you can use object properties on an array too. Neither of these types is exactly what most people think of as an associative array, but as I said in my answer, except for ordering, you can generally use an object with its properties for that purpose. – jfriend00 Mar 17 '15 at 22:41
  • @s0rT4_w3Bn1Nj4 - I will add that in ES6 (some features available now in newest browsers or server-side JS engines, the rest coming), there are some other data types such as a `Set` and a `Map` that might be relevant for you. A `Map` is like an object except the key can be any type (it doesn't have to be a string) and the `Map` remembers the insertion order (objects do not guarantee order). This means you use things that don't have a useful string conversion like an object as the key for a `Map`. – jfriend00 Mar 17 '15 at 22:47
  • Thanks for the responses. Appreciate it. If JS never has associative arrays, I guess it's no big deal. Just have to work around it. – JSn1nj4 Mar 24 '15 at 12:28