Years ago I watched a YouTube video about the bad parts of JavaScript and the author listed a few examples. One of those impressed me:
var numbers = ['1', '2', '3'];
numbers.map(parseInt);
> [1, NaN, NaN]
var numbers = ['1', '2', '3'];
numbers.map(parseInt);
> [1, NaN, NaN]
Problem
In the first place the example showed how buggy was JavaScript. The Array.prototype.map
Array.prototype.map
method calls the provided parseInt
parseInt
function once for each element in the array and converts strings into integers as a result. The expectation therefore was that each string would be parsed into an integer but the actual result failed and made the last two elements NaN
NaN
.
So why does parseInt
parseInt
regard the last two elements as Not A Number s ? Thanks to the hints from my colleague, let’s have a closer look at the code.
Explanation
What arguments does the Array.prototype.map
Array.prototype.map
pass into the callback
callback
function? Three. One is the current element being processed, another is the index of the current element in the array and the other is the entire array which map
map
was called upon. It’s those three arguments that the parseInt
parseInt
method accepts (Perhaps now you know what happened under the hook :-)
Every step how parseInt
parseInt
is executed is illustrated as below:
parseInt('1', 0, numbers); // S#1
parseInt('2', 1, numbers); // S#2
parseInt('3', 2, numbers); // S#3
parseInt('1', 0, numbers); // S#1
parseInt('2', 1, numbers); // S#2
parseInt('3', 2, numbers); // S#3
The third argument numbers
numbers
actually in every step is ignored and won’t affect the result because parseInt
parseInt
only defines two parameters: string and radix .
Step One
the string is 1
1
and the radix is 0
0
. As MDN describes, when the radix is 0 or undefined the method falls back to the default behavior and treats the input string as decimal unless the input string starts with a zero (octal) or 0x
0x
(hexadecimal). So parseInt('1', 0)
parseInt('1', 0)
is equal to parseInt('1', 10)
parseInt('1', 10)
and the result is a decimal 1
1
as expected.
If the input string begins with “0x” or “0X”, radix is 16 (hexadecimal) and the remainder of the string is parsed.If the input string begins with “0”, radix is eight (octal) or 10 (decimal). Exactly which radix is chosen is implementation-dependent. ECMAScript 5 specifies that 10 (decimal) is used, but not all browsers support this yet. For this reason always specify a radix when using parseInt.If the input string begins with any other value, the radix is 10 (decimal). MDN parseInt()
Step Two
in this situation the radix is 1 and obviously it is not reasonable so the result is Not A Number
Not A Number
as expected.
Here’s how V8 deals with the 1
1
radix:
function GlobalParseInt(string, radix) {
if (IS_UNDEFINED(radix) || radix === 10 || radix === 0) {
...
} else {
...
if (!(radix == 0 || (2 <= radix && radix <= 36))) {
return NaN;
}
}
}
function GlobalParseInt(string, radix) {
if (IS_UNDEFINED(radix) || radix === 10 || radix === 0) {
...
} else {
...
if (!(radix == 0 || (2 <= radix && radix <= 36))) {
return NaN;
}
}
}
Step Three
in this situation the radix 2
2
is reasonable but the input string 3
3
is not. Binary numbers use only two different symbols: 0 and 1. So the result is Not A Number
Not A Number
as expected.
Conclusion
The unexpected behavior of the code shown above is not caused by JavaScript but by the programmers. There’s a famous saying by an elder in China, “The youth ought to level up their knowledge” :-)
Solutions
Lodash
What if parseInt
parseInt
only accepts one argument and ignores others? Then the problem described above won’t bother us any more.
lodash
lodash
implements the unary
unary
method which creates a function that accepts up to one argument. So the problem can be resolved in a lodash-ized way:
numbers.map(_.unary(parseInt));
// OR
_.map(numbers, _.unary(parseInt));
numbers.map(_.unary(parseInt));
// OR
_.map(numbers, _.unary(parseInt));
Vanilla JS
What if we use only Vanilla JavaScript? Then we can implement our own unary
unary
function. This is my version:
function unary(fn) {
function wrapper() {
return fn(arguments[0]);
}
return wrapper;
}
numbers.map(unary(parseInt));
function unary(fn) {
function wrapper() {
return fn(arguments[0]);
}
return wrapper;
}
numbers.map(unary(parseInt));
Later I found that lodash implements another function ary
ary
to slice the number of arguments which the target function can be invoked with and lodash#unary
lodash#unary
calls it under the hook. So I refactor the original version with an ary
ary
-like function:
function ary (fn, n) {
const argsLength = n ? n : fn.length
function wrapper () {
return fn.apply(this, Array.from(arguments).slice(0, argsLength)
}
return wrapper
}
function unary (fn) {
return ary(fn, 1)
}
numbers.map(unary(parseInt))
function ary (fn, n) {
const argsLength = n ? n : fn.length
function wrapper () {
return fn.apply(this, Array.from(arguments).slice(0, argsLength)
}
return wrapper
}
function unary (fn) {
return ary(fn, 1)
}
numbers.map(unary(parseInt))
Those solutions are a bit complicated and we can fix the problem in a much easier way:
numbers.map((str) => parseInt(str, 10));
numbers.map((str) => parseInt(str, 10));
and done!