5 Basic Programming Concepts for JavaScript Developers
A deeper understanding of core programming concepts
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.

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
?

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!

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

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)

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.

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 someNameVar
value 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:

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.

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

Functions that return no value return 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.

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.keys
methods.
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).

Although this does not mean symbols are private.
To access their value, use Computed Member Access
or Reflect.ownKeys

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.

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

Number.MIN_SAFE_INT
, Number.MAX_SAFE_INTEGER
return max/min int 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.

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 occur when performing arithmetic operations on a non-numeric value or using Number
to convert non-numeric values/strings

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

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()

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

Operations between bigint
and number
types will throw a TypeError

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.

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.

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
.

Let’s go over the Evaluation:
- Steps 2 and 4 retrieve the value of the expression, in this case our operands
x
andy
- Steps 5 and 6 are ignored, as
ToPrimitive()
acts on objects, and the values of our operands are of primitive typesnumber
andstring
- Step 7 checks the condition of whether either of the values are a
string
. In this case,y
is and hence7a
converts our primitive value forx
into thestring
“2”, and7c
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
?

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
?

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
ory
is of typestring
- Steps 8 and 9 convert the values of
x
andy
tonumber
- Step 10 adds a check to make sure both
number
types are the same and that the priorToNumeric()
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:
- Since
Number(undefined)
isNaN
andtypeof NaN
isnumber
, the value of two undefined variables becomingNaN
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
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).

The ToPrimitive
operation works in the following order:
- Looks at the object’s
valueOf
method and if exists, calls it. IfvalueOf
returns a primitive, then that primitive value is used. - If
valueOf
does not return a primitive, then it looks at the object’stoString
method and if exists, calls it. IftoString
returns a primitive, then that value is used. - If
toString
does not return a primitive, then we look at theObject.prototype
built-in methods forvalueOf
andtoString
(repeating steps 1 and 2 until a primitive is returned). - 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.

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).

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).

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]
:
- Steps 2, 4 of
+
operation,GetValue({}) = {}
andGetValue([]) = []
- As these are not primitive, Step 5, 6 of
+
operation callToPrimitive
- Step 1 of
ToPrimitive
operation confirms{}
and[]
areobject
type. - We skip step 1a, 1b as these are empty and hence not exotic
- Step 1c uses
OrdinaryToPrimitive
. As these are empty, there are no user-definedvalueOf
andtoString
methods. - Consider left operand first.
{}
is a member ofObject.prototype
and hence inherits both a.valueOf()
and.toString()
method. By default,OrdinaryToPrimitive
calls{}.valueOf()
which returns anobject
. - Since
object
is not a primitive, it then calls{}.toString()
, this returns thestring
type[object Object]
. - Consider right operand.
[]
is a member ofArray.prototype
and also inherits both a.valueOf()
and.toString()
method. First[].valueOf()
is called, which returns anobject
type - Since
object
is not a primitive, it then calls[].toString()
, this returns thestring
type"”
. - Step 7 of
+
operation, since both operands ofstring
, 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:

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.