Javascript is a dynamically-typed, interpreted programming language that was originally developed to add dynamic and interactive elements to web pages. It runs both, on the client and server-side.
Running on the client-side means that the code is processed by the browser.
Running on the server-side is possible with Node.js.
Node.js is an open source development platform for executing JavaScript code server-side.
It means that source code isn’t compiled into binary code prior to execution.
Then how does the computer receives instructions to execute?
That’s the job for a JavaScript engine.
JavaScript engine is a program responsible for translating source code into machine code and executing the translation result on a computer’s central processing unit (CPU).
In order to start coding in JavaScript, you don’t have to install any additional software. Each modern web browser comes with a JavaScript engine out-of-the-box. You can simply run scripts inside the browser.
Dynamically-typed languages are those where the interpreter assigns variables a type at runtime based on the variable's value.
In computer science, runtime, run time or execution time is the time when CPU is executing machine code.
Consider the example code below:
let x = 10;
console.log(typeof x); // Prints "number"
x = "string value";
console.log(typeof x); // Prints "string"
You can reassign data of a different type to the variable and no error will be generated.
It seems cool that you don't have to bother what type to use, but in most cases, this approach leads to unexpected runtime errors:
let user = {
name: "John",
surname: "Doe",
};
const updateUser = () => {
user = {
name: "Andrew",
surnam: "Hopkins",
};
};
updateUser();
console.log(user.surname); // Prints "undefined"
Have you found an error in the code above?
You're absolutely right, we misspelled surname
property. Notice that the code is allowed to be run even though Javascript can be smart enough to raise an error before you even finish to type misspelled property name. But it needs your hand.
Static-typed languages are those where the types are checked on the fly, during code execution.
If you mess up the data types of your variables in a statically typed language, you’ll see an error instantly and won’t even be able to run your program.
Is this the only benefit? The answer is - no.
It also offers auto-completion, generating documentation, and faster compilation.
Typescript is not actually a static type check but can be used as one. It is a programming language developed by Microsoft, typed superset of JavaScript which is compiled to plain JavaScript. It's extremely useful when building large-scale applications.
Using typescript in the following code would raise type error:
let x: number = 10;
// ERROR: Type "string value" is not assignable to type "number".
x = "string value";
In the second example we define User
type which requires name
and surname
properties. Afterwards, we let the compiler know that user
variable is of a User
type and if the wrong field gets modified, an error is thrown:
type User = {
name: string;
surname: string;
};
let user: User = {
name: "John",
surname: "Doe",
};
const updateUser = () => {
user = {
name: "Andrew",
// ERROR:
// Type "{ name: string; surnam: string; }" is not assignable to type "User".
// Object literal may only specify known properties, but "surnam" does not
// exist in type "User". Did you mean to write "surname"?
surnam: "Hopkins",
};
};
I recommend you to read this awesome article to learn more about Typescript.
Conclusion: always use Typescript in medium/large scale applications.
There are 7 types considered to be Primitive and 1 Reference type.
Primitive is not an object and has no methods of its own. All primitives are immutable.
Reference points to the object’s location in memory. It does not actually contain value.
true
or false
10
, floats: 10.20
BigInt - created by appending n
to the end of an integer literal: 10n
Number
primitive type has some limitations, it can not represent value larger than 2^53
(or less than -2^53
for negatives). We can need really large numbers for cryptography or microsecond-precision timestampsthis is string
Everything else is an Object type (objects are used to store collections of data and more complex entities).
The key difference between them is how they store values.
If Primitive type is assigned to a variable, it means that the variable actually contains primitive value.
const a = 1;
let b = 2;
const c = a; // Variable "a" is copied to "c"
const d = b; // Variable "b" is copied to "d"
console.log(a); // Prints "1"
console.log(b); // Prints "2"
console.log(c); // Prints "3"
console.log(d); // Prints "4"
// Change "b" value
b = 3;
console.log(b); // Prints "3"
console.log(d); // Prints "2"
Important note: primitives are copied by value. Changing one of the variables above does not change the other because each of them stores its own copy of the value.
If the reference type is assigned to a variable, it means that the variable actually contains a reference to the value.
const user = {
name: "John",
surname: "Doe",
};
const newUser = user;
newUser.name = "Andrew";
console.log(newUser.name); // Prints "Andrew", as we expect, right?
console.log(user.name); // Prints "Andrew" as well! Pretty weird?
Let me explain this: when you create an object user
it's value {name: "John", surname: "Doe"}
is stored in some location in your computer's memory. What the variable user
receives is a memory address which points to stored value.
When you copy user
value to another variable newUser
it's the address that gets actually copied, not the value.
Important note: objects are copied by reference instead of by value.
Both user
and newUser
contain a reference to the same object in memory. Therefore, altering any of the values will cause both to update.
To reassign a reference means to replace the old reference with a new one. Example:
let user = {
name: "John",
surname: "Doe",
};
user = {
name: "Andrew",
surname: "Hopkins",
};
Now user
variable stores a reference to the new value { name: "Andrew", surname: "Hopkins" }
but the old value { name: "John", surname: "Doe" }
is still present in memory.
When there are no references to values in memory, JavaScript engine can perform garbage collection.
Some high-level languages, such as JavaScript, utilize a form of automatic memory management known as garbage collection(GC). The purpose of a garbage collector is to monitor memory allocation and determine when a block of allocated memory is no longer needed and reclaim it.
Object equality could be confusing at first sight, but after understanding Reference types it should become pretty straightforward.
You might suppose that if 2 objects have the same properties and values they can be considered equal. Not really, let's see some examples:
const johnsCar = {
name: "Audi",
topSpeed: 300,
};
const andrewsCar = {
name: "Audi",
topSpeed: 300,
};
console.log(johnsCar == andrewsCar); // Prints "false"
console.log(johnsCar === andrewsCar); // Prints "false"
You probably know what's going on, after mastering the previous section, right?
Reference types are compared by reference. It basically means, that javascript checks if both objects point to the same memory location. If the case, it is not true, so equality operator returns false
.
Consider another example:
const johnsCar = {
name: "Audi",
topSpeed: 300,
};
const andrewsCar = johnsCar;
console.log(johnsCar == andrewsCar); // Prints "true"
console.log(johnsCar === andrewsCar); // Prints "true"
andrewsCar
points to the same location as johnsCar
, therefore objects considered equal.
If we have two distinct objects and want to see if their properties are the same, the easiest way to do so is to turn them both into strings and then compare the strings:
const johnsCar = {
name: "Audi",
topSpeed: 300,
};
const andrewsCar = {
name: "Audi",
topSpeed: 300,
};
// Prints "true"
console.log(JSON.stringify(johnsCar) === JSON.stringify(andrewsCar));
Other options:
Javascript has both strict ===
and abstract ==
equality comparisons.
Strict comparison:
true
if operands are of the same type1 === 1 // true
1 === "1" // false
Abstract equality comparison:
1 == 1 // true
1 == "1" // true
Coercion is the term that is used for unexpected type casting in JavaScript
Every +
expression concatenates the values if one is already a string or they will be cast to strings if possible.
1 + 1 // 2
1 + "1" // "11"
"2" + 22 // "222"
// Explanation:
// String(["one", "two"]) -> "one,two"
// String(["three", "four"]) -> "three,four"
// "one,two" + "three,four" -> "one,twothree,four"
["one", "two"] + ["three", "four"] // "one,twothree,four"
// Explanation:
// String({}) -> "[object Object]"
// "[object Object]" + "[object Object]" -> "[object Object][object Object]"
{} + {} // "[object Object][object Object]"
// Explanation:
// String([]) -> ""
// String({}) -> "[object Object]"
// "" + "[object Object]" -> "[object Object]"
[] + {} // "[object Object]"
// Explanation
// {} is not parsed as object, but instead as empty block
// Return value of empty block is empty
// The result of this expression equals +[] -> +String([]) -> +"" -> 0
{} + [] // 0!
-
, *
, /
expression can be used only with numbers, so all operands will be casted to numbers.
"1" - 1 // 0
"22" - "2" // 20
10 / "5" // 2
"20" / "2" // 10
2 * "2" // 4
"3" * "3" // 9
// Explanation
// Number([3]) -> 3
// Number([1]) -> 1
// 3 - 1 -> 2
[3] - [1] = 2;
// Explanation
// Number(["one", "two"]) -> NaN
// Number(["three", "four"]) -> NaN
// NaN - NaN -> NaN
["one", "two"] - ["three", "four"] // NaN
["one", "two"] * ["three", "four"] // NaN
["one", "two"] / ["three", "four"] // NaN
// Explanation:
// Number({}) -> NaN
// NaN - NaN -> NaN
{} - {} // NaN
{} * {} // NaN
{} / {} // NaN
// Explanation:
// Number([]) -> 0
// Number({}) -> NaN
// 0 - NaN -> NaN
[] - {} // NaN
[] * {} // NaN
[] / {} // NaN
// Explanation
// {} is not parsed as object, but instead as empty block
// Return value of empty block is empty
// The result of this expression equals -[] -> -Number([]) -> -0
{} - [] // -0!
// Explanation
// *Number([]) or /Number([]) -> SyntaxError: Unexpected token
{} * [] // SyntaxError: Unexpected token!
{} / [] // SyntaxError: Unexpected token!
Type-converting comparison x == y
according to specification:
// typeof x equals typeof y -> return x === y
1 == 1 // true
[] == [] // false
// x is null and y is undefined -> return true
null == undefined // true
// y is null and x is undefined -> return true
undefined == null // true
// typeof x is number and typeof y is string -> return x == ToNumber(y)
1 == "str" // false
// typeof x is string and typeof y is number -> return ToNumber(x) == y
"str" == 1 // false
// typeof x is boolean -> return ToNumber(x) == y
true == 1 // true
// typeof y is boolean -> return x == ToNumber(y)
1 == true // true
// typeof x is string/number/symbol and typeof y is object -> return x == ToPrimitive(y)
"[object Object]" == {} // true
// typeof x is object and typeof y is string/number/symbol -> return ToPrimitive(x) == y
{} == "str" // Remember about "SyntaxError: Unexpected token" here
const obj = {};
obj == "[object Object]" // true
Important note: make sure to check the most complete list of WTFs in JavaScript.
Hopefully, you have an understanding of how cool is JavaScript and how easy is to start working with it.
Quick recap:
===
and abstract ==
equality comparisonsHiring managers looking to spot the best Javascript developers may want to check out this article from Toptal.