67

What is the cleanest way to make Javascript do something like Python's list comprehension?

In Python if I have a list of objects whose name's I want to 'pull out' I would do this...

list_of_names = [x.name for x in list_of_objects]

In javascript I don't really see a more 'beautiful' way of doing that other than just using a for loop construct.

FYI: I'm using jQuery; maybe it has some nifty feature that makes this possible?

More specifically, say I use a jQuery selector like $('input') to get all input elements, how would I most cleanly create an array of all the name attributes for each of these input elements--i.e., all of the $('input').attr('name') strings in an array?

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Chris W.
  • 37,583
  • 36
  • 99
  • 136
  • 1
    You can do better than that Python ;-) Well, it's sorta hackish because the language syntax isn't all there but ... see Functional JavaScript -- http://osteele.com/sources/javascript/functional/ (actually, there is extra magic with Python generators but...) –  Feb 11 '11 at 00:49

11 Answers11

56

generic case using Array.map, requires javascript 1.6 (that means, works on every browser but IE < 9) or with an object augmenting framework like MooTools works on every browser:

var list_of_names = document.getElementsByTagName('input').map(
  function(element) { return element.getAttribute('name'); }
);

If dealing with an iterable that doesn't provide map (such as generators), spread syntax can be used to first create an array:

[...Array(10).keys()].map(x => x * 2 + 3)

Spread syntax will also work with arrays, so if you need to apply map to any iterable, it's safest to first spread it into a list.

(Note this example also uses an arrow function, which requires ES6 support. Most browser versions support both or neither. IE supports neither, if it itself needs to be supported, though IE has been replaced by Edge for awhile.)

jQuery specific example, works on every browser:

var list_of_names = jQuery.map(jQuery('input'), function(element) { return jQuery(element).attr('name'); });

the other answers using .each are wrong; not the code itself, but the implementations are sub-optimal.

Edit: there's also Array comprehensions introduced in Javascript 1.7, but this is purely dependant on syntax and cannot be emulated on browsers that lack it natively. This is the closest thing you can get in Javascript to the Python snippet you posted. However that got removed from the language

outis
  • 75,655
  • 22
  • 151
  • 221
gonchuki
  • 4,064
  • 22
  • 33
  • note that I directly used `jQuery()` instead of `$()` as I didn't put the snippet inside a closure :) feel free to replace it with $ if you are running inside `(function($){ })(jQuery)` – gonchuki Feb 11 '11 at 01:03
  • I don't understand your implementation using the `map` function...? it takes an array and then a function... how is `jQuery('input')` an array? – Damien-Wright Feb 11 '11 at 01:05
  • The jQuery object is an array-like object, so it works as the first parameter on all the functions included in the Utilities module that accept an array-like object as a parameter. If you did `jQuery('input').map(...)` instead, then you would have ended with an array-like object (the jQuery object wrapping an array) in `list_of_names` instead of a vanilla javascript Array. – gonchuki Feb 11 '11 at 01:19
  • nice one, best answer i see here then :) +1 – Damien-Wright Feb 11 '11 at 01:35
  • thanks :), remember you can mostly always reduce the jQuery case to simply `var list_of_names = $.map($('input'), function(element) { return $(element).attr('name'); });` – gonchuki Feb 11 '11 at 01:41
  • Great! The jQuery map function is definitely what I was looking for. I noticed it in the jQuery docs, but it never clicked with me what it was there for. Still not as pretty as Python list comprehension but $.map($('input'), function(e) { return $(e).attr('name') }) is probably about as clean as you can get. – Chris W. Feb 11 '11 at 01:44
  • The original/top example does not work with `getElementsByTagName` returning a `HTMLCollection` which has no `map` attribute. Can someone with rep. to edit please integrate the spread syntax (second version) with that example. – matec Jan 04 '22 at 20:36
11

A list comprehension has a few parts to it.

  1. Selecting a set of something
  2. From a set of Something
  3. Filtered by Something

In JavaScript, as of ES5 (so I think that's supported in IE9+, Chrome and FF) you can use the map and filter functions on an array.

You can do this with map and filter:

var list = [1,2,3,4,5].filter(function(x){ return x < 4; })
               .map(function(x) { return 'foo ' + x; });

console.log(list); //["foo 1", "foo 2", "foo 3"]

That's about as good as it's going to get without setting up additional methods or using another framework.

As for the specific question...

With jQuery:

$('input').map(function(i, x) { return x.name; });

Without jQuery:

var inputs = [].slice.call(document.getElementsByTagName('input'), 0),
    names = inputs.map(function(x) { return x.name; });

[].slice.call() is just to convert the NodeList to an Array.

Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • 1
    This has become much more readable now with ES6 arrow functions: `var list = [1,2,3,4,5].filter(x => x < 4).map(x => 'foo ' + x);` – joeytwiddle Feb 19 '16 at 04:42
6

Those interested in "beautiful" Javascript should probably check out CoffeeScript, a language which compiles to Javascript. It essentially exists because Javascript is missing things like list comprehension.

In particular, Coffeescript's list comprehension is even more flexible than Python's. See the list comprehension docs here.

For instance this code would result in an array of name attributes of input elements.

[$(inp).attr('name') for inp in $('input')]

A potential downside however is the resulting Javascript is verbose (and IMHO confusing):

var inp;
[
  (function() {
    var _i, _len, _ref, _results;
    _ref = $('input');
    _results = [];
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      inp = _ref[_i];
      _results.push($(inp).attr('name'));
    }
    return _results;
  })()
];
Chris W.
  • 37,583
  • 36
  • 99
  • 136
  • It may be worth noting that this works because jQuery objects act "like arrays" which is what Coffeescript is expecting in its for loops. – Chris W. Jul 17 '12 at 20:49
  • 3
    Is it really "beautiful" Javascript if it outputs what we see above? – XML Sep 21 '12 at 17:51
  • 1
    @XMLilley, I agree--I'm very torn about Coffeescript. That's why I included the JS ouput, to make it clear that Coffeescript isnt all sunshine and rainbows. – Chris W. Sep 21 '12 at 19:00
  • @XMLilley This is why we have [source maps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/). – spender May 21 '15 at 10:04
4

So, python's list comprehensions actually do two things at once: mapping and filtering. For example:

list_of_names = [x.name for x in list_of_object if x.enabled]

If you just want the mapping part, as your example shows, you can use jQuery's map feature. If you also need filtering you can use jQuery's "grep" feature.

jpsimons
  • 27,382
  • 3
  • 35
  • 45
  • 7
    Actually `$.map()` can be used for filtering too! If you `return null` inside the mapping function jQuery removes that element from the array. – Chris W. Mar 31 '11 at 17:05
2

Added by @yurik: Do not use. Array comprehensions were proposed but later removed.

Array comprehensions are a part of the ECMAScript 6 draft. Currently (January 2014) only Mozilla/Firefox's JavaScript implements them.

var numbers = [1,2,3,4];
var squares = [i*i for (i of numbers)]; // => [1,4,9,16]
var somesquares = [i*i for (i of numbers) if (i > 2)]; // => [9,16]

Though ECMAScript 6 recently switched to left-to-right syntax, similar to C# and F#:

var squares = [for (i of numbers) i*i]; // => [1,4,9,16]

http://kangax.github.io/es5-compat-table/es6/#Array_comprehensions

Yuri Astrakhan
  • 8,808
  • 6
  • 63
  • 97
Brian Burns
  • 20,575
  • 8
  • 83
  • 77
2

A re-usable way of doing this is to create a tiny jQuery plugin like this:

jQuery.fn.getArrayOfNames = function() {
    var arr = [];
    this.each( function() { arr.push(this.name || ''); } );
    return arr;
};

Then you could use it like this:

var list_of_names = $('input').getArrayOfNames();

It's not list comprehension, but that doesn't exist in javascript. All you can do is use javascript and jquery for what it's good for.

typeof
  • 5,812
  • 2
  • 15
  • 19
2

Do this in 2020

const names = collection.map(x => x.name);

MDN | Array.prototype.map()

Old Answer from 2012

Yeah—I miss list comprehensions too.

Here's an answer that's slightly less verbose than @gonchuki's answer and converts it into an actual array, instead of an object type.

var list_of_names = $('input').map(function() {
    return $(this).attr('name');
}).toArray();

A use case of this is taking all checked checkboxes and joining them into the hash of the URL, like so:

window.location.hash = $('input:checked').map(function() {
    return $(this).attr('id');
}).toArray().join(',');
jedmao
  • 10,224
  • 11
  • 59
  • 65
1

There is a one line approach, it involves using a nested closure function in the constructor of the list. And a function that goes a long with it to generate the sequence. Its defined below:

var __ = generate = function(initial, max, list, comparision) {
  if (comparision(initial))
    list.push(initial);
  return (initial += 1) == max + 1 ? list : __(initial, max, list, comparision);
};

[(function(l){ return l; })(__(0, 30, [], function(x) { return x > 10; }))];
// returns Array[20]
var val = 16;
[(function(l){ return l; })(__(0, 30, [], function(x) { return x % val == 4; }))];
// returns Array[2]

This is a range based implementation like Python's range(min, max) In addition the list comprehension follows this form:

[{closure function}({generator function})];

some tests:

var alist = [(function(l){ return l; })(__(0, 30, [], function(x) { return x > 10; }))];
var alist2 = [(function(l){ return l; })(__(0, 1000, [], function(x) { return x > 10; }))];
// returns Array[990]
var alist3 = [(function(l){ return l; })(__(40, 1000, [], function(x) { return x > 10; }))];
var threshold = 30*2;
var alist3 = [(function(l){ return l; })(__(0, 65, [], function(x) { return x > threshold; }))];
// returns Array[5]

While this solution is not the cleanest it gets the job done. And in production i'd probably advise against it.

Lastly one can choose not to use recursion for my "generate" method as it would do the job quicker. Or even better use a built in function from of the many popular Javascript libraries. Here is an overloaded implementation that would also accommodate for object properties

// A list generator overload implementation for
// objects and ranges based on the arity of the function.
// For example [(function(l){ return l; })(__(0, 30, [], function(x) { return x > 10; }))] 
// will use the first implementation, while
// [(function(l){ return l; })(__(objects, 'name', [], function(x, y) { var x = x || {}; return x[y] }))];
// will use the second.

var __ = generator = function(options) {
  return arguments.length == 4 ?
// A range based implemention, modeled after pythons range(0, 100)
  (function (initial, max, list, comparision) {
    var initial = arguments[0], max = arguments[1], list = arguments[2], comparision = arguments[3];
    if (comparision(initial))
      list.push(initial);
    return (initial += 1) == max + 1 ? list : __(initial, max, list, comparision);
  })(arguments[0], arguments[1], arguments[2], arguments[3]):
// An object based based implementation. 
  (function (object, key, list, check, acc) {
    var object = arguments[0], key = arguments[1], list = arguments[2], check = arguments[3], acc = arguments[4];
    acc = acc || 0;
    if (check(object[acc], key))
      list.push(object[acc][key]);
    return (acc += 1) == list.length + 1 ? list : __(object, key, list, check, acc); 
  })(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
};

Usage:

var threshold = 10;
[(function(l){ return l; })(__(0, 65, [], function(x) { return x > threshold; }))];
// returns Array[5] -> 60, 61, 62, 63, 64, 65
var objects = [{'name': 'joe'}, {'name': 'jack'}];
[(function(l){ return l; })(__(objects, 'name', [], function(x, y) { var x = x || {}; return x[y] }))];
// returns Array[1] -> ['Joe', 'Jack']
[(function(l){ return l; })(__(0, 300, [], function(x) { return x > 10; }))];

The syntax sucks I know!

Best of luck.

1

This is an example of a place where Coffeescript really shines

pows = [x**2 for x in foo_arr]
list_of_names = [x.name for x in list_of_objects]

The equivalent Javascript would be:

var list_of_names, pows, x;

pows = [
  (function() {
    var _i, _len, _results;
    _results = [];
    for (_i = 0, _len = foo_arr.length; _i < _len; _i++) {
      x = foo_arr[_i];
      _results.push(Math.pow(x, 2));
    }
    return _results;
  })()
];

list_of_names = [
  (function() {
    var _i, _len, _results;
    _results = [];
    for (_i = 0, _len = list_of_objects.length; _i < _len; _i++) {
      x = list_of_objects[_i];
      _results.push(x.name);
    }
    return _results;
  })()
];
Evan
  • 7,396
  • 4
  • 32
  • 31
0

Using jQuery .each() function, you can loop through each element, get the index of the current element and using that index, you can add the name attribute to the list_of_names array...

var list_of_names = new Array();

$('input').each(function(index){
  list_of_names[index] = $(this).attr('name');
});

While this is essentially a looping method, which you specified you did not want, it is an incredibly neat implementation of looping and allows you to run the loop on specific selectors.

Hope that helps :)

Damien-Wright
  • 7,296
  • 4
  • 27
  • 23
0

Array comprehensions, which are very similar to Python's list comprehensions, were proposed for ECMAScript 6 but dropped in favor of Array.map and Array.filter. One advantage of the Array methods is they can be implemented if not supported natively; libraries such as core.js do this.

Note the following examples use arrow functions, which require ES6 support and can't be polyfilled. This shouldn't be an issue, as most browsers that support one of the features used should support all (some browsers added the features at different versions, but there wasn't a large gap). IE is the exception, supporting only Array.map (and that only beginning with IE 9). Hopefully, IE won't need to be supported, as it's been replaced by Edge for some time now.

Array.map

Array.map takes a function that will transform each element. Basically, the function corresponds to the expression in a Python list comprehension.

var list_of_names = list_of_objects.map(obj => obj.name);

Non-Arrays

If dealing with a collection or iterable that doesn't provide map (such as a generator or a NodeList), it can be converted to an array using one of a couple different features, such as spread syntax or Array.from.

Spread Syntax

Spread syntax, which is only supported in specific contexts, works by expanding the elements from an iterable into that context:

[...Array(10).keys()].map(x => x+3)

Spread syntax will also work with arrays (since they're iterables), though it's not necessary to do so:

var ints = [1,2,3,4];
[...ints].map(x => x*2)

If you need to apply map to an iterable that may or may not be an array, it's safest to first spread it into a list.

Array.from

The Array.from method converts not only iterables but anything array-like (i.e. has a length property and integer indices) to an array:

var list_of_names = Array.from(document.querySelectorAll('a[name]'))
        .map(elt => elt.name);

As with spread syntax, Array.from is safe to apply to arrays. If you need to apply map to non-iterable array-like objects as well as arrays and iterables, it's the safest, most general approach.

Array.filter

if clauses in a Python list comprehension translate to Array.filter:

// odd natural numbers < 10
[...Array(10).keys()].filter(x => x%2)

If the Python example were expanded to include an if clause as:

list_of_names = [x.name for x in list_of_objects if '-' not in x.value]

This would translate to:

var list_of_names = Array.from(list_of_objects)
  .filter(obj => ! obj.value.contains('-'))
  .map(obj => obj.name)

JQuery

JQuery provides .map and jQuery.map, both of which incorporate the functionality of Array.flatMap and Array.filter. Depending on the jQuery version, jQuery.map can be applied to arrays and array-like objects*, though not generators (according to my testing). Note .map differs from jQuery.map in that it passes arguments in the opposite order (index, item rather than item, index) and returns a jQuery object.

$('input').map((i, a) => a.name);
$.map(Array(10), (_, i) => i);
$.map(document.forms, elt => elt.id);

*The jQuery documentation asserts that array-like objects should first be converted to an array with (e.g.) jQuery.makeArray. Applying jQuery.map directly to array-like objects worked when I tested it, though there may be some cases where this causes issues.

outis
  • 75,655
  • 22
  • 151
  • 221