DEV Community

loading...
Cover image for Understand JavaScript Abstract Operations in 7 Minutes.

Understand JavaScript Abstract Operations in 7 Minutes.

Ifeoma Imoh
Updated on ・7 min read

To help you understand better, let’s quickly revisit types in JavaScript. Variables in JavaScript do not have types; values do. JavaScript has eight basic value data types, and they are divided into two categories, primitive and non-primitive.

Primitive Types:

  • Undefined
  • Null
  • Boolean
  • String
  • Symbol
  • Number
  • BigInt

Non-Primitive Types (Reference types):

  • Object
  • Array
  • Function

    Arrays and functions are subtypes of the object type.

One major difference between primitive and non-primitive values is that primitive values are immutable after creation while non-primitive values are mutable.

Let’s take an example:

// Let's alter the value of the string assigned to the variable person.
let person = "ifeoma"
// Here it simply made a copy and then altered the copied value.
console.log(person.toUpperCase()) // IFEOMA
// It didn't change the original value.
console.log(person) //  ifeoma
Enter fullscreen mode Exit fullscreen mode

From the example above, when we tried to alter the value, it only made a copy of the variable person and changed it, but it didn’t change the already existing string value because it is a primitive.

On the other hand, the variable assigned to a primitive value can be changed. Therefore, it can be reassigned so that it points to a new value but the existing value it holds cannot be changed.

let person = "ifeoma"
person = "sylvia"
console.log(person)  // sylvia
Enter fullscreen mode Exit fullscreen mode

In the example above, we assigned a new string value to the variable person so that it no longer points to the initial string value ifeoma .

Let’s try to mutate a non-primitive:

let array = [ "Jay Pritchet", "Phil Dunphy" ]
let array2 = array
array.push("Claire Pritchet")
console.log(array2) // [ "Jay Pritchet", "Phil Dunphy", "Claire Pritchet" ]
Enter fullscreen mode Exit fullscreen mode

In the above example, we made array a reference to array2 . Emphasis on reference, which means that even after we modified the data in array by adding a new item to it, logging array2 shows the new item added to array .
This is because the variable array2 is referencing the address of the variable array.
This behavior is expected from all non-primitive value types.

Another difference between primitives and non-primitives is that primitives are stored by value while non-primitives are stored by reference.

The typeOf operator is a built-in utility used to check the type of value assigned to a javascript variable.

The typeOf operator always returns a string type.

Let’s take a look at how values are changed from one type to another.

Coercion

Coercion refers to the process of converting values from one type to another(such as string to number ).
Using inbuilt functions(Number(), String() etc.) you can be obvious about your intention to convert a value from one type to another(explicit coercion) or let Javascript automatically handle the conversion for you(Implicit coercion).

Coercion always results in either strings , numbers , or booleans . Understanding coercion will help you avoid problems that can occur in your code.
Let's see some examples.

Implicit Coercion

5 - "1" // 4  JavaScript coerced the string 1 to a number.
10 * false // 0  Javascript coerced the boolean false to 0.
10 + true // 11  The boolean true is coerced to a number 1.
Enter fullscreen mode Exit fullscreen mode

Explicit Coercion

Number('5') - Number('2') // 3 Here we are explicitly converting both strings to numbers first using the Number() method.
Enter fullscreen mode Exit fullscreen mode

To fully understand how coercion happens, we need to talk about Abstract Operations.

Abstract Operations are the fundamental building block that makes up how we deal with type conversion. - Kyle Simpson

Abstract Operations

According to the ECMAScript specification, abstract operations are not part of the language specification, but they are responsible for performing type conversion in Javascript. Whenever coercion (implicit or explicit) occurs, one or more internal operations, known as abstract operations, are performed.

We will look at these four primary abstract operations, but you can find the rest of them in the spec.

  1. ToPrimitive()
  2. ToString()
  3. ToNumber()
  4. ToBoolean()

ToPrimitive()

When a non-primitive or reference value is used in the context where a primitive is required, the JavaScript engine calls the ToPrimitive() abstract operation.

When converting non-primitive types to primitives, the abstract operation ToPrimitive() is invoked to handle the operation.

Let's see how non-primitive values are converted to primitives.

The spec informs us that the ToPrimitive() abstract operation takes two arguments.

  1. An input
  2. An optional PreferredType hint

The PreferredType hint can be either one of these - string , number , default .

If you are performing a numeric operation and the ToPrimitive() operation is invoked, number will be sent as the hint. If you are working with strings, it will send string as the hint.
When ToPrimitive() is called with no hint present, it'll send default as a hint, and this will behave as if the hint were number (unless it's a Date which defaults to string ).

If the argument is already a primitive value, then it will be returned without conversion. Let's take a look at how the ToPrimitive algorithm works.

There are two methods available on every object type used to convert them from non-primitives to primitives:

  1. valueOf() — This is to retrieve the primitive value associated with an object.
  2. toString()

Number Algorithm
If the hint is number , it calls the valueOf() function first, and if the returned value is primitive, it'll use it. If the object has no primitive value, valueOf() returns the object back then the toString() function gets called. Its value will be used if it is primitive; otherwise, it would result in a type error.

String Algorithm
If the hint is string , the order is reversed compared to the number algorithm. It calls the non-primitive toString() function first, and if it gets a string representation, it'll just use it; otherwise, it'll try the valueOf() method to see if the object has any primitive value.

Default Algorithm
If no hint is sent, it sets the default hint to number , or it is set to string if it is a Date .

The algorithms within JavaScript are inherently recursive. This means that if the ToPrimitive() operation gets invoked and the return result is not a primitive, it will keep getting invoked until it returns a primitive or an error.

ToString()

This abstract operation takes any value and converts it to a representation of the value in string form.

Argument Result
Null “null”
Undefined “undefined
true “true”
false “false”
“Hi” “Hi”
Symbol Throw a TypeError exception.

As seen above, built-in primitives have natural stringification, but if the ToString() operation is called on an object type, it will invoke the ToPrimitive() operation, and pass string as the hint.

As explained earlier, this will call the toString() first, and if it gets a string representation, it'll just use it; otherwise, it'll try the valueOf() method.

By default, regular JavaScript objects have their built-in toString() method (located in Object.prototype.toString()) that is called when an object is used in a manner in which a string is expected and this will return their internal [[Class]] property (e.g [object Object]).

Unless you specify your toString() method, if you use an object in a string-like way, the toString() method on its object prototype will be called. This will return a string with the [object Type] format where Type is the object type.

Let’s see an example:

const obj = {}
obj.toString() // [object Object]
Enter fullscreen mode Exit fullscreen mode

Although arrays are subtypes of the object type, the array object has a built-in toString() method that overrides the default Object.toString() method and returns a string representation containing each array element separated by a comma. This toString() method lives on the Array’s prototype as Array.prototype.toString().

Here is an example:

// Calling toString() explicitly on an array
let array = []
array.toString() // ""  It returns an empty string.

let array1 = [1, 2, 3]
array1.toString() // "1,2,3"  It returns a string containing each element in the array seperated by a comma.
Enter fullscreen mode Exit fullscreen mode

In a situation where you want to get the object class, you need to skip the default override behavior of Array.prototype.toString in favor of Object.prototype.toString() .

You have to pass the array in the call() method to change the context from Array to Object.

console.log(Object.prototype.toString.call([1, 2, 3])) // [object Array] 
Enter fullscreen mode Exit fullscreen mode

ToNumber()

Whenever we perform a numeric operation, and one or both operands aren't numbers, the ToNumber() abstract operation will be invoked to convert it to a value of type number .
Let's see some examples:

Argument Result
undefined NaN
null 0
true 1
false 0
“” 0
“.” NaN
“1” 1
BigInt Throw a type error exception.
symbol Throw a type error exception.
object 1. ToPrimitive(argument, number).
2. Return ? ToNumber(primValue).

As seen from the table above, when ToNumber() is called on a non-primitive (any of the object types) value, it is first converted to its primitive equivalent by invoking ToPrimitive() abstract operation and passing number as the PreferredType hint.
The return value from the ToPrimitive() operation will then be coerced into a number by the ToNumber() abstract operation. If it still doesn't result in a primitive value, it throws an error.

Let's take an array with an empty string as an example.

ToPrimitive( [""], number) // It first calls the ToPrimitive abstract operation on it and pass number as the hint.

[""].valueOf() // [""]  Because  the hint is number, it calls valueOf first and this basically returns itself. So we still have the array with an empty string which isn’t a primitive.

[""].toString() // ""   It then calls the toString() function next and this will end up producing an empty string "".

ToNumber("") // 0   Next it calls ToNumber() and passes the empty string "" as an argument. As seen from the table above, that would result to 0.
Enter fullscreen mode Exit fullscreen mode

ToBoolean()

The abstract operation ToBoolean() is called to convert an argument to a Boolean type whenever we use a value that is not Boolean in a place that needs a Boolean . The ToBoolean() abstract operation does not invoke the ToPrimitive() or any of the other abstract operations.
It just checks to see if the value is either falsy or not. There is a lookup table in the spec that defines a list of values that will return false when coerced to a boolean . They are called falsy values.

These are the falsy values:

Argument Type Result
undefined false
null false
false false
NaN false
0 false
-0 false
“” false

Values that are not on the list of falsy values are truthy values.

Conclusion

Languages that take the first position about their rules are referred to as "strongly typed" because they are strict about not allowing you to break the rules.
Since JavaScript is not one of them, it is referred to as weakly or loosely typed because it gives room for a lot of flexibility in terms of implicit coercion, and you do not have to specify the type of your variables explicitly.

Like any other language, Javascript has its rules, and the rules that govern the type system of a language exist to help us. It is up to us to learn them to avoid unnecessary mistakes.

Thank you! I hope you enjoyed reading as much as I enjoyed writing ❤️.

Discussion (0)