Deep JavaScript. Tips
- Operator ++
- Types
- Undefined, Undeclared, and TDZ
- NaN and .isNaN()
- Negative zero -0
- Built-in Objects
- Coercion
- Scope
- Closure
- Objects
- Prototypes
1. Operator ++
let x = 40
x++ // 40
x // 41
++x // 42
x // 42
2. Types: Undefined, Null, Boolean, String, Symbol, Number, and Object.
// historical issues, not obvious behavior:
typeof someName // undefined
const a = null
typeof a // 'object'
const b = fucntion (){}
typeof b // 'function'
const c = [1,2,3]
typeof c // 'object'
3. Undefined, Undeclared and TDZ in JavaScript.
- Undefined variables have been declared but not assigned a value, representing the absence of a value. Undeclared variables are used without being formally declared, initially don’t exist, result is a
ReferenceError
.
// Undefined
let myVar
console.log(myVar) // Output: undefined
// Undeclared
console.log(myVar) // Output: ReferenceError: myVar is not defined
- Temporal Dead Zone (TDZ): specific to
let
andconst
variables, it is the period between the start of the scope and the actual declaration where the variable cannot be accessed, resulting in aReferenceError
if accessed during this period.
console.log(myVar) // Output: ReferenceError: Cannot access 'myVar' before initialization
let myVar = 42
4. NaN
and .isNaN()
NaN
stands for "Not-a-Number" / Invalid Number and is a special value that represents the result of an operation that should return a number but doesn't. It serves as an indicator that a mathematical operation failed or produced an undefined or unrepresentable value.
// Dividing zero by zero:
const result = 0 / 0 // NaN
// Converting a non-numeric string to a number:
const result = parseInt("abc") // NaN
// Performing arithmetic operations with non-numeric values:
const result = "hello" * 5 // NaN
To check whether a value is NaN
, you can use the isNaN()
function. However, it's important to note that isNaN()
has some quirks:
isNaN(NaN) // true
isNaN("hello") // true (because attempting to convert "hello" to a number results in NaN)
isNaN(42) // false (42 is a valid number)
(!) isNaN()
can return unexpected results when dealing with non-numeric values that can be converted to a number, like empty strings. To overcome the quirks use Number.isNaN()
Number.isNaN(NaN) // true
Number.isNaN("hello") // false
Number.isNaN(42) // false
Number.isNaN()
is more strict and only returns true
for values that are exactly NaN
. It does not perform any type coercion.
5. Negative zero -0
In JavaScript, negative zero (-0
) is a special value that represents the result of certain mathematical operations where the sign of zero matters. While conceptually, zero and negative zero are considered equal, they are distinct values with some nuanced behaviors. You can get by dividing a negative number or by negative infinity.
const result = -3 / Infinity // -0
0 === -0 // true
0 == -0 // true
0 < -0 // false
0 > -0 // false
The Object.is()
static method determines whether two values are the same, does no type conversion and no special handling for NaN
, -0
, and +0
let x = 0
let y = -0
console.log(Object.is(x, y)) // false
console.log(1 / x) // Infinity
console.log(1 / y) // -Infinity
(!) fix Math.sign()
method:
Math.sign(-3) // -1
Math.sign(3) // 1
Math.sign(-0) // -0
Math.sign(0) // 0
function sign(num) {
return num !== 0 ? Math.sign(num) : Object.is(num, -0) ? -1 : 1
}
sign(-3) // -1
sign(3) // 1
sign(-0) // -1
sign(0) // 1
6. Fundamental Objects / Built-in Objects / Native Functions
The use of new
is typically associated with constructor functions, which are functions specifically designed to be called with the new
keyword. When you use new
with a function, it transforms the function call into a constructor call, and the function is expected to initialize and return a new object.
// use 'new'
new Object()
new Array(1, 2, 3)
new Function()
new Date()
new RegExp("[0-9]")
new Error("This is an error message.")
// don't use 'new'
String("Hello, World!")
Number(42)
Boolean(true)
Math.sqrt(25)
7. Coercion (Type Conversion) in JavaScript
Coercion refers to the automatic conversion of values from one data type to another. JavaScript is a loosely typed or dynamically typed language, which means that variables are not bound to a specific data type. During operations or comparisons involving different data types, JavaScript may perform coercion to ensure that the operation can be completed.
// Implicit Coercion
const num = 5
const str = "10"
// Implicit coercion: Converts num to a string and performs string concatenation
const result = num + str
console.log(result) // Output: "510"
// Explicit Coercion
const str = "42"
// Using parseInt for explicit coercion from string to number
const num = parseInt(str)
console.log(num) // Output: 42
// Corner (wierd) Cases of Coercion
+'' // Output: 0
-'' // Output: -0
Number('') // Output: 0
Number(' \t\n') // Output: 0
Number(null) // Output: 0
Number(undefined) // Output: NaN
Number([]) // Output: 0
Number([1,2,3]) // Output: NaN
Number([null]) // Output: 0
Number([undefined]) // Output: 0
Number({}) // Output: NaN
String(-0) // Output: 0
String(null) // Output: 'null'
String(undefined) // Output: 'undefined'
String([null]) // Output: ''
String([undefined]) // Output: ''
Boolean(new Boolean(false)) // Output: true
1 < 2 < 3 // Output: true
// (true) < 3
// 1 < 3 is true
3 < 2 < 1 // Output: false
7.1. toString
method
toString
is a method that is available on most objects, including the fundamental objects like numbers, strings, and arrays. The toString
method is used to convert an object or a value to its string representation.
Number(42).toString() // Output: "42"
[1, 2, 3].toString() // Output: "1,2,3"
Boolean(true).toString() // Output: "true"
Number(-0).toString() // Output: "0"
7.2. Number
Number('') // Output: 0 (!)
Number('0') // Output: 0
Number('-0') // Output: -0
Number('009') // Output: 9
Number('3.14159') // Output: 3.14159
Number('0.') // Output: 0
Number('.0') // Output: 0
Number('.') // Output: NaN
Number('0xaf') // Output: 175
Number(false) // Output: 0 (!)
Number(true) // Output: 1 (!)
Number(null) // Output: 0
Number(undefined) // Output: NaN (!)
Number([]) // Output: 0
Number(['0']) // Output: 0
Number(['-0']) // Output: -0
Number([null]) // Output: 0 (!)
Number([undefined]) // Output: 0 (!)
Number([1,2,3]) // Output: NaN
Number([[[]]]) // Output: 0
Number({...}) // Output: NaN
Number({valueOf(){return 3}}) // Output: 3 (if you redefine the object valueOf method)
7.3. Boolean
'', 0, -0, null, undefined, NaN, false // Output: false
// other values are true
7.4. Boxing
Boxing refers to the automatic conversion of primitive data types (such as numbers, strings, and booleans) into their corresponding wrapper objects (Number, String, and Boolean) when an object-specific method is called on them. This conversion allows primitive values to temporarily act like objects, giving them access to object-related methods and properties.
let primitiveNumber = 42
let primitiveString = "Hello, World!"
let primitiveBoolean = true
// Using object-specific methods on primitives
let boxedNumber = new Number(primitiveNumber)
let boxedString = new String(primitiveString)
let boxedBoolean = new Boolean(primitiveBoolean)
console.log(boxedNumber.toFixed(2)) // Using a method on the boxed number
console.log(boxedString.length) // Using a property on the boxed string
console.log(boxedBoolean.valueOf()) // Using a method on the boxed boolean
7.5. Equality
===
(strict equality) checks both value and type without performing type coercion. Use ===
by default to avoid unexpected type coercion and ensure a more predictable comparison.
==
(loose equality) performs type coercion, converting values to a common type before comparison. If the types are the same, it works as ===
. If null
or undefined
is involved, values are considered equal. For non-primitives, uses the toPrimitive
method, while other primitives convert to a number. Use ==
cautiously, only when you explicitly want to allow type coercion.
=== // check values amd types (strict)
== // check values (loose)
// == works as === if types are equal
42 == [42] // Output: true, == convert the array to primitive
// Corner (wierd) Cases of Equality
[] == ![] // Output: true (!)
// The empty array [] is coerced to an empty string, and the boolean ![] (which is false) is also coerced to a string.
const arr = []
if (arr) {...} // Output: true
if (arr == true) {} // Output: false
if (arr == false) {} // Output: true
8. Scope
Scope defines the context in which variables and functions are accessible — where to look for things. JavaScript compilation consists of two parts: parsing and execution. Scope is considered within the execution process, which begins by identifying targets (assigning variables and calling functions; it receives something) and its sources (values and inner scopes of functions).
- Global Scope: Variables declared outside any function or block have global scope, meaning they can be accessed from anywhere in the code.
// Global scope
const globalVariable = "I am global"
function exampleFunction() {
console.log(globalVariable) // Accessible inside the function
}
exampleFunction()
console.log(globalVariable) // Accessible outside the function
- Local (Function) Scope: Variables declared inside a function have local scope and are only accessible within that function.
function exampleFunction() {
// Local scope
const localVariable = "I am local"
console.log(localVariable) // Accessible inside the function
}
exampleFunction()
// console.log(localVariable) // Error: localVariable is not defined outside the function
- Block Scope (with let and const): Variables declared with
let
andconst
have block scope, meaning they are accessible only within the block where they are defined.
if (true) {
// Block scope
let blockVariable = "I am in a block"
console.log(blockVariable) // Accessible inside the block
}
// console.log(blockVariable) // Error: blockVariable is not defined outside the block
- Nested Scope: Functions and blocks can be nested, creating nested scopes. Inner scopes can access variables from outer scopes, but the reverse is not true.
function outerFunction() {
// Outer scope
const outerVariable = "I am outer"
function innerFunction() {
// Inner scope
const innerVariable = "I am inner"
console.log(outerVariable) // Accessible in the inner function
}
innerFunction()
// console.log(innerVariable) // Error: innerVariable is not defined in the outer function
}
outerFunction()
8.1. Shadowing
Shadowing occurs when a variable declared within a certain scope has the same name as a variable in an outer scope.
// Outer scope variable
let variable = "Outer"
function exampleFunction() {
// Inner scope variable with the same name as the outer variable
let variable = "Inner"
console.log(variable) // Accesses the inner variable
}
exampleFunction()
console.log(variable) // Accesses the outer variable
8.2. Dynamic Global Variables
Dynamic creation or modification of global variables during runtime. Be careful or avoid creating new variables without declaration inside child scopes, as it can affect the global scope.
// Existing global variable
var globalVar = "Initial value"
function fn() {
// Dynamic modification of the global variable
globalVar = "Updated value"
// equal window.newUnexpectedGlobalVar = "Some value"
newUnexpectedGlobalVar = "Some value"
}
fn()
// Accessing the modified global variable
console.log(globalVar) // Output: Updated value
console.log(newUnexpectedGlobalVar) // Output: Some value
ask('???') // Output: ReferenceError
// If the source is undeclared, we always get a ReferenceError.
// Dynamic creation works only for the target.
(!) using 'strict mode'
you’ll get a ReferenceError
when trying to modify the global scope without declaration.
8.3. Function Expressions
A function expression in JavaScript is a way to define a function as part of an expression, assigning it to a variable. It allows functions to be treated as values, enabling features like anonymous functions, immediately invoked function expressions (IIFE), and the ability to pass functions as arguments. Function expressions offer flexibility and are commonly used for more advanced programming patterns.
// Anonymous Function Expression
const addNumbers = function(x, y) {
return x + y
}
console.log(addNumbers(3, 5)) // Output: 8
// Named Function Expression
const multiply = function multiplyNumbers(a, b) {
return a * b
}
console.log(multiply(2, 4)) // Output: 8
// since multiplyNumbers is within the scope of multiply
console.log(multiplyNumbers(1, 3)) // Output: ReferenceError
// Immediately Invoked Function Expression (IIFE)
const result = (function() {
return "I am an IIFE!"
})()
console.log(result) // Output: I am an IIFE!
(!) Named Function Expressions can help avoid anonymous functions in the debugging process, call functions recursively, and make the code more self-documenting.
8.4. Hoisting
Hoisting is a JavaScript behavior where variable and function declarations are moved to the top of their containing scope during compilation.
Variable Hoisting:
// var is accessible before its declaration
console.log(x) // Output: 'undefined'
var x = 5
console.log(x) // Output: 5
Function Hoisting:
foo() // Output: 'Hello'
function foo() {
console.log('Hello')
}
Note about let
and const
:
console.log(y) // Throws a ReferenceError
let y = 10
9. Closure
Closure in JavaScript is a function that retains access to variables from its outer (enclosing) scope, even after that scope has finished executing. Represents the lexical scope.
function outerFunction(x) {
// Inner function has access to 'x' from the outer scope
return function innerFunction(y) {
console.log(x + y)
}
}
// Create a closure by calling outerFunction and assigning
// the result to a variable
const closure = outerFunction(5)
// The closure can still access 'x' from its outer scope,
// even though outerFunction has finished executing
closure(3) // Output: 8
// var
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(`i: ${i}`)
}, i * 1000)
}
// i: 4
// i: 4
// i: 4
// let
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(`i: ${i}`)
}, i * 1000)
}
// i: 1
// i: 2
// i: 3
10. Objects
10.1. this
represents the idea of the dynamic scope in JS (a lexical scope basically uses).
Assigning this
using .bind()
const person = {
name: 'John',
greet: function() {
console.log(`Hello, ${this.name}!`)
}
}
// Using setTimeout without bind() would result in losing the correct 'this' context.
// 'this' inside the setTimeout callback would refer to the global object (e.g., window in a browser).
setTimeout(person.greet, 1000) // Output: Hello, undefined!
// Using bind to explicitly set 'this' to the person object
setTimeout(person.greet.bind(person), 1000) // Output: Hello, John!
10.2. new
creates and invokes a new instance of an object.
function Person(name, age) {
this.name = name
this.age = age
}
// Using new
const person = new Person('John', 30)
console.log(person) // { name: 'John', age: 30 }
function Person(name, age) {
this.name = name
this.age = age
}
// Without new (which can lead to issues)
const person = Person('Jane', 25)
console.log(person) // undefined
console.log(name) // 'Jane' (creates a global variable 'name')
(!) in none-'strict mode'
this
without new
points to the global environment.
10.3. Lexical this
refers to how the value of this
is determined within an arrow function. Unlike regular functions, arrow functions don't have their own this
context; instead, they inherit the this
value from the enclosing (surrounding) scope in which they are defined. This behavior is often referred to as "lexical scoping" or "lexical binding."
const myObject = {
value: 1,
regularMethod: function () {
// Regular function with its own 'this' binding
setTimeout(function () {
this.value++
console.log("Inside regularMethod:", this.value) // Output: NaN (in non-strict mode) or TypeError (in strict mode)
}, 1000)
},
arrowMethod: function () {
// Arrow function with lexical 'this'
setTimeout(() => {
this.value++
console.log("Inside arrowMethod:", this.value); // Output: 2
}, 1000)
}
}
myObject.regularMethod() // Issues with 'this' inside the regular method
myObject.arrowMethod() // 'this' is correctly maintained with the arrow method
(!) An arrow function is not typically used as a method because it captures the this
value from its surrounding lexical scope, which can lead to unintended behavior, often referring to the global scope.
// Error
const obj = {
name: 'John',
ask: question => {
console.log(this.name, question) // this referrs to the global scope
}
}
obj.ask('Some question') // Output: undefined Some question
obj.ask.call(obj, 'Some question') // Output: undefined Some question
but allowed in new ES6 syntax:
class Obj {
constructor (name) {
this.name = name
this.ask = question => {
console.log(this.name, question) // this referrs to the Obj scope
}
}
}
const person = new Obj('John')
person.ask('Some question') // Output: John Some question
10.4. super
keyword is used within a derived class to call functions or access properties from its superclass (or parent class).
class Animal {
constructor(name) {
this.name = name
}
makeSound() {
return "Some generic sound"
}
}
class Dog extends Animal {
constructor(name, breed) {
// Using super to call the constructor of the superclass (Animal)
super(name)
// Adding a new property specific to Dog
this.breed = breed
}
makeSound() {
// Using super to call the makeSound method of the superclass (Animal)
const genericSound = super.makeSound()
return `Woof, ${genericSound}`
}
bark() {
return "Woof!"
}
}
// Creating an instance of Dog
const myDog = new Dog("Buddy", "Golden Retriever")
// Accessing properties and methods
console.log(myDog.name) // Outputs: Buddy
console.log(myDog.breed) // Outputs: Golden Retriever
console.log(myDog.makeSound()) // Outputs: Woof, Some generic sound
console.log(myDog.bark()) // Outputs: Woof!
11. Prototypes
11.1. constructor
set up the initial state of the class
, makes an object linked to its own prototype. Prototype points to parent entity, constructor
points to the child from parent.
+------------------+
| Parent Entity |
| |
+------------------+
^ Constructor
| |
Prototype v
+------------------+
| Child (Class) |
| |
+------------------+
class MyClass {
constructor(property1, property2) {
this.property1 = property1
this.property2 = property2
}
myMethod() {
console.log(`Property values: ${this.property1}, ${this.property2}`)
}
}
const myObject = new MyClass('value1', 'value2')
// Accessing the constructor property
console.log(myObject.constructor) // Output: [Function: MyClass]
// Checking if the constructor is the same as MyClass
console.log(myObject.constructor === MyClass) // Output: true
// Accessing a method from the prototype
console.log(MyClass.prototype.myMethod) // Output: [Function: myMethod]
// Invoking the method from the prototype
MyClass.prototype.myMethod.call(myObject) // Output: Property values: value1, value2
11.2. Prototype Chain
The Prototype Chain is a mechanism that allows objects to inherit properties and methods from other objects through their prototypes. Every object in JavaScript has an associated prototype, and these prototypes form a chain, creating a hierarchy of objects linked together.
11.3. OLOO (Objects Linked to Other Objects)
OLOO [olu:] is a design pattern in JavaScript that promotes object delegation instead of classical inheritance. It stands for “Objects Linked to Other Objects” and emphasizes creating objects that directly delegate to other objects without relying on a traditional class hierarchy.
// Parent class (delegate)
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log('Some generic sound');
}
}
// Child class with delegation to Animal
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log('Woof!');
}
}
// Instances
const myDog = new Dog('Buddy', 'Golden Retriever');
// Using shared behavior from Animal
myDog.makeSound(); // Outputs: Some generic sound
// Using additional behavior from Dog
myDog.bark(); // Outputs: Woof!