I tried to implement a definitive, reliable URL query string parser that handles every corner case:
- it tries to be efficient by avoiding regex
- it takes full URLs or just query strings (as long as the query string begins with a question mark)
- it ignores the hash value
- it handles multiple equal parameter names
- it handles parameter names that equal built-in JavaScript methods and keywords
What do you think - did I miss something?
function parseURLParams(url) {
if (url === null) return;
var queryStart = url.indexOf("?") + 1,
queryEnd = url.indexOf("#") + 1 || url.length + 1,
query = url.slice(queryStart, queryEnd - 1);
if (query === url || query === "") return;
var params = {},
nvPairs = query.replace(/\+/g, " ").split("&");
for (var i=0; i<nvPairs.length; i++) {
var nv = nvPairs[i],
eq = nv.indexOf("=") + 1 || nv.length + 1,
n = decodeURIComponent( nv.slice(0, eq - 1) ),
v = decodeURIComponent( nv.slice(eq) );
if ( n !== "" ) {
if ( !Object.prototype.hasOwnProperty.call(params, n) ) {
params[n] = [];
}
params[n].push(v);
}
}
return params;
}
It returns an object of arrays for parsed URLs with query strings and undefined
if a query string could not be identified.
I used this in an answer over at SO.
3 Answers 3
Is it bug free? No.
These two corner-cases have been missed:
- parameter values containing '=', i.e. 'example.com?foo==bar' (double equals) or '?foo=k=v'
- cannot handle parameters called 'toString' and 'valueOf' (amongst others.)
The first may well count as malformed URL, but Chrome handles it and pass through == unencoded in location.search
. To handle this, go back to basic indexOf
usage.
The second problem's just pedantic really. You could try and work around it using !params.hasOwnProperty(n)
instead of !(n in params)
, but you'll still get stuck if someone passes a parameter called hasOwnProperty
. The only way I see around this is to fall back to some dire array-based collection populated something like:
var keys = [], params = [];
for (...) {
var n = ..., v = ...;
var i = keys.indexOf(n);
if (i >= 0) {
if (!(params[i] instanceof Array)) {
params[i] = [params[i]];
}
params.push(v);
} else {
params[i] = v;
keys.push(n);
}
}
I guess you'd then have to resort to returning an array of arrays rather than an object. i.e. each element of the array returned would either be [key, value]
or [key, [values]]
, although client might find it easier to work with if you returned something like [key, value1, value2, ...]
(which caters nicely for properties without values.)
-
\$\begingroup\$ Wow, great answer. :-) I'll come up with an improved version. \$\endgroup\$Tomalak– Tomalak2011年06月13日 19:30:56 +00:00Commented Jun 13, 2011 at 19:30
-
\$\begingroup\$ See modified answer. I've decided not to go down the array-of-arrays route as this is highly impractical. \$\endgroup\$Tomalak– Tomalak2011年06月13日 20:18:01 +00:00Commented Jun 13, 2011 at 20:18
-
\$\begingroup\$ I like the way you handle and call hasOwnProperty - very nice. \$\endgroup\$searlea– searlea2011年06月14日 06:28:48 +00:00Commented Jun 14, 2011 at 6:28
-
\$\begingroup\$ Of course it would still be broken "downstream" - the object returned would have its
hasOwnProperty
overwritten. But the function would not break. Anything else that comes to mind? Efficiency-wise maybe? It seems a little bloated to me... \$\endgroup\$Tomalak– Tomalak2011年06月14日 06:42:08 +00:00Commented Jun 14, 2011 at 6:42 -
\$\begingroup\$ Try
Object.prototype.hasOwnProperty.call(params, n)
\$\endgroup\$orip– orip2012年01月16日 08:42:49 +00:00Commented Jan 16, 2012 at 8:42
you could do a null check on the url argument because the following will throw an exception.
parseURLParams(null);
-
\$\begingroup\$ Good point. Done. \$\endgroup\$Tomalak– Tomalak2011年06月15日 06:32:34 +00:00Commented Jun 15, 2011 at 6:32
Seems a tiny bit over-engineered. Something like this should work just as well, and addresses searlea's points in his answer:
function parseURLParams(url) {
var out = {};
(url.split('?')[1] || url).split('#')[0].split('&').forEach(function(p) {
var kv = p.match(/([^=]*)=?(.*)/),
k = decodeURIComponent(kv[1]),
v = decodeURIComponent(kv[2] || '');
hasOwnProperty.call(out, k) ? out[k].push(v) : out[k] = [v];
});
return out;
}
The regex match is only needed if you want to support equals signs in values, otherwise you can use split
and the indices 0 and 1.
The main (only?) difference is that pretty much any string will be treated as a viable query -- if there are no equals signs or ampersands, it's a query with just one key and no value.
-
1\$\begingroup\$ Yikes, just realized how old this question is =/ \$\endgroup\$Dagg– Dagg2013年04月25日 02:52:23 +00:00Commented Apr 25, 2013 at 2:52
-
\$\begingroup\$ Hm. That's a little too compressed for my tastes. I like less "clever" code. Oh and
forEach()
is not portable. \$\endgroup\$Tomalak– Tomalak2013年04月25日 03:01:59 +00:00Commented Apr 25, 2013 at 3:01 -
\$\begingroup\$ Fair enough. I felt the same way about
forEach
and ES5-only features for a long time, but I figure we're at the point by now where ES5 is "normal" and anything else is "legacy," and can be shimmed or whatever if legacy support is needed. \$\endgroup\$Dagg– Dagg2013年04月25日 03:46:10 +00:00Commented Apr 25, 2013 at 3:46 -
\$\begingroup\$ Right, adding
forEach()
to the array prototype if necessary is not very difficult, you just have to remember to do it. I suppose most people use some sort of JS library anyway. \$\endgroup\$Tomalak– Tomalak2013年04月25日 03:52:52 +00:00Commented Apr 25, 2013 at 3:52
[jquery]
tag. \$\endgroup\$+
on the entire string (which may be more efficient), and you've allowed for a string to be passed instead of using the current URL, but I can't spot any differences beyond those. I decided not to bloat my answer by supporting dupe parameters, as that practice is a rare one. However, I did link to a proof-of-concept example that would parse the URL in a similar style to how PHP would handle it. \$\endgroup\$window.location.search
and ignore duplicate params, your original version is fine, too. I was testing with full URLs; your key/value regex did not work with them. So, uhm... At least your approach could be more simplified (no nested functiond()
). :-P \$\endgroup\$