Better Programming

Advice for programmers.

Follow publication

5 Basic Programming Concepts for JavaScript Developers

George Anton
Better Programming
Published in
17 min readDec 17, 2022

--

Photo by Nadine Shaabana on Unsplash

JavaScript is a weakly-typed programming language, which means it does not enforce data types for variables. Where strongly-typed languages will throw errors at compile time when certain variables are cast into different data types, JavaScript will generally not and will even sometimes perform type casting in the background which can lead to bugs and unforeseen problems. TypeScript addresses this issue but for those developing in JavaScript, this article is a good place to understand how to avoid the pitfalls of a weakly-typed language.

We will cover five concepts: Operator Precedence, Associativity, Short-Circuiting, Primitives, and (Implicit) Type Casting, in JavaScript.

Many of these concepts are language agnostic, the only change would be the rules may differ with each programming language. Whether you are a seasoned developer or just starting out, you are sure to learn something new!

Operators, Precedence, and Associativity

Operators are reserved symbols in programming languages that are used to perform mathematical calculations, assignments, comparisons, and logical operations on variables and values (called operands). There are:

  • un-ary operators — acts on one operand (i.e. ! negation operator)
  • bi-nary operators — acts on two operands ( i.e. % modulus operator)
  • tri-nary operators — acts on three operands (i.e. ? ternary operator)
  • n-ary operators — acts on n operands (i.e. () function call operator)

The vast majority of operators are either binary or unary (and sometimes, a symbol can be both depending on the context).

Order of Operator Execution

Precedence refers to the order in which operators will execute. The operator with the larger precedence value is executed before the ones with lower precedence values. Without precedence, expressions such as 3 + 4 * 5 can yield unexpected results and confusion.

Below is a snippet from Mozilla’s JS Operator Precedence table.

Columns (left to right): Precedence, Operator — Source [2]

Given the expression 3 + 4 * 5, since the multiplication operator has a higher precedence than the addition operator then multiplication is performed first with 3 then added to that result, giving a final result of 23.

So essentially 3+4*5 becomes 3 + (4*5)

Given that ** has precedence 13, * has precedence 12 and + has precedence 11, can you solve for 3 + 4 ** 2 * 5 ?

NodeJS precedence example

What if precedence is the same?

Associativity determines the order in which operations are performed. It’s most useful to know this in cases where multiple operations with the same precedence are used within an expression. In an earlier image, we saw that the operators + and - both have the same precedence.

Let’s consider the expression 1 + 2 - 3 + 4:

If the * and -operators have left-to-right associativity, then the expression evaluates as (1 + 2) -3) + 4 which evaluates to 0 + 4, or 4.

If the + and - operators have right-to-left associativity, then the expression evaluates as 1 + (2 -(3 + 4), which evaluates to 1 - 5, or -4.

Or consider the expression 3 ** 3 ** 3, where ** is exponentiation:

If ** has left-to-right associativity, then we have (3**3)**3 which is 19683.

But with right-to-left associativity, we have3**(3**3) giving 7625597484987.

Therefore associativity plays a crucial role in determining what results to expect!

Columns (left to right): Precedence, Operator, Associativity, Operator Expression— Source [2]

In JavaScript, while most arithmetic operators have left-associativity, the exponentiation operator is right-associative

Referring to the above table, we can now check our prior results in Node

NodeJS associativity example

The assignment operator is another operator that is right-associative

Hence a=b=c translates to a=(b=c) and a=b+=c translates to a=(b+=c)

NodeJS Assignment Operator Precedence/Associativity — Source [2]

While certain rules might be similar and hold true for most programming languages, it doesn’t imply that all programming languages share the same rules.

In general, both precedence and associativity of operators are determined by the rules of the programming language and since different languages can have different rules it is important to read each language’s documentation in regard to how their operators work.

For situations where operators are non-associative (n/a), it is recommended to use parenthesis to group expressions.

Note: I‘ve referenced the MDN docs here as the ECMAScript specification guide didn’t have an operator precedence/associativity table

Although expressions with higher precedence are evaluated first, there are scenarios where this doesn’t hold true. With short circuiting, an operand may not be evaluated at all!

Short-Circuiting (and Logical Operators)

Some logical operators have a feature called short-circuiting, where the interpreter stops evaluating an expression given the resulting value of an operand.

As an example, given x && (y || z) if x is falsy (evaluates to false) then (y || z) never gets evaluated, even though () has higher precedence than &&.

Short circuiting is generally used when certain conditions in an expression are dependent on others and so it would not always be necessary to evaluate an entire expression.

JavaScript Logical Operators — MDN

JS has three binary logical operators, all with left-to-right associativity

let user = { name : "George" }

const getUserName = (userObj) => {
if (user) {
if (user.name) {
console.log(user.name);
} else {
console.log("User object has no attribute called name");
}
} else {
console.log("User object does not exist");
}
};

const getUserNameWithShortCircuit = (userObj) => {
if (user && user.name) {
console.log(user.name);
} else {
console.log(`${user ? "User object has no attribute called name" : "User object does not exist"}`);
}
}

getUserName(user); // George
getUserNameWithShortCircuit(user); // George

user = {};

getUserName(user); // User object has no attribute called name
getUserNameWithShortCircuit(user); // User object has no attribute called name

user = null;

getUserName(user); // User object does not exist
getUserNameWithShortCircuit(user); // User object does not exist

In the above code snippet, the getUserName function has nested if conditions to check whether user is defined and if so, whether its name attribute exists. If they both exist, we print out the name attribute.

The getUserNameWithShortCircuit function uses the logical && (AND) operator and clumps both conditions in one expression. The difference being, if the first operand evaluates to false then the second expression is not evaluated and the interpreter jumps to the else block. This helps condense logic and also group together dependencies that conditions/operands have with one another.

Another use case for short circuits is to provide default values

let userName = someNameVar || "default"

If someNameVar is truthy (evaluates to true) then the OR operator doesn’t evaluate the right operand and assigns the someNameVarvalue to userName. Else, if someNameVar is falsy then we set userName to the string default.

Note: if the left operand is falsy, || returns the second operand regardless of whether it’s true or false

let userName = "" || false; //returns false

The ?? nullish coalescing operator is similar to the logical || OR, in that if the left operand is truthy they both short circuit. The difference is that ?? evaluates the right operand only if the left operand is either null or undefined.


console.log("" ?? false); // ""
console.log("" || false); // false

console.log(0 ?? false); // 0
console.log(0 || false); // false

console.log(null ?? false); // false
console.log(null || false); // false

console.log(undefined ?? false); // false
console.log(undefined || false); // false

console.log(true ?? false); // true
console.log(true || false); // true

Hence with || , any falsy value evaluates to the right operand.

Below is the documentation regarding these operators:

Binary Logical Operators — ECMAScript Guide

Short-Circuiting (More)

The assignment counterparts of the above operators (&&=, ||=, ??=) short circuit as well, such that assignment does not happen at all.

The ?. optional chaining operator also short-circuits if the left operand is null or undefined. As this is not a logical operator but used to access properties of an object, I didn’t delve into it but thought it was worth a mention.

Now that we’ve gone over operators, it’s time to learn about their operands!

Primitives

Primitives are considered the building blocks of most programming languages. In JavaScript, they are data types that are not objects; hence have no methods or properties. Primitives are also immutable and therefore their value cannot be altered.

JavaScript has seven primitive data types:

  • undefined
  • null
  • number
  • bigint
  • symbol
  • string
  • boolean

I will not go over the string or boolean data types, as these are generally well understood.

undefined

A variable is undefined if its value or property has not been declared or assigned.

undefined variable/property

The variable a is of type undefined when initialized without a value. Also if a is an empty object and we try to access a property which does not exist, instead of an error we get that the property is undefined

On the contrary, when trying to access a variable that does not exist/not-declared, then this throws a ReferenceError

function returns undefined

Functions that return no value return undefined

array index undefined

Accessing an array index that does not exist also returns undefined

null

Unlike undefined which indicates a variable devoid of any value, null is the value assigned to a variable and meant to represent “no value”. Hence, null is viewed as an object value and althoughtypeof null evaluates to object , it is not an object as it has no properties or methods, and is not mutable.

let newObject;

console.log(newObject); // undefined

newObject = null;

console.log(newObject); // null

console.log(newObject === null); // null

newObject = { name: "John Done", age: 30, email: "johndoe@email.com" }

//Clear object's value by assigning null
newObject = null;

A common use case for null is to clear, reset or be a placeholder for initialized variables.

strict-equality vs. typeof for null data types

Because typeof null evaluates to object , to test whether an object is null it is recommended to use the === strict equality operator. All other primitives can be tested using the typeof operator.

symbol

The symbol data type is a newer feature to JS that came out with ES6. Symbols are guaranteed to be unique and since they are immutable, are great to use as constants.

Symbols are created using the built-in Symbol object, whose constructor takes one optional argument (which sets the description attribute).

const firstName = Symbol("First name");
const lastName = Symbol("Last name");
let person = {
[firstName]: "George",
[lastName]: "Anton",
age: "Forever Young"
};
console.log(person[firstName]); // "George"
console.log(person[lastName]); // "Anton"
const new_symbol = Symbol("This is a new symbol");
const new_symbol2 = Symbol();
console.log(new_symbol.description) // "This is a new symbol"
console.log(new_symbol2.description) // undefined

Symbols are wrapped in [] square brackets in order to be set as keys for an object’s properties.

The description attribute is undefined if one is not provided.

const firstName = Symbol.for("First name");
const lastName = Symbol.for("Last name");
let person = {
[firstName]: "George",
[lastName]: "Anton",
age: "Forever Young"
};
console.log(person[firstName]); // "George"
console.log(person[lastName]); // "Anton"

Symbols can also be created using the Symbol.for, with the difference being that these symbols are now in the global scope of the program.

As shown below, Symbols cannot be accessed via the Member Access (dot) operator nor exposed by the Object.getOwnPropertyNames and Object.keysmethods.

They also can’t be converted to string so the JSON.stringify method overlooks them (and they also can’t be converted to number type).

Attempts to access value emailAddress symbol value

Although this does not mean symbols are private.

To access their value, use Computed Member Accessor Reflect.ownKeys

Accessing value of class using symbols

A common use case for symbols are as keys for object properties since their uniqueness avoids the possibility of having naming collisions. Prior to symbols, we had to use numbers or libraries like uuid to create unique strings for object keys, which is not always ideal.

number

The number data type is the default type for numeric values in JavaScript. It includes integers, floating-point values and special values such as Infinity , -Infinity , NaN (which stands for “Not a Number”) and both +/- 0. Since Numbers are represented using double-precision float, this means they have limited precision and hence have a finite representation.

maximum integer values — ECMAScript Docs

Integer values are bounded within the interval [-2⁵³, 2⁵³]

for integer x, domain of x is in [-2⁵³, 2⁵³]

Number.MIN_SAFE_INT , Number.MAX_SAFE_INTEGER return max/min int values

min/max safe integer values

The max/min floating point values can be found via the Number.MAX_VALUE and Number.MIN_VALUE attributes, values outside that range are not guaranteed precision.

maximum floating point values

Floats outside the range [-2¹⁰²⁴,2¹⁰²⁴] are represented by +/- Infinity

According to ECMAScript, there are 2⁵³-2 distinct NaN values, each one indistinguishable from the other (it is the only value in JavaScript that is not equal to itself).

NaN values are distinct from one another

NaN values occur when performing arithmetic operations on a non-numeric value or using Number to convert non-numeric values/strings

NaN conversions

bigint

bigint is the newest primitive introduced to JavaScript, and allows us to represent integers of any size without loss of precision. By using arbitrary-precision arithmetic, bigint’s ability to represent a numeric digit is limited only by the available memory of the host system.

Denoting a bigint is done by appending n to the end of an integer literal

Defining Big Int and Conversions

The logs on lines 7,10 indicate that number literals break down during arithmetic for (2⁵³-1) + 2 whereas bigint does not

Casting a numeric value to bigint can also be done with the global function, BigInt()

BigInt Literals defined past Number.MAX_VALUE

Where number literals use Infinity , BigInt is able to process the value

TypeError for arithmetic operations between number and bigInt types

Operations between bigint and number types will throw a TypeError

Numeric Type Conversion Warning — ECMAScript

The TypeError will be thrown in order to avoid loss of precision during calculations. Thus the implicit casting done by operators on different types will not work for BigInt. As best practice, it is recommended to solely work with either bigint or number types in a program and not both.

big int conversion loss

Precision loss can be experienced when casting values greater than Number.MAX_SAFE_INTEGER using BigInt function, as can shown above the with the number type conversion. It is recommend to cast larger values when they are string type to avoid this behavior.

BigInt makes it possible to perform integer operations without the risk of overflows and loss of precision, and might eventually form the basis for aBigDecimal implementation in JavaScript!

We saw this when performing arithmetic operations on bigint and number types, the interpreter throws a TypeError. Although, it is not uncommon for operators to perform calculations on different primitives (that’s practically how we get NaN values!). Such calculations can lead to unexpected results and even errors, this is thanks to something known as Implicit Type Casting (or coercion).

Implicit Type Casting (Coercion)

Type Casting is the process of converting a value from one data type to another. There are 2 ways to perform typecasting: implicitly and explicitly.

When we used the global BigInt() function to convert a string value to bigint, that is a form of explicit type casting. Number(), String() and Boolean() are three other functions globally available for explicit casting.

On the other hand, implicit type casting is when data types are converted by the language itself, without your knowledge. The only way to know is to be aware of the rules the language implements to undergo these casts.

Sidenote: BigInt type has no implicit conversions in JavaScript.

addition operator implicit casting

Above, we can see an example of implicit casting in action. The + operator is equipped to do both string concatenation as well as numeric addition. Although when the operands are of different types instead of throwing a TypeError, it seems JavaScript implicitly converts the number type to string and then returns the result of string concatenation.

Hence rather than getting an expected value of 3 we get 12.

Addition Operator — ECMAScript Guide

Let’s go over the Evaluation:

  • Steps 2 and 4 retrieve the value of the expression, in this case our operands x and y
  • Steps 5 and 6 are ignored, as ToPrimitive() acts on objects, and the values of our operands are of primitive types number and string
  • Step 7 checks the condition of whether either of the values are a string. In this case, y is and hence 7a converts our primitive value for x into the string “2”, and 7c returns us "1" + "2" which is "12".

Hence a fix to our previous script would be Number(x) + Number(y) to get 3.

Now what should we expect if x is a boolean, null or undefined ?

Addition Operations on various data types against string

As we can see, in all the above instances, x is converted to string type and concatenated with y. This is simply because y is of type string (step 7)

What if y is not of type string?

Addition Operations between non-numeric and non-string data types

Let’s go over the Evaluation again:

  • Steps 2 and 4 retrieve the value for each operand
  • Steps 5 and 6 are ignored, as for all cases we are dealing with Primitives
  • Step 7 is ignored, as neither x or y is of type string
  • Steps 8 and 9 convert the values of x and y to number
  • Step 10 adds a check to make sure both number types are the same and that the prior ToNumeric() conversion was successful
  • Step 12 returns the result of Number::add, which is the numeric arithmetic on the castednumber values.

Now to explain the outputs:

  1. Since Number(undefined) is NaN and typeof NaN is number, the value of two undefined variables becoming NaN is evaluated as expected.

2. NaN + any number expression (including Infinity) always results in NaN

3. null converted to a number is 0, which can be checked via Number(null), hence 0 + 0 = 0.

4/5. boolean true and false get cast to 1 and 0 respectively, giving the expected results.

Therefore knowing how operators treat differing data types helps prevent errors. Implicit casting is not unique to the + operator!

Now one case we skipped over is, how does + deal with objects?

ToPrimitive() and Primitive Coercion

If we recall, steps 5 and 6 call ToPrimitive() on the operand values.

In JavaScript, abstract operations are specifications of an algorithm or behavior which are used internally by the language to help perform specific tasks, like coercion.

ToPrimitive() is an abstract operation and is not available to developers.

ToPrimitive abstract operation — ECMAScript Guide

ToPrimitive is used to convert objects to primitives and takes 2 arguments.

Its first argument, input, is the expression or object to convert and the second, optional preferredType, is one of “default”, “string” or “number”. The preferredType is also referred to as hint.

Step 1 checks if the input is of type object, if not then the input is already primitive and no action is performed (Step 2).

valueOf() and toString() object methods

The ToPrimitive operation works in the following order:

  1. Looks at the object’s valueOf method and if exists, calls it. If valueOf returns a primitive, then that primitive value is used.
  2. If valueOf does not return a primitive, then it looks at the object’s toString method and if exists, calls it. If toString returns a primitive, then that value is used.
  3. If toString does not return a primitive, then we look at the Object.prototype built-in methods for valueOf and toString (repeating steps 1 and 2 until a primitive is returned).
  4. If no primitive is returned, a TypeError is thrown.

In the code above, we see that both obj + "World" and obj + 8 have “obj” evaluate to 2. This is because the valueOf method executes first, and as 2 is a number type, then it is a primitive and that is what is returned.

Now recall, this is Step 5 in the +operator casting. Therefore, the primitive 2 gets converted to string if the second operand is string, and hence we have string concatenation. Otherwise it remains a number and we add.

hint === “string”

One way to override the previous ToPrimitive operation behavior is to provide a hint. Previously hint was set as “default” and in order to provide a hint, we need to use explicit casting.

When explicitly casting “obj” with String, we tell ToPrimitive that we want to use toString() first (this is our preferredType) and in both situations Hello is returned (and then we’re back to Step 7 of + manual).

OrdinaryToPrimitive Operation — ECMAScript Guide

The steps we just outlined is actually defined by the OrdinaryToPrimitive abstract operation, which is actually Step 1d in ToPrimitive. We’ve essentially skipped over 1a — 1c. Now I will discuss when 1a is used, and what exoticToPrim means.

The internal [@@toPrimitive]() method

We’ve seen that when we pass a hint to the abstract ToPrimitive operation, like string then internally we call the appropriate methods (in this case toString()) of the object (if those methods exist).

The (internal) function that performs these tasks is a well-known Symbol called [@@toPrimitive]()(“well-known symbols are denoted by @@ and are pre-defined Symbols in JavaScript).

Although this symbol method is internal to JavaScript, it can be overridden using Symbol.toPrimitive. JavaScript view objects with this method as exotic and the exoticToPrim property become defined (Steps 1a/b).

an “exotic” object — a defined [Symbol.toPrimitive]() method

Above is an example of an exotic object. Although valueOf and toString are defined, the [Symbol.toPrimitive]() method overrides these behaviors.

Because we don’t have an explicit hint === "default" or simply a return outside of the scope of the if/else-if block, then obj + " World" will return undefined. This is because we overrode the “default” behavior and [@@toPrimitive]() expects a value from the hint for “default” but has none assigned.

Wrapping Up

To wrap this up, let’s walk through why {} + [] returns [object Object]:

  1. Steps 2, 4 of + operation, GetValue({}) = {}and GetValue([]) = []
  2. As these are not primitive, Step 5, 6 of + operation call ToPrimitive
  3. Step 1 ofToPrimitive operation confirms {} and [] are object type.
  4. We skip step 1a, 1b as these are empty and hence not exotic
  5. Step 1c uses OrdinaryToPrimitive. As these are empty, there are no user-defined valueOf and toString methods.
  6. Consider left operand first. {} is a member of Object.prototype and hence inherits both a .valueOf() and .toString() method. By default, OrdinaryToPrimitive calls {}.valueOf() which returns an object.
  7. Since object is not a primitive, it then calls {}.toString(), this returns the string type [object Object].
  8. Consider right operand. [] is a member of Array.prototype and also inherits both a .valueOf() and .toString() method. First [].valueOf() is called, which returns an object type
  9. Since object is not a primitive, it then calls [].toString(), this returns the string type "”.
  10. Step 7 of + operation, since both operands of string, we perform string concatenation, and so [object Object] + "" evaluates to the string [object Object].

Inspiration

If you’ve made it this far, I’d like to share the inspiration for this article:

Thanks for inventing JavaScript!

See if you can make sense of the outputs given what we now know!

I hope you find this article informative and most of all, thanks for reading!

Connect with me on LinkedIn.

Resources

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

George Anton
George Anton

Written by George Anton

Software Engineer | AWS | Python | Node | Math Enthusiast | BJJ Practitioner