Data Types

Number

// we can call methods on primitives! 
// 1. javascript converts them to wrapper objects - Number, String, Boolean, BigInt, Symbol
// 2. calls the method on that object and returns the output
// 3. destroys the wrapper object

let str = "Hello"
str.toUpperCase()	// HELLO

let n = 1.23456
n.toFixed(2)		// 1.23

let num = new Number(123)	// this is possible ofcourse, but never do this!

// null and undefined have no methods since they don't have wrapper objects


// Numbers
let billion = 1000000000
let billion1 = 1_000_000_000
let billion2 = 1e9
let micro = 1e-6

alert(0.1 + 0.2 == 0.3)       // false; its 0.30000000000000004; same issue as in C, C++, Java, etc

let hex = 0xffFF
let binary = 0b101
let octal = 0o127

// base conversions
let num = 255
num.toString(16)  // ff
num.toString(2)   // 11111111

123456..toString(2)		// notice 2 dots to call a method on a number literal

// rounding
Math.round()
Math.floor()
Math.ceil()
Math.trunc()

alert(1e500)	// Infinity

// isNaN() and isFinite()
isNaN(NaN)		// true
isNaN("str") 	// true
isFinite("12")	// true
isFinite(Infinity)	// false
isFinite("str")	// false; its NaN

// Object.is(a, b) is the same as a === b, but it does comparison with NaN as well.

// parseInt(n, radix) and parseFloat() output a number
// they start reading from left and parse until they can't anymore
parseInt('100px')		// 100
parseFloat('12.5em')	// 12.5
parseFloat('12.5.7')	// 12.5 (stops at second point)
parseInt('a123')		// NaN

// other math functions
Math.random()
Math.power(n, power)
Math.min(a, b, c...)
Math.max(a, b, c...)

String

// backticks allow multi-line strings in source code
let str1 = "Hello\nWorld"
let str2 = `Hello
World`
alert(str1 == str2)		// true

// string length property
let str = "my\n"
alert(str.length)	// 3

// access characters
let str = `Hello`
str[0] 		// H
str.at(0) 	// H
str[str.length - 1]	 // o
str.at(-1)	// o
str[-1]		// undefined

// for...of loop 
for (let char of "Hello") {
	alert(char) 	// H,e,l,l,o
}

// strings are immutable in JS
let str = 'Hi'
str[0] = 'h'	// error

// changing case
let strCaps = str.toLowerCase()
let strSmall =str.toUpperCase()
let charCaps = str[1].toUpperCase()

/* string functions

str.indexOf(substr, [pos])
str.lastIndexOf(substr, [pos])

str.includes(substr, [pos])

str.startsWith(substr)
str.endsWith(substr)

str.slice(start, [end])
str.substring(start, [end])
str.substr(start, [length])

str.localeCompare(str1)

str.trim()

*/

Array

// arrays need not be homogeneous in JS
let arr = ["foo", 8, true, { name: 'John' }, function(){ alert('hello') }]
let arr2 = new Array('abc', 'def', 'ghi')		// not recommended

// accessing elements
arr[1]
arr.at(1)
arr.at(-1)		// negative indices are only allowed in at() method
arr[4]()		// run function at index 4 and run it

// length property
arr.length

// be careful with it as it returns one more than the last element's index
let arr = []
arr[123] = 'foo'
arr.length 		// 124

// methods
arr.push('foo', 'bar')			// insert at right (faster)
arr.pop()						// remove from right (faster)
arr.unshift('alice', 'bob')		// insert from left
arr.shift()						// remove at left

// arrays get copied by reference
let fruits = ["Banana"]
let arr = fruits
arr === fruits		// true

// Internals - arrays are objects internally, so technically we can add random properties to it, use delete, for...in loops, but its not recommended
arr['foo'] = 5
arr[0] = 1
arr[99999] = 2
delete arr[2]

// Loops - use simple for or for...of loops. Although for...in can be used but not recommended
for(let element of arr){
	alert(element)
}

// arrays only implement toString() and not toValue() or [Symbol.toPrimitive] functions
let arr = [1, 2, 3]
alert(arr) 	// 1,2,3
alert(String(arr) === '1,2,3') 	// true

// as a result they get converted to string with + operator
alert([] + 1) 	 	// "1"
alert([1] + 1)	 	// "11"
alert([1,2] + 1)	// "1,21

// don't compare arrays with == operator, since it will compare reference and not the contents
// use for loop and manually compare each element
alert([] == []) 	// false
alert([0] == [0]) 	// false


/*

splice(pos, deleteCount, ...items)
slice(start, end)
concat(...items)

indexOf/lastIndexOf(item, pos) 
includes(value)
find/filter(func)
findIndex(func)

forEach(func)

map(func)
sort(func) 
reverse()
split/join(str)
reduce/reduceRight(func, initial)

NOTE: sort, reverse and splice modify the array in-place, rest do not.

arr.some(func)
arr.every(func)
arr.fill(value, start, end)

*/

// the func in above functions are of the format
function(item, index, array) {
  	// returns the new value instead of item
}

// most functions like find, filter, map, etc.. have an optional last argument "thisArg", 
// in which object like "this" can passed as the argument to func to pass context, otherwise its undefined
arr.filter(canJoin, this)

function canJoin(user){
	// logic here
}

// check if object is of array
Array.isArray(obj)

Iterables

// array and string are iterables, objects are not by default
// for...of loops only work on iterables

// any object that has [Symbol.iterator] symbol function is an iterable
// the function for symbol returns an object which has a next() function and "current" and "last" properties

// call an iterator explicitly (rarely needed)
let str = "Hello"
let iterator = str[Symbol.iterator]()
while (true) {
  let result = iterator.next()
  if (result.done) break
  alert(result.value) 		// H,e,l,l,o
}

// Iterables and array-likes:
// Iterables - objects that implement the Symbol.iterator method, as described above
// Array-likes - objects that have indexes and length, so they look like arrays

//  make a real Array from an Iterable or Array-like obj, and we can then use array methods on it
let arr = Array.from(obj, [mapFunc, thisArg])

Map and Set

// maps are objects but keys can be anything and not only string or symbols
let mp = new Map()

map.set(1, 'one')	// a numeric key
map.set('two', 2)	// a string key
map.set(user, 'foo')	// an object key

// set can be chained in JS as it returns a map
map.set('1', 'str1')
  .set(1, 'num1')
  .set(true, 'bool1')

map.set(key, value)
map.get(key)
map.has(key)
map.delete(key)
map.clear()
map.size

// don't do map[key] on a Map; don't treat is as a normal object 

// iterabte over map using for..of loop on the following methods that return iterables
// iteration order is insertion order 
map.keys()
map.values()
map.entries()

// convert to/from object
let map = Object.entries(obj)
let obj = Object.fromEntries(map)

// Sets
let set = new Set()
set.add(value)
set.delete(value)
set.has(value)
set.clear()
set.size

set.keys()
set.values()
set.entries() 

// iteration order is insertion order unlike Java's HashSet
// sets are maps internally hence set.keys() and set.values() both return the keys iterable!

WeakMap and WeakSet

// Note on Garbage Collection: if an object is part of an object, array, map, or set, then it won't be deleted by JS engine even if the reference to it is removed
let user = { name: "Alice" }
let arr = [ user ]
user = null 		// reference removed
// object user won't be garbage collected till reference of arr is present

// WeakMap and WeakSet automatically delete the object from Map/Set (and memory) if the object is referenced only inside them

// WeakMap and WeakSet are diff from their normal impl in the way that Weak counterparts can only store objects
let weakMap = new WeakMap()
WeakMap.set(obj, 'ok')
WeakMap.set('foo', 'bar')	// Error

// WeakMap and WeakSet do not support iteration and methods keys(), values(), entries() so there is no way to access all the elements

More on Objects

// all objects can be used with Object.keys(), Object.values(), Object.entries() methods, even if they have their own obj.entries() methods
let user = {
  name: "John",
  age: 30
}

Object.keys(user)     // ["name", "age"]
Object.values(user)   // ["John", 30]
Object.entries(user)  // [ ["name","John"], ["age",30] ]

// they all ignore symbolic properties ofcourse

Object.entries(obj)       // gets an array of key/value pairs from obj
Object.fromEntries(arr)   // convert 2-D pairs array into an object

// Destructuring Assignment: store arrays or objects into individual variables
let arr = [1, 2, 3]
let [x, y, z] = arr

let obj = { name: 'Alice', age: 30 }
let { age, name } = obj     // order doesn't matter for correct mapping, JS handles it implicitly

// general syntax
let [item1 = defaultValue, item2, ...rest] = array
let {prop : varName = defaultValue, ...rest} = object

// extra elements in array or unmapped props in object are ignored, if we try to map to some prop which doesn't exist in object, then it is assigned "undefined" 

// works flawlessly with nested objects and arrays too

// Trick: swap variables using destructuring assignments
let guest = "Alice"
let admin = "Bob"
[guest, admin] = [admin, guest]
// here we create a temporary array of two variables and immediately destructure it in swapped order

Date and Time

let now = new Date()
alert(now)    // Sun Dec 29 2024 15:18:10 GMT+0530 (India Standard Time)

// micro-benchmarking: benchmarking language features (not much meaningful thoughlet date1 = new Date(0);
let start = new Date(0);
let end = new Date();
end - start     // date-number conversion, then subtraction

end.getTime() - start.getTime()   // much faster; no date-number conversion is done

// below is the same as new Date().getTime() but no object is created; no Garbage Collection hassles
let start = Date.now()    
let end = Date.now()

JSON

JavaScript Object Notation was initially created for JS but now its a standalone specification for data exchange.

JSON is represented in JS as strings.

// JSON static class methods
let json = JSON.parse(str)      // convert json string to object
let str = JSON.stringify(json)  // convert object to json string

// JSON must be valid for parsing especially with valid double-quotes ("")

/*

JSON.stringify() skips the following properties: 
- function properties (methods)
- symbolic keys and their values
- properties that store "undefined"

There must be no circular references in nested properties, otherwise error.

*/

// general syntax
let json = JSON.stringify(value[, replacer, space])
let value = JSON.parse(str[, reviver])

// we an implement a custom toJSON() method - JSON.stringify automatically calls it if available