Here's an updated version of the URLSearchParams answer from @oriadam / @error, with support for multi-level nesting:
const urlString = (data) => {
if (data == null) { return ""; }
const urlParams = new URLSearchParams();
const rbracket = /\[\]$/;
const add = (name, valueOrFunction) => {
const value = typeof valueOrFunction === "function" ? valueOrFunction() : valueOrFunction;
urlParams.append(name, value == null ? "" : value);
};
const buildParams = (prefix, obj) => {
if (Array.isArray(obj)) {
obj.forEach((value, index) => {
if (rbracket.test(prefix)) {
add(prefix, value);
} else {
const i = typeof value === "object" && value != null ? index : "";
buildParams(`${prefix}[${i}]`, value);
}
});
} else if (typeof obj === "object" && obj != null) {
for (const [name, value] of Object.entries(obj)) {
buildParams(`${prefix}[${name}]`, value);
}
} else {
add(prefix, obj);
}
};
if (Array.isArray(data) || data instanceof NodeList) {
// If an array or NodeList was passed in,
// assume that it is a collection of form elements:
data.forEach(el => add(el.name, el.value));
} else {
for (const [name, value] of Object.entries(data)) {
buildParams(name, value);
}
}
return urlParams.toString();
};
This is based on the jQuery 3.5.1 source, so it should produce identical outputs to $.param for the same inputs. The only difference is that spaces will be encoded as + instead of %20.
I've provided a Fiddle using the jQuery test cases, all of which pass.
Edit: If you need to support an environment without support for the URLSearchParams class, you'll need to use a combination of an array and the encodeURIComponent method, as shown in the other answers:
const urlString = (data) => {
if (data == null) { return ""; }
const urlParams = [];
const rbracket = /\[\]$/;
const add = (name, valueOrFunction) => {
let value = typeof valueOrFunction === "function" ? valueOrFunction() : valueOrFunction;
if (value == null) { value = ""; }
urlParams.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
};
const buildParams = (prefix, obj) => {
if (Array.isArray(obj)) {
obj.forEach((value, index) => {
if (rbracket.test(prefix)) {
add(prefix, value);
} else {
const i = typeof value === "object" && value != null ? index : "";
buildParams(`${prefix}[${i}]`, value);
}
});
} else if (typeof obj === "object" && obj != null) {
for (const [name, value] of Object.entries(obj)) {
buildParams(`${prefix}[${name}]`, value);
}
} else {
add(prefix, obj);
}
};
if (Array.isArray(data)) {
// If an array was passed in,
// assume that it is a collection of form elements:
data.forEach(el => add(el.name, el.value));
} else {
for (const [name, value] of Object.entries(data)) {
buildParams(name, value);
}
}
return urlParams.join("&");
};
Updated test Fiddle