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". numbers 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:

  1. // multiplying `1.07` by `1000`, seven times over
  2. var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
  3. // seven times three digits => 21 digits
  4. 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:

  1. var a = [1,2,3];
  2. 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:

  1. JSON.stringify( 42 ); // "42"
  2. JSON.stringify( "42" ); // ""42"" (a string with a quoted string value in it)
  3. JSON.stringify( null ); // "null"
  4. 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: undefineds, functions, (ES6+) symbols, and objects 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:

  1. JSON.stringify( undefined ); // undefined
  2. JSON.stringify( function(){} ); // undefined
  3. JSON.stringify( [1,undefined,function(){},4] ); // "[1,null,null,4]"
  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:

  1. var o = { };
  2. var a = {
  3. b: 42,
  4. c: o,
  5. d: function(){}
  6. };
  7. // create a circular reference inside `a`
  8. o.e = a;
  9. // would throw an error on the circular reference
  10. // JSON.stringify( a );
  11. // define a custom JSON value serialization
  12. a.toJSON = function() {
  13. // only include the `b` property for serialization
  14. return { b: this.b };
  15. };
  16. 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:

  1. var a = {
  2. val: [1,2,3],
  3. // probably correct!
  4. toJSON: function(){
  5. return this.val.slice( 1 );
  6. }
  7. };
  8. var b = {
  9. val: [1,2,3],
  10. // probably incorrect!
  11. toJSON: function(){
  12. return "[" +
  13. this.val.slice( 1 ).join() +
  14. "]";
  15. }
  16. };
  17. JSON.stringify( a ); // "[2,3]"
  18. 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 strings, 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.

  1. var a = {
  2. b: 42,
  3. c: "42",
  4. d: [1,2,3]
  5. };
  6. JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
  7. JSON.stringify( a, function(k,v){
  8. if (k !== "c") return v;
  9. } );
  10. // "{"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.

  1. var a = {
  2. b: 42,
  3. c: "42",
  4. d: [1,2,3]
  5. };
  6. JSON.stringify( a, null, 3 );
  7. // "{
  8. // "b": 42,
  9. // "c": "42",
  10. // "d": [
  11. // 1,
  12. // 2,
  13. // 3
  14. // ]
  15. // }"
  16. JSON.stringify( a, null, "-----" );
  17. // "{
  18. // -----"b": 42,
  19. // -----"c": "42",
  20. // -----"d": [
  21. // ----------1,
  22. // ----------2,
  23. // ----------3
  24. // -----]
  25. // }"

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:

  1. string, number, boolean, and null values all stringify for JSON basically the same as how they coerce to string values via the rules of the ToString abstract operation.
  2. If you pass an object value to JSON.stringify(..), and that object has a toJSON() 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 numbers later in this chapter in detail, but for this next code snippet, just assume the Number(..) function does so.

Consider:

  1. var a = {
  2. valueOf: function(){
  3. return "42";
  4. }
  5. };
  6. var b = {
  7. toString: function(){
  8. return "42";
  9. }
  10. };
  11. var c = [4,2];
  12. c.toString = function(){
  13. return this.join( "" ); // "42"
  14. };
  15. Number( a ); // 42
  16. Number( b ); // 42
  17. Number( c ); // 42
  18. Number( "" ); // 0
  19. Number( [] ); // 0
  20. Number( [ "abc" ] ); // NaN

ToBoolean

Next, let’s have a little chat about how booleans 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 numbers are numbers and the booleans are booleans. 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 booleans behave whenever you coerce to their boolean equivalent.

All of JavaScript’s values can be divided into two categories:

  1. values that will become false if coerced to boolean
  2. 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, and NaN
  • ""

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:

  1. var a = new Boolean( false );
  2. var b = new Number( 0 );
  3. 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:

  1. var d = Boolean( a && b && c );
  2. 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:

  1. var a = "false";
  2. var b = "0";
  3. var c = "''";
  4. var d = Boolean( a && b && c );
  5. 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?

  1. var a = []; // empty array -- truthy or falsy?
  2. var b = {}; // empty object -- truthy or falsy?
  3. var c = function(){}; // empty function -- truthy or falsy?
  4. var d = Boolean( a && b && c );
  5. 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.