Quantcast
Channel: User Ben Aston - Stack Overflow
Viewing all articles
Browse latest Browse all 56

Answer by Ben Aston for valueOf() vs. toString() in Javascript

$
0
0

TLDR

Type coercion, or implicit type conversion, enables weak typing and is used throughout JavaScript. Most operators (with the notable exception of the strict equality operators === and !==), and value checking operations (eg. if(value)...), will coerce values supplied to them, if the types of those values are not immediately compatible with the operation.

The precise mechanism used to coerce a value depends on the expression being evaluated. In the question, the addition operator is being used.

The addition operator will first ensure both operands are primitives, which, in this case, involves calling the valueOf method. The toString method is not called in this instance because the overridden valueOf method on object x returns a primitive value.

Then, because one of the operands in the question is a string, both operands are converted to strings. This process uses the abstract, internal operation ToString (note: capitalized), and is distinct from the toString method on the object (or its prototype chain).

Finally, the resulting strings are concatenated.

Details

On the prototype of every constructor function object corresponding to every language type in JavaScript (ie. Number, BigInt, String, Boolean, Symbol, and Object), there are two methods: valueOf and toString.

The purpose of valueOf is to retrieve the primitive value associated with an object (if it has one). If an object does not have an underlying primitive value, then the object is simply returned.

If valueOf is invoked against a primitive, then the primitive is auto-boxed in the normal way, and the underlying primitive value returned. Note that for strings, the underlying primitive value (ie. the value returned by valueOf) is the string representation itself.

The following code shows that the valueOf method returns the underlying primitive value from a wrapper object, and it shows how unmodified object instances that do not correspond to primitives, have no primitive value to return, so they simply return themselves.

console.log(typeof new Boolean(true)) // 'object'console.log(typeof new Boolean(true).valueOf()) // 'boolean'console.log(({}).valueOf()) // {} (no primitive value to return)

The purpose of toString, on the other hand, is return a string representation of an object.

For example:

console.log({}.toString()) // '[object Object]'console.log(new Number(1).toString()) // '1'

For most operations, JavaScript will silently attempt to convert one or more operand(s) to the required type. This behavior was chosen to make JavaScript easier to use. JavaScript initially did not have exceptions, and this may have also played a role in this design decision. This kind of implicit type conversion is called type coercion, and it is the basis of JavaScript's loose (weak) type system. The complicated rules behind this behavior are intended to move the complexity of typecasting into the language itself, and out of your code.

During the coercive process, there are two modes of conversion that can occur:

  1. Conversion of an object to a primitive (which might involve a type conversion itself), and
  2. Direct conversion to a specific type instance, using a constructor function object of one of the primitive types (ie. Number(), Boolean(), String() etc.)

Conversion To A Primitive

When attempting to convert non-primitive types to primitives to be operated upon, the abstract operation ToPrimitive is called with an optional "hint" of 'number', or 'string'. If the hint is omitted, the default hint is 'number' (unless the @@toPrimitive method has been overridden). If the hint is 'string', then toString is tried first, and valueOf second if toString did not return a primitive. Else, vice-versa. The hint depends on the operation requesting the conversion.

The addition operator supplies no hint, so valueOf is tried first. The subtraction operator supplies a hint of 'number', so valueOf is tried first. The only situations I can find in the spec in which the hint is 'string' are:

  1. Object#toString
  2. The abstract operation ToPropertyKey, which converts an argument into a value that may be used as a property key

Direct Type Conversion

Each operator has its own rules for completing their operation. The addition operator will first use ToPrimitive to ensure each operand is a primitive; then, if either operand is a string, it will then deliberately invoke the abstract operation ToString on each operand, to deliver the string concatenation behavior we expect with strings. If, after the ToPrimitive step, both operands are not strings, then arithmetic addition is performed.

Unlike addition, the subtraction operator does not have overloaded behavior, and so will invoke toNumeric on each operand having first converted them to primitives using ToPrimitive.

So:

 1  +  1   //  2                 '1'+  1   // '11'   Both already primitives, RHS converted to string, '1'+'1',   '11' 1  + [2]  // '12'   [2].valueOf() returns an object, so `toString` fallback is used, 1 + String([2]), '1'+'2', 12 1  + {}   // '1[object Object]'    {}.valueOf() is not a primitive, so toString fallback used, String(1) + String({}), '1'+'[object Object]', '1[object Object]' 2  - {}   // NaN    {}.valueOf() is not a primitive, so toString fallback used => 2 - Number('[object Object]'), NaN+'a'       // NaN    `ToPrimitive` passed 'number' hint), Number('a'), NaN+''        // 0      `ToPrimitive` passed 'number' hint), Number(''), 0+'-1'      // -1     `ToPrimitive` passed 'number' hint), Number('-1'), -1+{}        // NaN    `ToPrimitive` passed 'number' hint', `valueOf` returns an object, so falls back to `toString`, Number('[Object object]'), NaN 1 +'a'   // '1a'    Both are primitives, one is a string, String(1) +'a' 1 + {}    // '1[object Object]'    One primitive, one object, `ToPrimitive` passed no hint, meaning conversion to string will occur, one of the operands is now a string, String(1) + String({}), `1[object Object]`[] + []    // ''     Two objects, `ToPrimitive` passed no hint, String([]) + String([]), '' (empty string) 1 - 'a'   // NaN    Both are primitives, one is a string, `ToPrimitive` passed 'number' hint, 1-Number('a'), 1-NaN, NaN 1 - {}    // NaN    One primitive, one is an object, `ToPrimitive` passed 'number' hint, `valueOf` returns object, so falls back to `toString`, 1-Number([object Object]), 1-NaN, NaN[] - []    // 0      Two objects, `ToPrimitive` passed 'number' hint => `valueOf` returns array instance, so falls back to `toString`, Number('')-Number(''), 0-0, 0

Note that the Date intrinsic object is unique, in that it is the only intrinsic to override the default @@toPrimitive method, in which the default hint is presumed to be 'string' (rather than 'number'). The reason for having this, is to have Date instances translate to readable strings by default, instead of their numeric value, for the convenience of the programmer. You can override @@toPrimitive in your own objects using Symbol.toPrimitive.

The following grid shows the coercion results for the abstract equality operator (==) (source):

enter image description here

Addendum

Note that JavaScript coercion rules are designed to maintain internal consistency of behavior, and are not designed to meet your intuition (although they usually do).

So there are counterintuitive edge cases. For example, the following looks counterintuitive and inconsistent, but it is an artefact of maintaining consistent internal behavior 👇

!![] // true because Boolean([]) => true, [[Negation]](true) => false => [[Negation]](false) => true[] == false // true because [].valueOf() => [], which is not primitive, fallback to [].toString() => '', Boolean('') => false, false === false => true

See also and here in You Don't Know JS.


Viewing all articles
Browse latest Browse all 56

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>