Abstract Value Operations
Before we can explore explicit vs implicit coercion, we need to learn the basic rules that govern how values become either a string
, number
, or boolean
. The ES5 spec in section 9 defines several “abstract operations” (fancy spec-speak for “internal-only operation”) with the rules of value conversion. We will specifically pay attention to: ToString
, ToNumber
, and ToBoolean
, and to a lesser extent, ToPrimitive
.
ToString
When any non-string
value is coerced to a string
representation, the conversion is handled by the ToString
abstract operation in section 9.8 of the specification.
Built-in primitive values have natural stringification: null
becomes "null"
, undefined
becomes "undefined"
and true
becomes "true"
. number
s are generally expressed in the natural way you’d expect, but as we discussed in Chapter 2, very small or very large numbers
are represented in exponent form:
// multiplying `1.07` by `1000`, seven times over
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// seven times three digits => 21 digits
a.toString(); // "1.07e21"
For regular objects, unless you specify your own, the default toString()
(located in Object.prototype.toString()
) will return the internal [[Class]]
(see Chapter 3), like for instance "[object Object]"
.
But as shown earlier, if an object has its own toString()
method on it, and you use that object in a string
-like way, its toString()
will automatically be called, and the string
result of that call will be used instead.
Note: The way an object is coerced to a string
technically goes through the ToPrimitive
abstract operation (ES5 spec, section 9.1), but those nuanced details are covered in more detail in the ToNumber
section later in this chapter, so we will skip over them here.
Arrays have an overridden default toString()
that stringifies as the (string) concatenation of all its values (each stringified themselves), with ","
in between each value:
var a = [1,2,3];
a.toString(); // "1,2,3"
Again, toString()
can either be called explicitly, or it will automatically be called if a non-string
is used in a string
context.
JSON Stringification
Another task that seems awfully related to ToString
is when you use the JSON.stringify(..)
utility to serialize a value to a JSON-compatible string
value.
It’s important to note that this stringification is not exactly the same thing as coercion. But since it’s related to the ToString
rules above, we’ll take a slight diversion to cover JSON stringification behaviors here.
For most simple values, JSON stringification behaves basically the same as toString()
conversions, except that the serialization result is always a string
:
JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (a string with a quoted string value in it)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"
Any JSON-safe value can be stringified by JSON.stringify(..)
. But what is JSON-safe? Any value that can be represented validly in a JSON representation.
It may be easier to consider values that are not JSON-safe. Some examples: undefined
s, function
s, (ES6+) symbol
s, and object
s with circular references (where property references in an object structure create a never-ending cycle through each other). These are all illegal values for a standard JSON structure, mostly because they aren’t portable to other languages that consume JSON values.
The JSON.stringify(..)
utility will automatically omit undefined
, function
, and symbol
values when it comes across them. If such a value is found in an array
, that value is replaced by null
(so that the array position information isn’t altered). If found as a property of an object
, that property will simply be excluded.
Consider:
JSON.stringify( undefined ); // undefined
JSON.stringify( function(){} ); // undefined
JSON.stringify( [1,undefined,function(){},4] ); // "[1,null,null,4]"
JSON.stringify( { a:2, b:function(){} } ); // "{"a":2}"
But if you try to JSON.stringify(..)
an object
with circular reference(s) in it, an error will be thrown.
JSON stringification has the special behavior that if an object
value has a toJSON()
method defined, this method will be called first to get a value to use for serialization.
If you intend to JSON stringify an object that may contain illegal JSON value(s), or if you just have values in the object
that aren’t appropriate for the serialization, you should define a toJSON()
method for it that returns a JSON-safe version of the object
.
For example:
var o = { };
var a = {
b: 42,
c: o,
d: function(){}
};
// create a circular reference inside `a`
o.e = a;
// would throw an error on the circular reference
// JSON.stringify( a );
// define a custom JSON value serialization
a.toJSON = function() {
// only include the `b` property for serialization
return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"
It’s a very common misconception that toJSON()
should return a JSON stringification representation. That’s probably incorrect, unless you’re wanting to actually stringify the string
itself (usually not!). toJSON()
should return the actual regular value (of whatever type) that’s appropriate, and JSON.stringify(..)
itself will handle the stringification.
In other words, toJSON()
should be interpreted as “to a JSON-safe value suitable for stringification,” not “to a JSON string” as many developers mistakenly assume.
Consider:
var a = {
val: [1,2,3],
// probably correct!
toJSON: function(){
return this.val.slice( 1 );
}
};
var b = {
val: [1,2,3],
// probably incorrect!
toJSON: function(){
return "[" +
this.val.slice( 1 ).join() +
"]";
}
};
JSON.stringify( a ); // "[2,3]"
JSON.stringify( b ); // ""[2,3]""
In the second call, we stringified the returned string
rather than the array
itself, which was probably not what we wanted to do.
While we’re talking about JSON.stringify(..)
, let’s discuss some lesser-known functionalities that can still be very useful.
An optional second argument can be passed to JSON.stringify(..)
that is called replacer. This argument can either be an array
or a function
. It’s used to customize the recursive serialization of an object
by providing a filtering mechanism for which properties should and should not be included, in a similar way to how toJSON()
can prepare a value for serialization.
If replacer is an array
, it should be an array
of string
s, each of which will specify a property name that is allowed to be included in the serialization of the object
. If a property exists that isn’t in this list, it will be skipped.
If replacer is a function
, it will be called once for the object
itself, and then once for each property in the object
, and each time is passed two arguments, key and value. To skip a key in the serialization, return undefined
. Otherwise, return the value provided.
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
JSON.stringify( a, function(k,v){
if (k !== "c") return v;
} );
// "{"b":42,"d":[1,2,3]}"
Note: In the function
replacer case, the key argument k
is undefined
for the first call (where the a
object itself is being passed in). The if
statement filters out the property named "c"
. Stringification is recursive, so the [1,2,3]
array has each of its values (1
, 2
, and 3
) passed as v
to replacer, with indexes (0
, 1
, and 2
) as k
.
A third optional argument can also be passed to JSON.stringify(..)
, called space, which is used as indentation for prettier human-friendly output. space can be a positive integer to indicate how many space characters should be used at each indentation level. Or, space can be a string
, in which case up to the first ten characters of its value will be used for each indentation level.
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, null, 3 );
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }"
JSON.stringify( a, null, "-----" );
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"
Remember, JSON.stringify(..)
is not directly a form of coercion. We covered it here, however, for two reasons that relate its behavior to ToString
coercion:
string
,number
,boolean
, andnull
values all stringify for JSON basically the same as how they coerce tostring
values via the rules of theToString
abstract operation.- If you pass an
object
value toJSON.stringify(..)
, and thatobject
has atoJSON()
method on it,toJSON()
is automatically called to (sort of) “coerce” the value to be JSON-safe before stringification.
ToNumber
If any non-number
value is used in a way that requires it to be a number
, such as a mathematical operation, the ES5 spec defines the ToNumber
abstract operation in section 9.3.
For example, true
becomes 1
and false
becomes 0
. undefined
becomes NaN
, but (curiously) null
becomes 0
.
ToNumber
for a string
value essentially works for the most part like the rules/syntax for numeric literals (see Chapter 3). If it fails, the result is NaN
(instead of a syntax error as with number
literals). One example difference is that 0
-prefixed octal numbers are not handled as octals (just as normal base-10 decimals) in this operation, though such octals are valid as number
literals (see Chapter 2).
Note: The differences between number
literal grammar and ToNumber
on a string
value are subtle and highly nuanced, and thus will not be covered further here. Consult section 9.3.1 of the ES5 spec for more information.
Objects (and arrays) will first be converted to their primitive value equivalent, and the resulting value (if a primitive but not already a number
) is coerced to a number
according to the ToNumber
rules just mentioned.
To convert to this primitive value equivalent, the ToPrimitive
abstract operation (ES5 spec, section 9.1) will consult the value (using the internal DefaultValue
operation — ES5 spec, section 8.12.8) in question to see if it has a valueOf()
method. If valueOf()
is available and it returns a primitive value, that value is used for the coercion. If not, but toString()
is available, it will provide the value for the coercion.
If neither operation can provide a primitive value, a TypeError
is thrown.
As of ES5, you can create such a noncoercible object — one without valueOf()
and toString()
— if it has a null
value for its [[Prototype]]
, typically created with Object.create(null)
. See the this & Object Prototypes title of this series for more information on [[Prototype]]
s.
Note: We cover how to coerce to number
s later in this chapter in detail, but for this next code snippet, just assume the Number(..)
function does so.
Consider:
var a = {
valueOf: function(){
return "42";
}
};
var b = {
toString: function(){
return "42";
}
};
var c = [4,2];
c.toString = function(){
return this.join( "" ); // "42"
};
Number( a ); // 42
Number( b ); // 42
Number( c ); // 42
Number( "" ); // 0
Number( [] ); // 0
Number( [ "abc" ] ); // NaN
ToBoolean
Next, let’s have a little chat about how boolean
s behave in JS. There’s lots of confusion and misconception floating out there around this topic, so pay close attention!
First and foremost, JS has actual keywords true
and false
, and they behave exactly as you’d expect of boolean
values. It’s a common misconception that the values 1
and 0
are identical to true
/false
. While that may be true in other languages, in JS the number
s are number
s and the boolean
s are boolean
s. You can coerce 1
to true
(and vice versa) or 0
to false
(and vice versa). But they’re not the same.
Falsy Values
But that’s not the end of the story. We need to discuss how values other than the two boolean
s behave whenever you coerce to their boolean
equivalent.
All of JavaScript’s values can be divided into two categories:
- values that will become
false
if coerced toboolean
- everything else (which will obviously become
true
)
I’m not just being facetious. The JS spec defines a specific, narrow list of values that will coerce to false
when coerced to a boolean
value.
How do we know what the list of values is? In the ES5 spec, section 9.2 defines a ToBoolean
abstract operation, which says exactly what happens for all the possible values when you try to coerce them “to boolean.”
From that table, we get the following as the so-called “falsy” values list:
undefined
null
false
+0
,-0
, andNaN
""
That’s it. If a value is on that list, it’s a “falsy” value, and it will coerce to false
if you force a boolean
coercion on it.
By logical conclusion, if a value is not on that list, it must be on another list, which we call the “truthy” values list. But JS doesn’t really define a “truthy” list per se. It gives some examples, such as saying explicitly that all objects are truthy, but mostly the spec just implies: anything not explicitly on the falsy list is therefore truthy.
Falsy Objects
Wait a minute, that section title even sounds contradictory. I literally just said the spec calls all objects truthy, right? There should be no such thing as a “falsy object.”
What could that possibly even mean?
You might be tempted to think it means an object wrapper (see Chapter 3) around a falsy value (such as ""
, 0
or false
). But don’t fall into that trap.
Note: That’s a subtle specification joke some of you may get.
Consider:
var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );
We know all three values here are objects (see Chapter 3) wrapped around obviously falsy values. But do these objects behave as true
or as false
? That’s easy to answer:
var d = Boolean( a && b && c );
d; // true
So, all three behave as true
, as that’s the only way d
could end up as true
.
Tip: Notice the Boolean( .. )
wrapped around the a && b && c
expression — you might wonder why that’s there. We’ll come back to that later in this chapter, so make a mental note of it. For a sneak-peek (trivia-wise), try for yourself what d
will be if you just do d = a && b && c
without the Boolean( .. )
call!
So, if “falsy objects” are not just objects wrapped around falsy values, what the heck are they?
The tricky part is that they can show up in your JS program, but they’re not actually part of JavaScript itself.
What!?
There are certain cases where browsers have created their own sort of exotic values behavior, namely this idea of “falsy objects,” on top of regular JS semantics.
A “falsy object” is a value that looks and acts like a normal object (properties, etc.), but when you coerce it to a boolean
, it coerces to a false
value.
Why!?
The most well-known case is document.all
: an array-like (object) provided to your JS program by the DOM (not the JS engine itself), which exposes elements in your page to your JS program. It used to behave like a normal object—it would act truthy. But not anymore.
document.all
itself was never really “standard” and has long since been deprecated/abandoned.
“Can’t they just remove it, then?” Sorry, nice try. Wish they could. But there’s far too many legacy JS code bases out there that rely on using it.
So, why make it act falsy? Because coercions of document.all
to boolean
(like in if
statements) were almost always used as a means of detecting old, nonstandard IE.
IE has long since come up to standards compliance, and in many cases is pushing the web forward as much or more than any other browser. But all that old if (document.all) { /* it's IE */ }
code is still out there, and much of it is probably never going away. All this legacy code is still assuming it’s running in decade-old IE, which just leads to bad browsing experience for IE users.
So, we can’t remove document.all
completely, but IE doesn’t want if (document.all) { .. }
code to work anymore, so that users in modern IE get new, standards-compliant code logic.
“What should we do?” **”I’ve got it! Let’s bastardize the JS type system and pretend that document.all
is falsy!”
Ugh. That sucks. It’s a crazy gotcha that most JS developers don’t understand. But the alternative (doing nothing about the above no-win problems) sucks just a little bit more.
So… that’s what we’ve got: crazy, nonstandard “falsy objects” added to JavaScript by the browsers. Yay!
Truthy Values
Back to the truthy list. What exactly are the truthy values? Remember: a value is truthy if it’s not on the falsy list.
Consider:
var a = "false";
var b = "0";
var c = "''";
var d = Boolean( a && b && c );
d;
What value do you expect d
to have here? It’s gotta be either true
or false
.
It’s true
. Why? Because despite the contents of those string
values looking like falsy values, the string
values themselves are all truthy, because ""
is the only string
value on the falsy list.
What about these?
var a = []; // empty array -- truthy or falsy?
var b = {}; // empty object -- truthy or falsy?
var c = function(){}; // empty function -- truthy or falsy?
var d = Boolean( a && b && c );
d;
Yep, you guessed it, d
is still true
here. Why? Same reason as before. Despite what it may seem like, []
, {}
, and function(){}
are not on the falsy list, and thus are truthy values.
In other words, the truthy list is infinitely long. It’s impossible to make such a list. You can only make a finite falsy list and consult it.
Take five minutes, write the falsy list on a post-it note for your computer monitor, or memorize it if you prefer. Either way, you’ll easily be able to construct a virtual truthy list whenever you need it by simply asking if it’s on the falsy list or not.
The importance of truthy and falsy is in understanding how a value will behave if you coerce it (either explicitly or implicitly) to a boolean
value. Now that you have those two lists in mind, we can dive into coercion examples themselves.