- Published on
Notes on JavaScript
- Authors
- Name
- Rakesh Tembhurne
- @tembhurnerakesh
JavaScript Introduction
- Has its own specification - ECMAScript
- JS Engine Codenames
- V8 - Chrome & Opera
- Spider Monkey - Firefox
- Chakra - IE
- Nitro & SquirrelFish - Safari
- How JS Engine Works
- Parses the script
- Compiles to machine code
- Runs machine code
- Safety & Abilities
- It's safe: no low-level access.
- Abilities in the browser are limited for user's safety.
- No direct access to OS functions.
- JS on one page has no access to other pages.
- Ability to access data from other sites is crippled.
- Above limits do not exist outside the browser.
- Transpilers
- CoffeeScript
- TypeScript
- Flow
- Dart
- Brython
- Kotlin
- The ECMA-262 Specification
Fundamentals
Script tag can be inserted anywhere.
HTML4
<script>
tag must havetype
attribute.HTML5 does not require
type
orlanguage
attributes.If
src
is set, the script contents are ignored.Automatic semicolon insertion: line break is interpreted as implicit semicolon.
JS does not assume semicolon before a square bracket
;[]
.It is recommended to use semicolons.
Nested comments are not supported inside
/* */
.After 2009, ES5 added new features. To enable them, had to use
"use strict"
for modern behavior."strict mode"
must be at the top, or it won't be enabled.Browser developer consoles do not enable
"strict mode"
by default.Reliable way to use
"strict mode"
is to use it inside a wrapper:;(() => { 'use strict' // other code })()
No need to explicitly write
"use strict"
if we use modern JS like classes or modules.Use
"use strict"
until you are coding in all classes and modules.
Variables
- It's a named storage for data.
- Types
let
var
const
- Declaring twice triggers an error (except for
var
). - Name must contain only letters, digits, or the symbols
$
and_
. - The first character for the name must not be a digit.
- Case (uppercase/lowercase) matters.
- Non-Latin letters are allowed, but not recommended.
- It's an international convention to use English in variable names.
- When using
"use strict"
,let
/var
/const
is required. - Can't reassign a
const
. - Constants known prior to execution are
NAMED_WITH_CAPS_N_UNDERSCORES
. - Capital named constants are only used as aliases for "hard-coded" values.
- A variable name should have a clean, obvious meaning, describing the data that it stores.
- An extra variable is good, not evil.
Data Types
- Primitive Types
- Number
- Integers
- Floats
Infinity
-Infinity
NaN
- Represents computational error. It's a result of incorrect or an undefined mathematical operation.
NaN
is sticky. Any further operation onNaN
returnsNaN
.
- BigInt
- The
"n"
at the end means it's a BigInt. - Rarely needed.
- BigInt is supported in Chrome/Firefox/Edge/Safari but not in IE.
- The
- String
- Three types:
- Double quotes (Simple quotes)
- Single quotes (Simple quotes)
- Back-ticks (Extended functionality quotes)
- The expression inside
${...}
is evaluated and the result becomes a part of the string. - There are no character types (like in C++ or Java, a single character is also a string of length 1).
- Three types:
- Boolean
True
False
- The
"null"
value- Represents
"nothing"
,"empty"
or"value unknown"
.
- Represents
- The
"undefined"
value- Its meaning is –
"value is not assigned"
.
- Its meaning is –
- Symbols
- Used to create unique identities for Objects.
- Number
- Special Type
- Objects
- Objects are used to store collections of data and more complex entities.
- Objects
typeof
operator
The - Can be used like:
typeof x
- Can be used like:
typeof(x)
- Returns a lowercase string with the name of the type, like
"string"
. - For
null
returns"object"
(known error in JS).
Type Conversion
alert
automatically converts any value to a string to show it.- Mathematical operations convert values to numbers.
- String conversion happens when we need a string form of a value.
- Numeric conversion happens in mathematical functions and expressions automatically.
alert("5" / "2")
// converts to a number
- If a string is not a valid number, the result of such conversion is
NaN
(e.g.,Number("some string")
=NaN
).
Value | Number(x) Becomes |
---|---|
undefined | NaN |
null | 0 |
true | 1 |
false | 0 |
string | First trimmed, if empty then 0 , else number. Error gives NaN . |
alert(Number("123"))
//123
alert(Number("123z"))
//NaN
- Boolean conversion: Values that are intuitively
"empty"
, like0
, an empty string,null
,undefined
, andNaN
, becomefalse
. - In JS, non-empty strings are always
true
-Boolean("0")
=true
=Boolean(" ")
. - Boolean of
0
,null
,undefined
,NaN
,""
=>false
. Anything else =true
.
+
String Concatenation with binary - Usually, the
+
operation sums numbers. - But if the binary
+
is applied to strings, it merges (concatenates) them. - If any of the operands is a string, then the other one is converted to a string too.
- It doesn't matter whether the first operand is a string or the second one.
- Operators work one after another (e.g.,
2 + 2 + "1"
="41"
). - Other arithmetic operators work only with numbers and ALWAYS convert their operands to numbers.
- If the operand is not a number, the unary plus converts it to a number.
- Unary plus (
+x
) works similar toNumber(x)
. - Unary plus is applied first; they convert strings to numbers, and then the binary plus sums them up.
Assignments
Chained assignments evaluate from right to left.
Modify-and-assign operators exist for all arithmetical and bitwise operators (
/=
,-=
, etc).Increment/decrement can only be applied to variables. Trying to use it on a value like
5++
will give an error.++
or--
operators can be placed either before or after a variable.- When the operator goes after the variable, it is in postfix form.
- Similarly, prefix form.
- The prefix value returns the new value, while postfix form returns the old value.
The comma operator is one of the rarest and most unusual operators.
let a = (1 + 2, 3 + 4) alert(a) // Shows 7. (1+2) is calculated and result thrown away, then (3+4) is calculated and returned.
- The first expression
1+2
is calculated, and its result is thrown away. - Then
3+4
is calculated, and its result is returned as the result.
- The first expression
The comma has a very low precedence, lower than
=
.
Comparisons
- JS uses so-called dictionary or
"lexicographical"
order for string comparison. - String comparison uses Unicode order, not real dictionary order.
- When comparing values of different types, JS converts the values to numbers.
- A regular equality check
==
has a problem: it cannot differentiate0
fromfalse
. - A strict equality operation
===
checks the equality without type conversion.alert(null === undefined);
//false
alert(null == undefined);
//true
- For math and other comparisons (
>
,<
,<=
,>=
),null
/undefined
are converted to numbers:null
becomes0
, whileundefined
becomesNaN
. - Strange result
null
vs0
null > 0;
//false
null == 0;
//false
null >= 0;
//true
- Comparison converts
null
to a number, treating it as0
.
undefined
Incomparable undefined > 0;
//false
undefined < 0;
//false
undefined == 0;
//false
undefined
gets converted toNaN
, andNaN
is a special numeric value which returnsfalse
for all comparisons.- The equality check (
undefined == 0
) returnsfalse
becauseundefined
only equalsnull
,undefined
and no other value. - Don't use comparison with a variable which may be
null
/undefined
. - When values of different types are compared, they get converted to numbers (for
==
and not for===
). - Be careful when using comparisons like
>
or<
with variables that can occasionally benull
/undefined
. - Checking for
null
/undefined
separately is a good idea. - The conditional operator
?
has low precedence, so it executes after the comparison>
.
Logical Operators
- Four in number:
||
(OR)&&
(AND)!
(NOT)??
(Nullish Coalescing)
- If an operand is not a Boolean, it's converted to a boolean for the evaluation.
OR (||)
finds the first truthy value.OR (||)
operands convert to boolean; if the result is true, stops and returns the original value of that operand.- Short-circuit evaluation (
xx || yy || zz
) returns the first truthy value or the last one if no truthy values. AND (&&)
finds the first falsey value.AND (&&)
: For each operand, converts it to a boolean. If the result is false, stops and returns the original value of that operand.- If all operands have been evaluated, returns the last operand.
AND
returns the first falsey value or the last value if none were found.- Precedence of
AND (&&)
is higher thanOR (||)
. - Don't replace
if
with||
or&&
. - A double
NOT (!!)
is sometimes used to convert a value to boolean. a ?? b
- If
a
is defined, thena
. - If
a
isn't defined, thenb
.
- If
??
treatsnull
andundefined
similarly.??
Returns the first argument if it's notnull
/undefined
. Otherwise, the second one.- The important difference between
??
And||
is:||
returns the first truthy value.??
Returns the first defined value.
- The precedence of the
??
(5) Operator is about the same as||
(6), just a bit lower. - Just like
||
,??
is evaluated before=
and?
, but after most operations such as+
,*
.
let area = (height ?? 100) * (width ?? 50)
- It is forbidden to use
??
with||
and&&
without explicit parenthesis. ??
Provides a short way to choose the first "defined" value from a list.
Loops
Three types:
while
- The condition is checked before each iteration.do...while
- The condition is checked after each iteration.for (;;)
- The condition is checked before each iteration, additional settings are available.
Curly braces are not required for a single-line body.
do...while
loop is used to execute at least once.for (begin; condition; step) { body }
Working of
for
loop- (If condition)
run body
andrun step
- (If condition)
run body
andrun step
- (If condition)
run body
andrun step
- Any part of the
for
can be skipped.
- (If condition)
In
for
, two semicolons must be present, otherwise it's an error.We can force the exit at any time, using the special
break
directive.The
continue
is a lighter version ofbreak
.Use no
break
/continue
to the right side of?
.To break out of multiple nested loops, we need to use
break <labelName>
.labelName: for (;;) { // ... break labelName }
continue
directive can also be used with a label.Labels do not allow to
"jump"
anywhere.A
break
directive must be inside a code block. Technically, any labelled code block will do.label: { some code; break label; }
Although 99.9% of the time
break
is used inside loops.A
continue
is only possible from inside a loop.
"switch"
statement
The switch(x) { ... cases ... }
- The value of
x
is checked for strict equality. - If the equality is found,
switch
starts to execute the code starting from the correspondingcase
until the nearestbreak
. - If there is no
break
, then the execution continues with the nextcase
without any checks. - Any expression can be a
switch
/case
argument. - Several variants of
case
which share the same code can be grouped. - Equality check is always strict.
Function
- Functions are the main
"building blocks"
of the program. - Variables declared outside of any function are called global.
- It's a good practice to minimize the use of global variables.
- A default parameter is evaluated every time the function is called without the respective parameter.
- When the execution reaches the
"return"
directive, the function stops. - A function with an empty return or without it, returns
undefined
. - Never add a newline between
return
and the value. - If we want the returned expression to wrap across multiple lines, we should start it at the same line as
return
. Or at least put the opening parenthesis there. - It's a widespread practice to start a function name with a verbal prefix.
- A function should do exactly what is suggested by its name, no more (one function = one action).
- A function may access outer variables. But it works only from inside out. The code outside of the function doesn't see its local variables.
- A function can return a value. If it doesn't, then its result is
undefined
. - A semicolon is recommended at the end of statements, no matter what the value is.
- Function expression vs function declaration
- A more subtle difference is when a function is created by the JS Engine.
- A function expression is created when the execution reaches it, and it is usable only from that moment.
- A function Declaration can be called earlier than it is defined.
- In strict mode, when a Function Declaration is within a code block, it's visible everywhere inside that block, but not outside of it.
- Declaration syntax gives more freedom in how to organize our code.
- Function Expressions are created when the execution flow reaches them.
- Function Declaration is preferable.
Objects
There are eight data types in JS. Seven of them are called primitives.
Two syntaxes:
let user = new Object();
// "object constructor" syntaxlet user = {};
// "object literal" syntax
Trailing or hanging comma is good.
Square brackets like
user["likes birds"] = true;
We can use square brackets in object literals:
let bag = { [fruit + 'computers']: 5, }
Square brackets are much more powerful than dot notation.
Language restricted words such as
"let"
,"for"
,"return"
can be used for an object property; there is no restriction.Object properties can be any strings or symbols.
Other types are automatically converted into strings.
A number
0
becomes a string"0"
when used as a property key:obj[0] === obj["0"]
.The property
__proto__
cannot be set to a non-object.Reading a non-existing property just returns
undefined
.Situations like when
obj.prop = undefined
, it works right.A special operator
"in"
is used to check if a key exists in an object ("key" in object
).Left side of the
"in"
must be a property name.To walk over all keys of an object, there exists a special form of the loop:
for...in
.for (let prop in object) { ... }
Object properties are ordered in a special fashion: Integer properties are sorted, others appear in creation order.
"Integer Properties"
means a string that can be converted to and from an integer without change."49"
is an integer key but not"+49"
or"1.2"
.To delete a property:
delete obj.prop
.Objects are stored and copied
"by reference"
, whereas primitive values (string, numbers, booleans, etc.) are always copied"as a whole value"
.A variable assigned to an object stores not the object itself, but its
"address in memory"
– in other words,"a reference"
to it.When an object variable is copied, the reference is copied, but the object itself is not duplicated.
Object.assign
Cloning and Merging, Copying an object variable creates one more reference to the same object.
There's no built-in method for deep cloning in JS.
We need to create a new object and replicate the structure of the existing one by iterating over its properties and copying them on the primitive level.
for (let key in obj) { clone[key] = user[key] }
Object.assign(dest, [src1, src2, src3, ...])
Object.assign
can be used to:- Modify an existing object.
- Create a new Object.
If the copied property name already exists, it gets overwritten.
let clone = Object.assign({}, user)
A third way is to use spread syntax:
let clone = { ...user };
Object.assign()
creates a shallow copy; for a deep copy, we need to use a library like_.cloneDeep(obj)
.
Garbage collection
- The main concept of memory management in JavaScript is reachability.
- Reachable values are those that are accessible or usable somehow. They are guaranteed to be stored in memory.
- Outgoing references do not matter. Only incoming ones can make an object reachable.
- The basic garbage collection algorithm is called mark-and-sweep.
- The garbage collector tries to run only when the CPU is idle, to reduce the possible effect on execution.
- Garbage collection is performed automatically. We cannot force or prevent it.
- Being referenced is not the same as being reachable from root.
this
Object methods, - A function that is a property of an object is called its method.
- We can omit
"function"
and can just writesayHi()
inside the object. - There are subtle differences related to object inheritance with respect to how you write a function name.
- The value of
this
is evaluated during runtime. - Calling without an object,
this === undefined
. - In non-strict mode, the value of
this
in such case will be the global object -function() { alert(this); }
. - In JS,
this
is free; its value is evaluated at call-time and does not depend on where the method was declared, but rather on what object is"before the dot"
. - Arrow functions have no
this
. - Arrow functions are special. They have no
this
. Whenthis
is accessed inside an arrow function, it is taken from outside (lexicalthis
).
Constructors
Constructor functions technically are regular functions.
Two conventions:
- Named with a Capital letter first.
- Should be executed only with the
"new"
operator.
When a function is executed with
"new"
:- A new empty object is created and assigned to
this
. - The function body executes. Usually, it modifies
this
, adds new properties to it. - The value of
this
is returned.
- A new empty object is created and assigned to
Inside a function, we can check whether it was called with
new
or without it, using a specialnew.target
property.function User() { alert(new.target) } User() // undefined (called without new) new User() // function User {...} (called with new)
You can add a check inside a construction function with
if (!new.target) return new User(name);
.This approach is sometimes used in libraries to make the syntax more flexible, so that people may call the function with or without
new
, and it still works.Usually, constructors do not have
return
statements.If there are
return
statements:- If
return
is called with an object, then that object is returned instead ofthis
. - If
return
is called with a primitive, it is ignored.
- If
In other words,
return
with an object returns that object. In all other cases,this
is ignored.We can omit parentheses after
new
, if it has no arguments (new User;
), but it is not considered good practice.A constructor function is a regular function, but there is a common agreement to name it with a Capital Letter first.
A constructor function should only be called with
new
. Such a call implies creation ofthis
at the start and returning the populated one at the end.
Optional Chaining
- Optional chaining
?.
is a safe way to access nested object properties, even if an intermediate property doesn't exist. - If
user.address
isundefined
, an attempt to getuser.address.street
fails with an Error. - Eg:
user?.address
,user?.address?.street
. - The
?.
syntax makes optional the value before it, but not any further. - The variable before
?.
must be declared. user?.sayHi(x++)
(Thex++
part executes before the check ofsayHi
method).- Other variations:
?.()
,?.[]
.- Eg,
userAdmin.isAdmin?.()
//isAdmin()
function is called if it exists. - Eg,
user?.[key]
// access property if it exists.
- Eg,
delete user?.name
// deletesuser.name
if it exists.- We can use
?.
for safe reading and deleting, but not for writing. ?.
obj?.prop
- returnsobj.prop
ifobj
exists, otherwiseundefined
.obj?.[prop]
- returnsobj[prop]
ifobj
exists, otherwiseundefined
.obj.method?.()
– callsobj.method()
ifobj.method
exists, otherwise returnsundefined
.
Symbol Type
- Symbol is a primitive type for unique identifiers.
- A
"symbol"
represents a unique identifier (let id = Symbol("id");
). - Symbols are guaranteed to be unique.
- The description is a label; it doesn't affect anything.
- Symbols are special. Symbols don't auto-convert to strings.
alert(id)
// error
- Strings and symbols are fundamentally different and should not accidentally convert one into another.
symbol.description
is used to show the description.- Symbols are skipped by
for...in
loop. Object.keys(user)
also ignores symbols.Object.assign()
copies both string and symbol properties. That's by design.- There is a global symbol registry. We can create symbols in it and access them later, and it guarantees that related accesses by the same name return exactly the same symbol -
Symbol.for(key)
. - Symbols inside the global registry are called global symbols.
Symbol.for(key)
returns a symbol by name, but there's a reversal call:Symbol.keyFor(sym)
.- If a symbol is not global,
Symbol.keyFor
won't be able to find it and returnsundefined
. - If we want to add a property into an object that belongs to another script or a library, we can create a symbol and use it as a property key.
- Technically, symbols are not 100% hidden. There is a built-in method
Object.getOwnPropertySymbols(obj)
that returns all keys of an object including symbolic ones.
Object to Primitive conversion
- Objects are auto-converted to primitives, and then the operation is carried out.
- All objects are
true
in a boolean context. - There are only numeric and string conversions for objects to primitives.
ToPrimitive(input, preferredType)
- Hint:
"string"
- Hint:
"number"
- Hint:
"default"
- Hint:
- The greater and less comparison operators, such as
< >
, can work with both strings and numbers too. Still, they use the"number"
hint and not"default"
hint. That's for historical reasons. - There is no
"boolean"
hint. - Conversion steps:
- Call
obj[Symbol.toPrimitive](hint)
– the method with the symbolic keySymbol.toPrimitive
. - Otherwise, if hint is
"string"
, tryobj.toString()
andobj.valueOf()
, whatever exists. - Otherwise, if hint is
"number"
, or"default"
, tryobj.valueOf()
andobj.toString()
, whatever exists.
- Call
obj[Symbol.toPrimitive] = function(hint) { ... // must return primitive value }
- If
toString
orvalueOf
returns an object, then it is ignored, as if there were no methods. - By default:
- The
toString
method returns a string"[object Object]"
. - The
valueOf
method returns the object itself.
- The
user.valueOf() === user
- There is no control whether
toString
returns exactly a string, or whetherSymbol.toPrimitive
method returns a number for a hint"number"
. - The only mandatory thing: these methods must return a primitive, not an object.
- For historical reasons:
- If
toString
orvalueOf
returns an object, there is no error, but such values are ignored. Symbol.toPrimitive
must return a primitive, otherwise there will be an error.
- If
Data Types (cont.)
- There are 7 primitive types: string, number, bigint, boolean, symbol, null and undefined.
- Objects are
"heavier"
than primitives. - Primitive as an Object: A special
"object wrapper"
that provides the extra functionality is created and then is destroyed. str.toUpperCase()
:- The string
str
is a primitive. So, at the moment of accessing its property, a special object is created that knows the value of the string, and has useful methods, liketoUpperCase()
. - That method runs and returns a new string.
- The special object is destroyed, leaving the primitive
str
alone.
- The string
- Constructors
String
/Number
/Boolean
are for internal use only. new Number
ornew Boolean(false)
is possible for historic reasons, but not recommended.- Things will go crazy at several places if we use
new
before primitives.let zero = new Number(0);
(zero === true
inif
condition, because objects are always truthy).
Null
/undefined
have no methods.
Numbers
- Regular numbers are stored in 64-bit IEEE-754 format - also known as
"double precision floating point numbers"
. - Regular numbers can't exceed
2**53
to-2**53
, so BigInt is used. - We can use
_
as a number separator like:let billion = 1_000_000_000
. _
is only a syntactic sugar.- Numbers can be shortened by appending
"e"
-1e0
(1 billion),7.3e9
(7.3 billion). let ms = 1e-6
(six zeros to the left of 1).- Hex, binary, and octal numbers are used with their respective prefixes:
let a = 0xFF;
//255
let b = 0b1111111;
//255
let c = 0o377;
//255
num.toString(base)
-base
can be from 2 to 36, default is 10.alert(123456..toString(36));
//2n9c
. If we want to call a method directly on a number, two dots are used.
num.toFixed()
method returns a string of number. We can prepend+
if we want a number:+num.toFixed(2)
.- Numbers stored in memory in their binary form, a sequence of bits. So
0.1 + 0.2 == 0.3
isfalse
. - In decimal, division by 10 guarantees a result, but not division by 3. Similarly, in binary, division by 2 works, but not by 10.
- These errors happen in all languages.
- The multiply-divide approach reduces the error, but doesn't remove it totally.
- Out of 64 bits, 52 bits can be used to store digits.
- In JS, there exist two zeroes,
0
and-0
, that's because the sign is represented by a single bit, so it can be set or not set for any number including zero. Infinity
(and-Infinity
) is a special numeric value that is greater (less) than anything.NaN
represents an error.isNaN(value)
converts its arguments to a number and then tests it for beingNaN
.- They belong to type
Number
, but are not"normal"
numbers.alert(isNaN(NaN));
//true
alert(isNaN("str"));
//true
- We cannot use
=== NaN
comparator. The valueNaN
is unique in that it does not equal to anything, including itself.alert(NaN === NaN);
//false
isFinite(value)
converts its arguments to a number and returnstrue
if it's a regular number, notNaN
/Infinity
/-Infinity
.isFinite
is used to validate whether a string value is a regular number.- An empty or space-only string is treated as
0
in all numeric functions, includingisFinite()
. Object.is(value, anotherValue)
compares value like===
.- It works with
NaN
.Object.is(NaN, NaN) === true
. - Values
0
and-0
are different.Object.is(0, -0) === false
. Object.is(a, b)
is same asa === b
.
- It works with
- Numeric conversion using unary
+
orNumber()
is strict. If value is not exactly a number, it fails.alert(+"100px");
//NaN
- In the above situation, spaces at beginning or ending are ignored.
- In the above situation,
parseInt()
andparseFloat()
functions should be used. - They
"read"
a number from a string, until they can't. In case of an error, the gathered number is returned. - There are situations when
parseInt
/parseFloat
will returnNaN
. It happens when no digits could be read. - The second argument of
parseInt(str, radix)
can be any base between 2 and 36. - Result is always in decimal.
Math.random()
returns a number from 0 to 1, excluding 1.Math.max(a, b, c, ...)
/Math.min(a, b, c, ...)
returns the greatest or smallest number from a list of arguments.
Strings
- Single and double quotes are essentially the same.
- Backticks, however, allow us to embed any expression into a string, by wrapping it in
${...}
. - Another advantage of backticks is that they allow a string to span multiple lines.
- Backticks also allow us to specify a
"template function"
before the first backtick. The syntax isfunc
`string`. This is called
"tagged templates"`. - The
length
property has the string length. - The
\n
is a single"special"
character, so"My\n".length
===3
. str.length
is a numeric property, not a function.- A character from the string can be obtained by
theString.charAt(0)
(gets first char) ortheString[theString.length - 1]
(gets last char). - Square brackets are the modern way of getting a character;
charAt()
exists mostly for historic reasons. []
returnsundefined
andcharAt
returns an empty string, if no character is found.- We can also use
for...of
loop on a string to access characters. - Strings can't be changed in JS. So
str[0] = 'h'
will throw an error. toUpperCase()
andtoLowerCase()
functions can be used on strings, as well as on characters liketheString[0].toUpperCase()
.str.indexOf(substr, pos)
looks forsubstr
instr
, starting from the given positionpos
, and returns the position where the match was found, or-1
if nothing can be found.- The optional second parameter allows us to start searching from a given position.
str.lastIndexOf(substr, pos)
searches from the end to the beginning.- The bitwise NOT trick (
~1
) is sometimes used at some places for the replacement ofstr.lastIndexOf("text") != -1
. - In practice, that means a simple thing: for 32-bit integers,
~n
equals-(n+1)
. ~n
is zero only ifn === -1
.- So, the test
if (~str.indexOf("..."))
is truthy only if the result ofindexOf
is not-1
. - Above is not recommended to use in a non-obvious way.
- Just remember
if (~str.indexOf(...))
reads as"if found"
. - Big numbers are truncated to 32 bits by
~
operator, so use it on a string which is smaller. str.includes()
,str.startsWith()
,str.endsWith()
returnstrue
orfalse
.
Getting a substring
str.slice(start [, end])
- Returns the part of string from
start
, but not includingend
. - If there is no second argument, then slice goes till the end of the string.
- Negative values of
start
,end
are possible. It means counted from the end.
- Returns the part of string from
str.substring(start [, end])
- Returns the part of string between
start
andend
. - Almost similar to
slice
, but allowsstart
to be greater thanend
. - Negative arguments are not supported; they are considered to be as
0
.
- Returns the part of string between
str.substr(start [, length])
- Returns the part of the string from
start
, with the givenlength
. - The first argument can be negative, to count from the end.
- Returns the part of the string from
method | Selects... | negatives |
---|---|---|
slice(start, end) | From start to end (excluding end) | Allows negatives |
substring(start, end) | Between start and end | Negative values means 0 |
substr(start, length) | From start get length characters | Allows negative start |
- It's enough to remember solely
slice
of these three methods.
Comparing Strings
- A lowercase letter is always greater than uppercase.
- Letters with diacritical marks are
"out of order"
; may give strange results. - All strings are encoded using UTF-16.
str.codePointAt(pos)
returns the code for the character at positionpos
.String.fromCodePoint(code)
- creates a character by its numeric code.- We can add Unicode characters by their code using
\u
followed by the hex code. - The characters are compared by their numeric codes.
- The code for
a
(97) is greater than the code forZ
(90). - The call
str.localeCompare(str2)
returns an integer indicating whetherstr
is less, or equal, or greater thanstr2
according to the language rules:- Returns a negative number if
str
is less thanstr2
. - Returns a positive number if
str
is greater thanstr2
. - Returns
0
if they are equivalent.
- Returns a negative number if
- Surrogate Pair
- All frequently used characters have 2-byte codes.
- But 2 bytes only allow 65,536 combinations, and that's not enough for every possible symbol.
- We actually have a single symbol in each of the string (like 👶🏽👩🏽💻), but the length shows a length of 2.
String.fromCodePoint
andstr.codePointAt
methods deal with surrogate pairs correctly.- Above methods are actually the same as
fromCodePoint
/codePointAt
, but don't work with surrogate pairs. - To support arbitrary compositions, UTF-16 allows us to use several Unicode characters: the base character followed by one or many
"mark"
characters that"decorate"
it.alert('S\u0307\u0323');
//S
with dot above symbol
str.normalize()
actually brings together a sequence of three characters to one.str.trim()
trims the spaces around.str.repeat(n)
repeats the stringn
times.
Arrays
- Arrays can store elements of any type.
- The
"trailing comma"
style makes it easier to insert/remove items, because all lines become alike. - Arrays as queue
push
adds an element to the end.shift
gets an element from the beginning, advancing the queue, so that the second element becomes the first.shift <== array <== push
.
- There is another use case for Arrays – the data structure named a stack.
push
adds an element to the end.pop
takes an element from the end.
- For stacks, the latest pushed item is received first; that's also called as LIFO (last in, first out) principle. For Queues, we have FIFO (first in, first out).
- In computer science, the data structure that allows this is called a
deque
(double-ended queue). fruits.push(...)
is equal tofruits[fruits.length] = ...
.Push
andshift
can add multiple items at once.- An Array is a special kind of object, and thus behaves like an object.
- Think of arrays as special structures to work with ordered data. They provide special methods for that.
- Methods
push
/pop
run fast, whileshift
/unshift
are slow. - The
shift
operation does 3 things:- Remove the element with index 0.
- Move all elements to the left, renumber them from index 1 to 0, from 2 to 1, and so on.
- Update the
length
property.
- The
pop
method does not need to move anything, because other elements keep their indexes. That's why it's blazingly fast. - One can use
for
loop, but there is also a specialfor...of
loop. for...of
doesn't give access to the number of the current element, just its value, but in most cases that's enough and that's shorter.- The loop
for...in
iterates over all properties, not only the numeric ones. - The
for...in
loop is optimized for generic objects, not arrays, and thus 10-100 times slower. - Generally, we shouldn't use
for...in
for arrays. - In arrays,
length
property is writable. The simplest way to clear the array isarr.length = 0
. - If
new Array
is called with a single argument which is a number, then it creates an array without items, but with the given length. toString()
method returns a comma-separated list of elements (without space).- Don't compare arrays with
==
.- Two objects are equal (
==
) only if they are references to the same object. - If one of the arguments of
==
is an object, and the other one is primitive, then the object gets converted into primitive. - With an exception of
null
andundefined
that equal==
each other and nothing else. - They are never the same, unless we compare two variables that reference exactly the same array.
- Two objects are equal (
- Some comparisons:
[] == [];
//false
[0] == [0];
//false
0 == [];
//true
'0' == [];
//true
- To compare arrays, don't use
==
operator (as well as<
,>
, and others). arr.push(...items)
- adds items to the end.arr.pop()
- extracts an item from the end.arr.shift()
- extracts an item from the beginning.arr.unshift(...items)
- adds items to the beginning.- If any of the middle elements was removed with
delete arr[3]
, thenarr.length
still remains the same. - The
arr.splice
method is a Swiss Army knife for arrays. It can do everything: insert, remove, and replace elements.arr.splice(start, deleteCount, elem1, ..., elemN)
- It modifies
arr
starting from the indexstart
, removesdeleteCount
elements, and then insertselem1
,elem2
, and so on at their place. Returns the array of removed elements.
- The
splice
method is also able to insert the elements without any removals. Just setdeleteCount
to0
. - It's similar to
str.slice
, but instead of substrings, it makes a subarray. - If we call
arr.slice()
method without arguments, then it creates a copy ofarr
. arr.concat
creates a new array that includes values from other arrays and additional items.- Normally, it only copies elements from arrays. Other objects, even if they look like arrays, are added as a whole.
- If an array-like object has a special
Symbol.isConcatSpreadable
property, then it's treated as an array byconcat
. arr.forEach(item, index, array) { }
arr.indexOf(item, from)
- looks foritem
starting fromindex
from
, and returns the index where it was found, otherwise-1
.arr.lastIndexOf(item, from)
- same, but looks forfrom
right to left.arr.includes(item, from)
- looks foritem
starting fromindex
from
, returnstrue
if found.- Above three methods use
===
comparison. - A very minor difference of
includes
is that it correctly handlesNaN
, unlikeindexOf
/lastIndexOf
. arr.find(item, index, array) { }
- The
find
method looks for a single (first) element.
- The
- The
filter
function returns an array of all matching elements. arr.map(item, index, array) { }
arr.sort
- sorts the array in place, changing its element order.- It also returns the sorted array, but the returned value is usually ignored, as
arr
itself is modified. - The items are sorted as strings by default (lexicographic ordering -
"2"
>"15"
).
- It also returns the sorted array, but the returned value is usually ignored, as
arr.sort(fn)
method implements a generic sorting algorithm.arr.sort((a, b) => a - b)
. The comparison function is only required to return a positive number to say"greater"
and a negative number to say"less"
.
- Use
a.localeCompare(b)
for strings. arr.reverse()
reverses the order of the elements ofarr
. It also returns thearr
after the reversal.- The
arr.split()
method has an optional second numeric argument, to limit the length of the returning array. arr.reduce(accumulator, item, index, array) { }
reduce
uses only 2 arguments; that's typically enough.- If there is no initial value, then
reduce
takes the first element of the array as the initial value and starts the iteration from the second element. - But such use requires extreme care. If the array is empty, then
reduce
call without initial values gives an error. - The method
arr.reduceRight
does the same thing, but goes from right to left. Array.isArray(arr)
- checks if it's an array.- Almost all array methods that call functions – like
find
,filter
,map
– with a notable exception ofsort
, accept an optional additional parameterthisArg
. - Methods
sort
,reverse
,splice
modify the array itself. arr.fill(value, start, end)
– fills array with repeatingvalue
fromindex start
toend
.arr.copyWithin(target, start, end)
- copies its elements from positionstart
till positionend
into itself, at positiontarget
(overwrites existing).arr.flat(depth)
/arr.flatMap(fn)
- creates a new flat array from a multidimensional array.
Iterables
Iterable objects are a generalization of arrays. They make any object usable in a
for...of
loop.To make an object iterable, we need to implement
Symbol.iterator
method.When
for...of
starts, it calls that method once. The method must return aniterator
– an object with the namenext
.Onward,
for...of
works only with that returned object.While
for...of
wants the next value, it callsnext()
on that object.The result of
next()
must have the form{done: Boolean, value: any}
, wheredone: true
means iteration has finished, otherwisevalue
is the next value.There are no limitations on
next
; it can return more and more values, that's normal.for...of
loop can be stopped usingbreak
.String
is iterable withfor...of
, and it works correctly with surrogate pairs.We can call iterator explicitly:
let iterator = str[Symbol.iterator]() while (true) { let result = iterator.next() if (result.done) break alert(result.value) }
Iterables are objects that implement the
Symbol.iterator
method.Array-likes are objects that have indexes and
length
. So they look like arrays.Strings are both iterables and Array-likes.
let arrayLike = { 0: 'hello', 1: 'World', length: 2, } // Error, no Symbol.iterator
Both iterables and array-likes are usually not arrays. They don't have
push
,pop
, etc.There's an
Array.from
method that takes an iterable or array-like value and makes a"real"
array.Array.from
takes an object, examines it for being an iterable or array-like, then makes a new array and copies all items to it.Array.from(obj[, mapFn, thisArg])
Array.from
can be used to convert a string into an array.Unlike
str.split
, it relies on the iterable nature of the string and so, just likefor...of
, correctly works with surrogate pairs.function surrogateAwareSplit(str, start, end) { return Array.from(str).slice(start, end).join('') }
Map
Map
is a collection of keyed data items, just like anObject
. But the main difference is thatMap
allows keys of any type.new Map()
- creates a map.map.set(key, value)
- stores thevalue
bykey
.map.get(key)
- returns thevalue
by thekey
.undefined
ifkey
doesn't exist in map.map.has(key)
– returnstrue
if thekey
exists,false
otherwise.map.delete(key)
removes thevalue
by thekey
.map.clear()
removes everything from the map.map.size
- returns the current element count.map[key]
isn't the right way to use aMap
.- Using objects as keys is one of the most stable and important
Map
features. The same does not count forObject
. String as a key inObject
is fine, but we can't use anotherObject
as a key inObject
. - To test keys for equivalence,
Map
uses the algorithmSameValueZero
. - It is roughly same as strict equality
===
, but the difference is thatNaN
is considered equal toNaN
. SoNaN
can be used as the key. - Chaining - Every
map.set
call returns the map itself, so we can"chain"
the calls. - Iteration on Map
map.keys()
- returns an iterable for keys.map.values()
- returns an iterable for values.map.entries()
- returns an iterable for entries[key, value]
.
Map
has built-inforEach
method likeArrays
.- A Map from Object -
let map = new Map(Object.entries(obj))
. - An Object from Map -
let object = Object.fromEntries(map.entries())
.
Set
- A
Set
is a special type collection -"set of values"
without keys, where each value may occur only once. new Set(iterable)
- creates the set, and if an iterable object is provided (usually an array), copies values from it into the set.set.add(value)
- adds thevalue
, returns the set itself.set.delete(values)
- removes thevalue
, returnstrue
if the value existed at the moment of the call, otherwisefalse
.set.has(value)
– returnstrue
if thevalue
exists in the set, otherwisefalse
.set.clear()
removes everything from the set.set.size()
- is the elements count.Set
is much better optimized internally for uniqueness checks.- We can loop over a set either with
for...of
or usingforEach
. - Supports
set.keys()
set.values()
set.entries()
- returns[value, value]
.
WeakMap
Usually, properties of an object or elements of an array or another data structure are considered reachable and kept in memory while that data structure is in memory.
If we put an object into an array, then while the array is alive, the object will be alive as well, even if there is no other reference to it.
Similarly, if we use an object as the key in a regular
Map
, then while theMap
exists, that object exists as well. It occupies memory and may not be garbage collected.WeakMap
is fundamentally different in this aspect. It doesn't prevent garbage collection of key objects.The first difference between
Map
andWeakMap
is that keys must be objects, not primitive values.WeakMap
does not support iteration and methodskeys()
,values()
,entries()
, so there is no way to get keys, values or entries.WeakMap
has only four methods:get
,set
,has
, anddelete
.The main area of application for
WeakMap
is additional data storage.If we would like to store some data associated with an object, that should only exist while the object is alive - then
WeakMap
is exactly what we needed.Another common example is caching. We can store
"cache"
results from a function.let cache = new Map(); // Can be WeakMap for garbage collection function process(obj) { if (cache.has(obj)) { return cache.get(obj); } let result = /* ... complicated calculation ... */; cache.set(obj, result); return result; } let result = process(obj);
Multiple calls of the
process(obj)
with the same object, it only calculates the result the first time, and then just takes it from cache.If we replace
Map
withWeakMap
, then the problem is disappeared.Cached results will be removed from the memory automatically after the object gets garbage collected.
WeakSet
- It is analogous to the
Set
, but we may only add objects toWeakSet
(not primitives). - The most notable limitation of
WeakMap
andWeakSet
is the absence of iterations, and the inability to get all current contents. - Their main job is to be
"additional"
storage of data for objects.
Object.keys, values, entries
- If we create a data structure of our own, then we should implement
keys
,values
,entries
. - They are supported for:
Map
Set
Array
- The first difference is that we have to call
Object.keys(obj)
and notobj.keys()
, while in Maps we can callmap.keys()
. - The second difference is that
Object.*
methods return"real"
array objects, not just an iterable. That's mainly for historic reasons. Object.keys
,values
,entries
ignores symbolic properties.- If we want symbolic keys as well, we need to use
Object.getOwnPropertySymbols()
returning only symbolic keys. - There is also
Reflect.ownKeys(obj)
that returns all keys. Object
lacks many methods that exist for arrays –map
,filter
.- We can use
Object.entries
followed byObject.fromEntries
.
Destructuring assignments
Destructuring assignment is a special syntax that allows us to
"unpack"
an object/array as a whole.Destructuring does not mean destructive - original arrays/objects are not modified.
let [firstName, lastName] = arr;
Unwanted elements of the array can also be thrown away via extra comma:
let [firstName,, title] = arr;
.We can use it with any iterables, not only arrays.
let [a, b, c] = "abc";
let [one, two, three] = new Set([1, 2, 3]);
- It works because internally it iterates over each element with
for...of
.
We can use any
"assignable"
at the left side.let user = {} ;[user.name, user.surname] = 'John Smith'.split(' ')
for (let [key, value] of Object.entries(obj))
// for objectsfor (let [key, value] of map)
// for mapsSwap Variable Trick -
[guest, admin] = [admin, guest]
- More than 2 variables can be swapped.The rest
...
pattern -let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the roman republic"];
Just make sure it has three dots before it and goes last in the destructuring assignment.
Absent values are considered
undefined
.let [firstName, lastName] = [];
//undefined
,undefined
Default values can be a more complex expression or even function calls.
let [name = "guest", surname = "Anonymous"] = ["Julius"];
In Object destructuring, order does not matter on the left-hand side (
let { z, x, y } = {}
).We can set variable names using a colon:
let { width: w, height: h, title } = {title: "Menu", width: 100, height: 200};
For potentially missing properties, we can set default values using
=
:let { width = 100, height = 200, title } = options;
We can also combine both colon and equality:
let { width: w = 100, height: h = 100, title } = options;
We can use the rest (
...
) pattern just like arrays.This won't work:
let title, width, height; { title, width, height } = options;
- The problem is that JS treats
{...}
in the main code flow (not inside another expression) as a code block. Such close blocks can be used to group statements.
- The problem is that JS treats
To Show JS that it's not a code block, we can wrap that expression in parentheses
(...)
.Smart function parameters
function({ incomingProperty: varName = defaultValue }) { /* ... */ } function showMenu({ title = "Menu", width = 100, height = 200 } = {}) { /* ... */ }
The full syntax is
let { prop: varName = default, ...rest } = options;
.
Date and Time
new Date()
- creates a newDate
object.new Date(milliseconds)
- creates a newDate
object with the time equal to the number of milliseconds passed after Jan 1st, 1970 UTC+0.- Integer number passed since the beginning of 1970 is called timestamp.
date.getTime()
function returns the timestamp.new Date(datestring)
- creates a newDate()
object. If time is not passed, it is midnight GMT.- Dates before 1/1/1970 have negative timestamps.
new Date(year, month, date, hours, minutes, seconds, ms)
year
is four digits,month
starts from0
(Jan) to11
(Dec),day
is absent it is set to1
, rest0
by default.- Access Date Component
getFullYear()
// notgetYear()
which is deprecatedgetMonth()
getDate()
getHours()
getMinutes()
getSeconds()
getMilliseconds()
getDay()
- returns0
(Sunday) to6
(Saturday) – first day is always Sunday.- All above methods have UTC version
getUTCxxx()
. getTime()
- returns timestamp.getTimezoneOffset()
- returns difference between UTC and the local time zone.
- Setting date component
setFullYear(year, [month], [date])
setMonth(month, [date])
setDate(date)
setHours(hour, [min], [sec], [ms])
setMinutes(min, [sec], [ms])
setSeconds(sec, [ms])
setMilliseconds(ms)
setTime(milliseconds)
- Autocorrection - We can set out-of-range values, and it will auto-adjust itself.
date.setDate(date.getDate() + 2);
// two days laterdate.setSeconds(date.getSeconds() + 70);
// 70 seconds laterdate.setDate(1);
// set date to 1 of this monthdate.setDate(0);
// set date to the last date of previous month
- When a date object is converted to a number, it becomes the timestamp, same as
date.getTime()
(alert(+date)
). - Dates can be subtracted; the result is their difference in milliseconds.
Date.now()
returns the current timestamp, similar tonew Date().getTime()
, but it doesn't create an intermediateDate
object.Date.now()
is faster and doesn't put pressure on garbage collection.- Using
getTime()
is so much faster! That's because there's no type conversion. It is much easier for engines to optimize. - The method
Date.parse(str)
can read a date from a string. - The string format should be:
"YYYY-MM-DDTHH:mm:ss.sssZ"
where:YYYY-MM-DD
is the date.- The character
"T"
is used as the delimiter. HH:mm:ss.sss
- the time: hours, minutes, seconds and milliseconds.- The optional
'Z'
part denotes the time zone in the format+-hh:ms
. A single letterZ
(Zulu) means UTC+0.
- Shorter variants are also possible, like
YYYY-MM-DD
,YYYY-MM
, or evenYYYY
. Date.parse(str)
parses the string in the given format and returns the timestamp (number of milliseconds from 1 Jan, 1970 UTC+0).- We can create a date from a timestamp:
let date = new Date(timestamp)
. - We can't create
"only date"
or"only time"
. Date objects always carry both. - Months are counted from zero (Jan = 0).
- Days of the week are also counted from zero (Sunday = 0).
- Date auto-corrects itself in case of out-of-range components.
- Dates become timestamps when converted to numbers, hence can be used in arithmetic.
- Use
Date.now()
to get timestamp fast. - JavaScript does not have a way to measure performance, but environments have (
performance.now()
function). - Node.js has a
microtime
module and other ways to get more precision.
toJSON
JSON methods, JSON (JavaScript Object Notation) is described in RFC 4627.
Initially made for JS, but handled by other language libraries as well.
JSON.stringify()
converts an object into JSON.JSON.parse
converts JSON back into an object.The result string of
JSON.stringify()
is called JSON-encoded, serialized, stringified or marshalled.Strings use double quotes only, no single quote, or backticks.
Supported Data Types
- Objects
{...}
- Arrays
[...]
- Primitives:
- Strings
- Numbers
- Boolean values
true
/false
Null
- Objects
JSON.stringify(variable)
is the same variable type.JSON is a data-only, language-independent specification, so it skips following:
- Function properties (methods)
- Symbolic keys and values
- Properties that store
undefined
.
let user = { sayHi() { alert('Hello') }, [Symbol('id')]: 123, something: undefined, } alert(JSON.stringify(user)) // {}
Nested objects are supported and converted automatically.
The important limitation: there must be no circular reference.
Full Syntax
JSON.stringify(value[, replacer, space])
replacer
- Array of properties to encode or a mapping functionfunction(key, value)
.space
- amount of space used for formatting.
JSON.stringify(meetup, ['title', 'participants'])
The function
replacer
will be called on every(key, value)
pair and should return the"replaced"
values, otherwiseundefined
if to skip.return (key === 'keyToSkip') ? undefined : value;
The value of
this
insidereplacer
is the object that contains the current property.The first
(key, value)
pair has an empty key, and the value is the target object as a whole.The
space
can be a number or a string.JSON.parse
JSON.parse(str[, reviver])
reviver
- optional function(key, value
) that will be called for each(key, value)
pair and can transform the value.- Return the value or changed value.
- It works on nested objects as well.
Adding comments to JSON makes it invalid.
Functions, Recursions and Stack
- Recursion is a programming pattern useful when a task can be split into.
- Two ways of thinking:
- Iterative Thinking
- Recursive thinking
- Base of recursion - it immediately produces an obvious result.
- Recursive step - simple call to the same task.
- Recursive solutions are usually shorter than an iterative one.
- Maximum number of nested calls is called recursion depth.
- Max recursion depth is limited by the JavaScript engine. We can rely on it being 10K, sometimes 100K (depends on the engine).
- The information about the process of execution of the running function is stored in its execution context.
- The execution context is an internal data structure that contains details about the execution of a function:
- Where the control flow is now.
- The current variables.
- The value of
this
. - And a few other internal details.
- When a function makes a nested call:
- The current function is paused.
- The execution context associated with it is remembered in a special data structure called execution context stack.
- The nested call executes.
- After it ends, the old execution context is retrieved from the stack, and the outer function is resumed from where it stopped.
- Contexts take memory.
- Loop-based algorithms are more memory-saving.
- Any recursion can be rewritten as a loop. The loop variant usually can be made more effective.
- Linked List
- A
LinkedList
is a recursive structure - a better alternative for arrays in some cases. "Delete element"
and"insert element"
are two problems with arrays; they are expensive operations.- Same with
array.unshift()
andarray.shift()
functions. - If we need really fast insertion and deletion, we need to use a linked list.
- A Linked List is an object with two properties:
value
andnext
.
- A
- Lists are not always better than arrays.
- The main drawback is that we can't easily access an element by its number.
- It is best used where adding/deleting from both ends is required.
- We can add
prev
to move back and forth.
Rest parameters and spread syntax
- There will be no errors due to excessive arguments passed to a function call.
function sumAll(...args)
-args
is the name for the array.- The rest parameter must be at the end; causes an error otherwise.
- There is also a special array-like object named
"arguments"
that contains all arguments by their index. - In old languages, when rest params were not available,
arguments
was used. arguments
is not an array, but an array-like and iterable object. Does not support array functions.- Arrow functions do not have
"arguments"
and"this"
. - Spread Syntax looks like a rest parameter but does quite the opposite.
Math.max(...arr, ...another)
- Spread syntax can be used to merge arrays.
- The spread syntax internally uses iterables to gather elements, the same way
for...of
does. - The list of characters is passed to an array initializer.
- The result of
[...str]
is same asArray.from(str)
.- But the only difference is:
Array.from
operates on both array-like and iterables.- The spread syntax works only on iterables.
Copy of an object/array
Object.assign()
let arrCopy = [...arr]
alert(JSON.stringify(arr) === JSON.stringify(arrCopy))
//true
alert(arr === arrCopy)
//false
- Similar thing is possible with an object.
- Spread syntax is preferred compared to
Object.assign()
.let copyOb = Object.assign({}, obj)
let copyArr = Object.assign([], arr)
- When we see
...
, it's either a rest parameter or spread parameter.- Rest parameters - is at the end, and gathers the rest of the list of arguments into an array.
- Spread syntax - is at the beginning and expands an array into the list.
- Rest parameters are used to create functions that accept any number of arguments.
- The spread syntax is used to pass an array to functions that normally require a list of many objects.
Variable scope, closure
- If a variable declared inside a code block
{...}
, it's only visible inside that block. - For
if
,for
,while
, and so on, variables declared in{...}
are also only visible inside. - A function is called
"nested"
if it is created inside another function. - What's more interesting, a nested function can be returned, either as a property of a new object or as a result by itself. It can then be used somewhere else. No matter where. It still has access to the same outer variables.
- Slightly modified variants of that code have practical uses, for instance, as a random number generator.
Lexical Environment
- Step 1: Variables
- In JS, every running function, code block
{...}
, and the script as a whole have an internal (hidden) associated object known as the Lexical Environment. - Lexical Environment has two parts:
- Environment Record - An object that stores all local variables as its properties.
- A reference to the outer lexical environment. As one associated with the outer code.
- A
"variable"
is just a property of the special internal object,Environment Record
. To get or change a variable means to get or change a property of that object. - Lexical Environment is a specification object - it exists only
"theoretically"
.
- In JS, every running function, code block
- Step 2: Function Declaration
- A function is also a value, like a variable.
- The difference is that a Function Declaration is instantly fully initialized.
- That's why we can use a function even before its declaration.
- Step 3: Inner and outer Lexical Environment
- When the code wants to access a variable - the inner lexical Environment is searched first, then the outer one, then the more outer one, and so on until the global one.
- If a variable is not found anywhere, that's an error in strict mode (without
"use strict"
, assignment to a non-existing variable creates a new global variable, for compatibility with the old code).
- Step 4: Returning a function
- All functions remember the Lexical Environment in which they were made.
- Technically, there's no magic here. All functions have the hidden property named
[[Environment]]
, that keeps the reference to the Lexical Environment where the function was created. - So
counter.[[Environment]]
has the reference to{count: 0}
Lexical Environment. That's how the function remembers where it was created, no matter where it's called. The[[Environment]]
reference is set once and forever at function creation time. - A new Lexical Environment is created for the call, and its outer Lexical Environment reference is taken from
counter.[[Environment]]
. - A variable is updated in the Lexical Environment where it lives.
- A closure is a function that remembers its outer variables and can access them.
- They automatically remember where they were created using a hidden
[[Environment]]
property, and then their code can access outer variables.
Garbage Collection (cont.)
A Lexical Environment is removed from memory, as any JavaScript object; it's only kept in memory while it's reachable.
However, if there's a nested function that is still reachable after the end of a function, then it has
[[Environment]]
property that references the lexical environment.A Lexical Environment object dies when it becomes unreachable.
let g = f() // Lexical environment exists g = null // Now the Lexical environment can be garbage collected if no other references.
"var"
The Old Internally,
var
is a very different beast that originates from very old times. It's generally not used in modern scripts, but still lurks in the old ones.It's important to understand the difference when migrating old scripts from
var
tolet
, to avoid odd errors."var"
has no block scope. Variables declared withvar
are tighter function-scoped or global-scoped."var"
ignores code blocks.If a code block is inside a function, then
var
becomes a function-level variable.Long ago, there were no Lexical Environments in JS.
"var"
is a remnant of it."var"
tolerates re-declarations any number of times. Already ignored variables are ignored."var"
variables can be declared before their use.People call such behavior
"hoisting"
(raising), because allvar
declarations are"hoisted"
(raised) to the top of the function.Declarations are hoisted, but assignments are not.
IIFE (Immediately Invoked Function Expressions)
- In old scripts, there was only
var
- it has no block-level visibility - so programmers invented IIFE. - That's not something we should use nowadays, but you can find them in old scripts.
;(function () { // ... })()
Parentheses around the function are a trick to show JavaScript that the function is created in the context of another expression, and hence it's a Function Expression: It needs no name and can be called immediately.
Ways to create IIFE:
;(function () { /* ... */ })() ;(function () { /* ... */ })() // Parenthesis around the whole thing !(function () { /* ... */ })() // Bitwise NOT operator starts the expression ;+(function () { /* ... */ })() // Unary plus operator starts the expression
Nowadays there is no need to write such codes.
- In old scripts, there was only
var
variables have no block scope; their visibility is scoped to the current function, or global if declared outside a function.var
declarations are processed at function start (script start for globals).These differences make
var
worse thanlet
most of the time.
Global Object
In a browser, it is named
window
; for Node.js, it isglobal
.Recently
globalThis
was added to the language, supported by most environments and browsers.Using global variables is generally discouraged.
Using for Polyfills:
if (!window.Promise) { window.Promise = { /* ... custom Promise implementation ... */ } }
Function object, NFE
name
property- A function in JavaScript is a value, but what type of value?
- In JavaScript, functions are objects.
- All functions have a
name
property. let sayHi = function() { /* ... */ }; alert(sayHi.name);
//sayHi
- When there is no name, it's an empty string.
- Most of the time, the name has a value.
length
property- Returns the number of function parameters.
- Rest parameters are not counted.
Custom properties
function sayHi() { sayHi.counter++ } sayHi.counter = 0 sayHi() sayHi()
- A property is not a variable.
- A property
counter
and a variablelet counter
are two unrelated things. - Variables are not function properties and vice versa. There are just parallel worlds.
- Function properties can replace closures sometimes.
Named Function Expression (NFE) is a term for a Function Expression that has a name.
let sayHi = function func(who) { /* ... */ };
- It allows the function to reference itself internally.
- It is not visible outside of the function.
"func"
is function-local; it is not taken outside."func"
is an internal function name: how the function can call itself internally.- The internal name feature described here is only available for Function Expressions, not for Function Declarations, for which no syntax is present for adding
"internal"
name.
"new Function"
syntax
The let func = new Function([arg1, arg2, ...argN], functionBody)
Examples
let sum = new Function('a', 'b', 'return a+b') let sum2 = new Function(`a,b`, `return a+b`) let sum3 = new Function(`a b`, `return a+b`)
The major difference from other ways is that the function is created literally from a string. That is passed at run time.
new Function
allows us to turn any string into a function.
Closure (cont.)
- Usually, a function remembers where it was born in the special property
[[Environment]]
. - But when a function is created with
new Function
, its[[Environment]]
is set to reference not the current Lexical Environment, but the global one. - If
new Function
had access to outer variables, it would have problems with minifiers. - Passing parameters explicitly is a much better method architecturally and causes no problems with minifiers.
setTimeout
and setInterval
Scheduling: setTimeout
allows us to run a function once after an interval of time.setInterval
allows us to run a function repeatedly, starting after the interval of time, then repeating continuously at that interval.setTimeout
let timerId = setTimeout(func|code, [delay], [arg1, arg2, ...argN])
- For historical reasons, a string of code can be passed, but that's not recommended.
- Arguments are not supported by IE9-.
delay
is in milliseconds, default value is0
.- If the first argument is a string, then JS creates a function from it, but it's not recommended to use strings.
- Pass a function, but don't run it (i.e., don't use
func()
). setTimeout
expects a reference to a function.- A call to
setTimeout
returns a"timer identifier"
that can be used to cancel the execution. clearTimeout(timerId)
- In a browser, the timer identifier is a number; in other environments, this can be something else (e.g., Timer object in Node.js).
setInterval
- The
setInterval
method has the same syntax assetTimeout
. - Unlike
setTimeout
, it runs the function not only once, but regularly after the given interval of time. - To stop further calls, we should call
clearInterval(timerId)
. - Timer goes on while
alert
is shown.
- The
Nested
setTimeout
Two ways to run something regularly:
setInterval
or NestedsetTimeout
.let timerId = setTimeout(function tick() { alert('tick') timerId = setTimeout(tick, 2000) // Schedule next tick }, 2000)
The nested
setTimeout
is a more flexible method thansetInterval
. This way, the next call may be scheduled differently, depending on the result of the current one.If the functions that we're scheduling are CPU-hungry, then we can measure the time taken by the execution and plan the next call sooner or later.
Nested
setTimeout
allows to set the delay between the executions more precisely thansetInterval
.
The real delay between function calls for
setInterval
is less than in the code.It is possible that a function's execution turns out to be longer than we expected and takes more than the given interval.
In the edge case, if the function always executes longer than
delay
ms, then the calls will happen without a pause at all.The nested
setTimeout
guarantees a fixed delay.For
setInterval
, the function stays in memory untilclearInterval
is called.A function reference is the outer lexical environment, so, while it lives, outer variables live too. They may take much more memory than the function itself. So when we don't need the scheduled function anymore, it's better to cancel it, even if it's very small.
Zero Delay
setTimeout
setTimeout(func, 0)
or justsetTimeout(func)
.- This schedules the execution of
func
as soon as possible. - The first line
"puts the call into calendar after 0 ms"
. But the schedule will only"check the calendar"
after the current script is complete. - Zero delay is in fact not zero (in a browser).
- HTML5 standard says
"after five nested timers the interval is forced to be at least 4 ms"
. - The similar thing happens if we use
setInterval
instead ofsetTimeout
. ThesetInterval(f)
runsf
a few times with Zero delay, and afterwards with 4+ Millisecond delay. - All scheduling methods do not guarantee the exact delay.
call
/apply
Decorators and forwarding, function cachingDecorator(func) { let cache = new Map() // A Map to store results return function (x) { // The wrapper function if (cache.has(x)) { return cache.get(x) // Return cached result if available } let result = func(x) // Call the original function cache.set(x, result) // Cache the result return result } } // Example usage: // let slow = cachingDecorator(originalSlowFunction);
Benefits:
- The
cachingDecorator
is reusable. We can apply it to another function. - The caching logic is separate. It did not increase the complexity of
slow
itself. - We can combine multiple decorators if needed.
- The
The caching decorator mentioned above is not suited to work with object methods.
func.call(context, arg1, arg2, ...argN)
let result = func.call(this, x) // Calls func with 'this' context and argument 'x'
let worker = { slow: function (x) { /* ... original slow logic ... */ }, } worker.slow = cachingDecorator(worker.slow) // Decorate the method
How
this
is passing:- After the decoration,
worker.slow
is now the wrapper functionfunction(x) { ... }
. - So when
worker.slow(2)
is executed, the wrapper gets2
as an argument andthis=worker
(its object before the dot). - Inside the wrapper, assuming the result is not yet cached,
func.call(this, x)
passes the currentthis
(worker
) and the current argument (x
) to the original method.
- After the decoration,
Going Multi Arguments
The native
Map
takes a single value only as the key.Three solutions for multi-argument caching:
- Implement a new map-like data structure.
- Use nested maps.
- Join two values into one.
For practical applications, the third one is a good solution.
let key = hash(arguments) if (cache.has(key)) return cache.get(key)
Hash functions:
function hash(args) { return args[0] + ',' + args[1] } // Simple concatenation function hash(args) { return args.join() } // Using Array.prototype.join
- Unfortunately,
arguments.join()
won't work directly becausearguments
object is both iterable and array-like but not a real array.
function hash(args) { return [].join.call(arguments) } // Method borrowing
- Unfortunately,
This trick is called method borrowing.
call
and apply
)
Two methods (func.call(context, ...args)
func.apply(context, args)
func.apply
will probably be faster, because most JS engines internally optimize them better.
Function bindings
When passing object methods as callbacks, there is a known problem:
"losing this"
.setTimeout(user.sayHi, 1000) // user.sayHi will lose its 'this' context // This is equivalent to: // let f = user.sayHi; // setTimeout(f, 1000);
Solutions:
A wrapper:
setTimeout(function () { user.sayHi() }, 1000)
- This works because it receives
user
from the outer lexical environment.
setTimeout(() => user.sayHi(), 1000)
- Looks fine but slightly vulnerable (if
user
is changed after the above use).
- This works because it receives
bind
:let boundFunc = func.bind(context)
- Calling
boundFunc
is likefunc
with a fixedthis
. let sayHi = user.sayHi.bind(user)
bindAll
(for binding all methods of an object):for (let key in user) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user) } }
Partial function application:
It can take parameters as well.
let bound = func.bind(context, [arg1], [arg2] ...)
function mul(a, b) { return a * b } let double = mul.bind(null, 2) // Creates a new function that always has 'a' as 2 alert(double(3)) // mul(2, 3) = 6 alert(double(5)) // mul(2, 5) = 10
Partial application is useful when we have a very generic function and want a less universal variant of it for convenience.
Going partial without context:
- The
bind
function does not allow to fix arguments without the context. - Function
partial
allows binding arguments without context.
function partial(func, ...argsBound) { return function (...args) { // Wrapper function return func.call(this, ...argsBound, ...args) } } let user = {} user.sayNow = partial(user.say, someDate) user.sayNow('Hello')
- When we fix some arguments of an existing function, the resulting (less universal) function is called partially applied or partial.
- Partials are convenient when we don't want to repeat the same argument over and over again.
- The
Arrow function revisited
- We usually don't want to leave the current context; that's where arrow functions come in handy.
- Arrow functions have no
this
. - Arrow functions do not run with
"new"
. - The arrow function doesn't create any
bind
(no.bind
). - Arrow functions have no
arguments
object.
Object Properties Configuration
Besides
value
, three special attributes (flags):writable
(value can't be changed iffalse
).enumerable
(iftrue
, listed in loops).configurable
(iftrue
, property can be deleted, flags can be changed).
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
{ "value": "John", "writable": true, "enumerable": true, "configurable": true, }
Object.defineProperty(obj, propertyName, descriptor)
In non-strict mode, no errors occur when writing to non-writable properties, etc.
Flag-violating actions are just silently ignored in non-strict mode.
When
enumerable: false
:- Will not appear in
for...in
loop. - Will not appear in
Object.keys()
.
- Will not appear in
Non-configurable (
configurable: false
) property cannot be deleted.Making a property non-configurable is a one-way road. We cannot change it back with
defineProperty
.Non-configurable imposes:
- Can't change
configurable
flag. - Can't change
enumerable
flag. - Can't change
writable: false
totrue
(The other way around works). - Can't change
get
/set
for an accessor property.
- Can't change
The idea of
"configurable: false"
is to prevent changes of property flags and its deletion, while allowing to change its value.Object.defineProperties
Object.defineProperties(obj, { prop1: descriptor1, prop2: descriptor2, Surname: { value: 'Smith', writable: false }, })
If we want a better clone of an object, use:
let clone = Object.defineProperties([], Object.getOwnPropertyDescriptors(obj));
Object.getOwnPropertyDescriptors
returns all property descriptors including symbolic ones.Sealing an object:
Object.preventExtensions(obj)
: Forbids the addition of new properties to the object.Object.seal(obj)
: Forbids adding/removing of properties. Setsconfigurable: false
for all existing properties.Object.freeze(obj)
: Forbids adding/removing/changing of properties. Setsconfigurable: false
andwritable: false
for all properties.Object.isExtensible(obj)
Object.isSealed(obj)
Object.isFrozen(obj)
Getters and Setters
let obj = { get propName() {}, set propName(value) {}, }
let user = { name: 'Rakesh', surname: 'Tembhurne', get fullName() { return `${this.name} ${this.surname}` }, set fullName(value) { ;[this.name, this.surname] = value.split(' ') }, }
We don't call
user.fullName
as a function; we read it normally. The getter runs behind the scenes.There will be an error when doing like:
user.fullName = xxx
if no setter is defined.fullName
is a virtual property; it is readable and writable withget()
andset()
.Accessor Descriptors
get
set
enumerable
- same as for data properties.configurable
- same as for data properties.
A property can be either an accessor (has
get
/set
methods) or a data property (has avalue
), not both.// Error: Invalid property descriptor Object.defineProperty({}, 'prop', { get() { return 1 }, value: 2, // Cannot have both get/set and value })
Prototypes, Inheritance
- In JavaScript, objects have a special hidden property
[[Prototype]]
, that is eithernull
or an object. That object is called"prototype"
. - The property
[[Prototype]]
is internal and hidden. - Multiple ways to access
[[Prototype]]
:rabbit.__proto__ = animal;
- If
animal
has a lot of useful properties and methods, then they become automatically available inrabbit
. - The prototype chain can be longer.
- There are only two limitations:
- The references cannot go in circles. (Error)
- The value of
__proto__
can be eithernull
or an object. Other types are ignored.
- There can be only one
[[Prototype]]
. An object may not inherit from two others. __proto__
is a historical getter/setter for[[Prototype]]
.__proto__
is not the same as the internal[[Prototype]]
property. It's a getter/setter for[[Prototype]]
.- The
__proto__
property is a bit outdated.Object.getPrototypeOf
andObject.setPrototypeOf
are used nowadays. - The prototype is only used for reading purposes. Writing/deleting works directly with the object.
- Accessor properties are an exception to the above rule. An assignment is handled by a setter function.
this
is not affected by the prototypes at all.- No matter where the method is found: in an object or its prototype. In a method call,
this
is always the object before the call. - When the inheriting object runs the inherited methods, they will modify only their own states, not the state of the base object.
Object.keys()
only returns an object's own keys, but thefor...in
loop iterates over inherited properties too.- To exclude inherited properties, there is a built-in method
obj.hasOwnProperty(key)
. - Why does
hasOwnProperty
not appear infor...in
loop? The answer is – it's not enumerable.for...in
only shows enumerable properties.
F.prototype
If
F.prototype
is an object, then thenew
operator uses it to set[[Prototype]]
for the new object.Setting
Rabbit.prototype = animal
literally states the following:"When a new Rabbit is created, assign its [[Prototype]] to animal"
.Rabbit ───────► prototype ───────► animal eats: true ▲ [[Prototype]] │ rabbit ──────────┘ name: "White Rabbit"
On the picture,
"prototype"
is a horizontal arrow, meaning a regular property, and[[Prototype]]
is vertical, meaning the inheritance ofrabbit
fromanimal
.F.prototype
property is only used whennew F()
is called; it assigns[[Prototype]]
of the new object.If, after the creation,
F.prototype
property changes (F.prototype = <new object>
), then new objects created bynew F
will have another object[[Prototype]]
, but already existing objects keep the old one.
Default F.prototype, constructor property
F.prototype
property (don't mistake it for[[Prototype]]
) sets[[Prototype]]
of new objects whennew F()
is called.The value of
F.prototype
should be either an object ornull
; other values won't work.The
"prototype"
property only has such a special effect when set on a constructor function and invoked withnew
.Every function has the
"prototype"
property even if we don't supply it.The default prototype is an object with the only property
constructor
that points back to the function itself.function Rabbit() {} // Rabbit.prototype = { constructor: Rabbit } alert(Rabbit.prototype.constructor === Rabbit) // True
function Rabbit() {} let rabbit = new Rabbit() alert(rabbit.constructor == Rabbit) // true let rabbit2 = new rabbit.constructor('White Rabbit') // Creates new rabbit using constructor let rabbit3 = new Rabbit('Black Rabbit') // Creates new rabbit using constructor
That's handy when we have an object, don't know which constructor was used for it, and we need to create another one of the same kind.
Javascript does not ensure the right
"constructor"
value. It's on us after creation.
Native Prototypes
Object.prototype
The short notation
obj = {}
is the same asobj = new Object()
.When
new Object()
is called (or a literal object{...}
is created), the[[Prototype]]
of it is set toObject.prototype
.let obj = {} alert(obj.__proto__ === Object.prototype) // true alert(obj.toString === obj.__proto__.toString) //true alert(obj.toString === Object.prototype.toString) //true
Other Built-in Prototypes
graph TD subgraph Object Hierarchy A[null] --> B[[Prototype]] B --> C[Object.prototype] C --> D[toString: function] C --> E[other object methods] end subgraph Array Hierarchy F[[Prototype]] --> C G[Array.prototype] --> F G --> H[slice: function] G --> I[other array methods] J[[1, 2, 3]] --> G end subgraph Function Hierarchy K[[Prototype]] --> C L[Function.prototype] --> K L --> M[call: function] L --> N[other function methods] O[function f(args) { ... }] --> L end subgraph Number Hierarchy P[[Prototype]] --> C Q[Number.prototype] --> P Q --> R[toFixed: function] Q --> S[other number methods] T[5] --> Q end
All of the built-in prototypes have
Object.prototype
on the top.let arr = [1, 2, 3];
alert(arr.__proto__ === Array.prototype);
//true
// then from Object.prototype?
alert(arr.__proto__.__proto__ === Object.prototype);
//true
// and null on the top.
alert(arr.__proto__.__proto__.__proto__);
//null
Primitives (cont.)
- The most intricate thing happens with strings, numbers, and booleans.
- As they are not objects, a temporary wrapper object is created using built-in constructors:
String
,Number
, andBoolean
. They provide methods and disappear. - Values
null
andundefined
have no object wrappers.
Changing native prototypes
Native prototypes can be modified.
Prototypes are global, so it's easy to get conflicts. If two libraries add the same method, it can cause issues.
In modern programming, there is only one case where modifying native prototypes is approved: polyfilling.
if (!String.prototype.repeat) { String.prototype.repeat = function (n) { /* ... implementation ... */ } }
Object borrowing:
let obj = {} obj.join = Array.prototype.join // Borrowing join method alert(obj.join(',')) // Works on obj if obj has a .length property or similar structure
All built-in objects follow the same pattern:
- The methods are stored in the prototype (
Array.prototype
,Object.prototype
,Date.prototype
, etc.). - The object itself stores only the data.
- The methods are stored in the prototype (
Primitives also store methods in prototypes of wrapper objects:
Number.prototype
,String.prototype
, andBoolean.prototype
.Only
undefined
andnull
do not have wrapper objects.Built-in prototypes can be modified or populated with new methods. But it's not recommended to change them. The only allowable case is probably when we add a new standard, but it's not yet supported by the JavaScript engine.
__proto__
Prototype Methods, objects without The
__proto__
is considered outdated and somewhat deprecated.Modern methods to get/set prototypes are:
Object.getPrototypeOf(obj)
- returns[[Prototype]]
ofobj
.Object.setPrototypeOf(obj, proto)
- sets the[[Prototype]]
ofobj
toproto
.Object.create(proto, [descriptors])
- creates an empty object with a givenproto
as[[Prototype]]
and optional property descriptors.
Object.create()
has an optional second argument: property descriptors. We can provide additional properties to the new object there.let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
Don't change
[[Prototype]]
on existing objects if speed matters.Object.create(null)
creates an empty object without a prototype ([[Prototype]]
isnull
).The built-in
__proto__
getter/setter is unsafe if we'd want to put user-generated keys into an object.We can use
Object.create(null)
to create a"very plain"
object without__proto__
, or stick toMap
object for that.Object.create
provides an easy way to shallow-copy an object with all descriptors.__proto__
is a getter/setter for[[Prototype]]
and resides inObject.prototype
, just like other methods.
Classes
The
class
construct introduces great new features which are useful for object-oriented programming.class User { constructor() {} sayHi() {} method2() {} }
There is no comma between class methods. Within the class, no comma is required.
What
class User { ... }
construct really does is:- Creates a function named
User
, that becomes the result of the class declaration. - The function code is taken from the
constructor
method. - Stores class methods, such as
sayHi()
inUser.prototype
.
- Creates a function named
alert(typeof User)
//function
alert(User === User.prototype.constructor)
//true
alert(Object.getOwnPropertyNames(User.prototype))
//constructor
,sayHi
,method2
Difference between
function User(){}
andclass User {}
- A function created by a class is labelled by a special internal property
[[IsClassConstructor]]: true
. - String representation of a class starts with
"class User {...]"
. User()
// Error: class constructorUser
cannot be invoked without"new"
.- Class methods are non-enumerable. Class definition sets
enumerable: false
for all methods in"prototype"
. - So, class methods won't be shown in
for...in
loop. - Classes always
"use strict"
.
- A function created by a class is labelled by a special internal property
Just like functions, classes can be defined inside another expression:
let User = class { ... };
.If a class expression has a name, it's visible inside the class only:
let User = class MyClass{...};
//MyClass
can be used inside class.Classes can be created on-demand:
return class { ... };
.
Class Fields
Previously, our classes only had methods. Class fields are a recent addition.
The important difference of class fields is that they are set on individual objects, not
User.prototype
.class User { name = 'John' // A public class field } let user = new User() alert(user.name) // John alert(user.prototype.name) // undefined
Methods like
click() { alert(this.value); }
have a problem of losingthis
, which can be fixed with:- Pass a wrapper function, such as
setTimeout(() => buttons.click(), 1000)
. - Bind the method to the object, e.g., in the constructor.
- Pass a wrapper function, such as
MyClass
is technically a function, while methods, getters, and setters are written toMyClass.prototype
.
Class Inheritance
"extends"
keyword is used:class Rabbit extends Animal
.Any expression is allowed after
"extends"
:function f(phrase) { return class { sayHi() { alert(phrase) } } } class User extends f('Hello') {}
By default, all methods that are not specified in
class Rabbit
are taken directly"as is"
fromclass Animal
.In order to extend the functionality of methods or tweak them, we use
"super"
.super.method(...)
to call a parent method.super(...)
to call a parent constructor.
Arrow functions have no
super
.If a class extends another class and has no constructor, then the following empty constructor is generated:
class Rabbit extends Animal { constructor(...args) { super(...args) } }
That happens if we don't write a constructor of our own.
Constructors in inheriting classes must call
super(...)
, and do it before usingthis
.Inheriting class (so-called derived constructor) has a special internal property
[[ConstructorKind]]: "derived"
."derived"
label affects behavior with"new"
:- When a regular function is executed with
new
, it creates an empty object and assigns it tothis
. - But when a derived constructor runs, it doesn't do this. It expects the parent constructor to do this job.
- When a regular function is executed with
We can override not only methods, but also class fields.
Parent constructor ALWAYS uses its own field value, not the overridden one.
Static properties and methods
We can also assign a method to the class function itself, not to its
"prototype"
. Such methods are called static.class User { static staticMethod() {} }
Above is the same as:
class User {} User.staticMethod = function () {}
Usually, static methods are used to implement functions that belong to the class, but not to a particular object of it.
Technically, a static declaration is the same as assigning to the class itself.
MyClass.property = ...
MyClass.method = ...
For
class B extends A
, the prototype ofclass B
itself points toA
.B.[[Prototype]] = A
.So if a field is not found in
B
, the search continues inA
.
Protected fields
- Protected properties are usually prefixed with an underscore (
_
). - This is not enforced on the language level, but there's a well-known convention between programmers that such properties and methods should not be accessed from the outside.
- Protected fields are naturally inheritable. Unlike private ones that we will see.
Private fields
- It's a recent addition.
- Privates should start with
#
. - They are only accessible from inside the class.
#
is a special sign that the field is private.- Private fields do not conflict with public ones. We can have both
#waterAmount
andwaterAmount
(public property). - Unlike protected ones, private fields are enforced by the language itself.
- Fields like
this['#name']
doesn't work.
Extending built-in classes
class PowerArray extends Array { isEmpty() { return this.length === 0 } }
When
arr.filter()
is called, it internally creates a new array of results using exactlyarr.constructor
, not basicArray
.That's actually very cool, because we can keep using
PowerArray
methods further on the result.Even more, we can customize this behavior by using
Symbol.species
:class PowerArray extends Array { static get [Symbol.species]() { return Array } }
Now,
.filter()
returnsArray
. So the extended functionality is not passed any further.Other collections such as
Map
,Set
work alike. They also useSymbol.species
.There is no link between
Date
andObject
. They are independent; onlyDate.prototype
inherits fromObject.prototype
.obj instanceof class
returnstrue
ifobj
belongs to theClass
or a class inheriting from it.arr instanceof Array
//true
arr instanceof Object
//true
We can set custom logic in the static method
Symbol.hasInstance
:class Animal { static [Symbol.hasInstance](obj) { if (obj.canEdit) return true } } // Usage: new Animal() instanceof Animal // true // { canEdit: true } instanceof Animal // true
Most classes do not have
Symbol.hasInstance
; then it's checked like this:obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
There is also a method
objA.isPrototypeOf(objB)
, which returnstrue
/false
.obj instanceof Class
can be rephrased asClass.prototype.isPrototypeOf(obj)
.Alternative to
instanceof
is extendedtypeof
:alert({}.toString.call(value))
- For a number, it will be
[object Number]
. - For a boolean, it will be
[object Boolean]
. - For
null
:[object Null]
. - For
undefined
:[object Undefined]
. - For arrays:
[object Array]
.
let objectToString = Object.prototype.toString let arr = [] alert(objectToString.call(arr)) // [object Array]
The behavior of
Object.prototype.toString
can be customized using a special object propertySymbol.toStringTag
.let User = { [Symbol.toStringTag]: 'User', } alert({}.toString.call(User)) // [object User] alert(window[Symbol.toStringTag]) // Window alert(XMLHttpRequest.prototype[Symbol.toStringTag]) // XMLHttpRequest (for browser environments) alert({}.toString.call(window)) // [object Window] alert({}.toString.call(new XMLHttpRequest())) // [object XMLHttpRequest]
We can use
{}.toString.call
instead ofinstanceof
for built-in objects when we want to get the type as a string rather than just to check.
Mixins
There can be only one
[[Prototype]]
for an object.A mixin is a class containing methods that can be used by other classes without a need to inherit from it.
// Example Mixin let sayHiMixin = { sayHi() { alert('Hi!') }, sayBye() { alert('Bye!') }, } // Use the mixin with a class class User extends Params { /* ... */ } // Assuming Params is some base class Object.assign(User.prototype, sayHiMixin)
Error Handling
- Usually, a script
"dies"
in case of an error, printing it to the console. - But
try...catch
allows us to"catch"
errors. - The
try
block is executed. If there are no errors, then thecatch
block is ignored. - If there is an error, then the
catch
block is executed. try...catch
does not kill the script; we can handle things in thecatch
block.try...catch
only works for runtime errors.- For
try...catch
to work, the code must be runnable. In other words, it should be valid JavaScript. - It won't work if the code is syntactically wrong, for instance, it has unmatched curly braces.
try...catch
can only handle errors that occur in valid code - such errors are called runtime errors or sometimes exceptions.try...catch
works synchronously. If an exception happens in"scheduled"
code likesetTimeout
, thentry...catch
won't catch it.- To catch an exception inside a scheduled function,
try...catch
must be inside that function. - Error Object
- It has two main components:
name
andmessage
. stack
is also available for debugging purposes.- Recent addition: If we don't need error details, we may omit
(err)
fromcatch
:try { /* ... */ } catch {}
.
- It has two main components:
- Throw Operator
- Technically, we can use anything as an error object.
- It's better to be an object, preferably with
name
andmessage
properties (for compatibility).
- Built-in constructors for errors:
Error
SyntaxError
ReferenceError
TypeError
- For built-in errors, the
name
property is exactly the same as the name of the constructor. Andmessage
is taken from the argument. - We can get the error class name from
err.name
property. All native errors have it. Another option is to readerr.constructor.name
. - The code has two ways of execution when using
try...catch...finally
:- If you answer
"Yes"
to"Make an error?"
, thentry
=>catch
=>finally
. - If you say
"No"
, thentry
=>finally
.
- If you answer
- The function may finish with
return
orthrow
; that doesn't matter. Thefinally
clause executes in both cases. - Variables are local inside
try...catch...finally
. - The
finally
clause works for any exit fromtry...catch
, that includes an explicitreturn
. - The
try...finally
construct, without acatch
clause, is also useful. We apply it when we don't want to handle errors here, but want to be sure that processes that we started are finalized. - Global Catch
- Node.js:
process.on("uncaughtException", handler);
- Browser:
window.onerror = function (message, url, line, col, error) { ... };
- Node.js:
- Rethrowing is a very important pattern of error handling: a
catch
block usually expects and knows how to handle the particular error type, so it should rethrow errors it doesn't know. - We can inherit from
Error
and other built-in error classes normally. We just need to take care of thename
property and don't forget to callsuper
. - We can use
instanceof
to check for particular errors. It also works with inheritance. But sometimes we have an error object coming from a 3rd-party library and there's no easy way to get its class. Thenname
property can be used for such checks. - Wrapping exceptions is a widespread technique: a function handles low-level exceptions and creates higher-level errors instead of various low-level ones. Low-level exceptions sometimes become properties of that object like
err.cause
in the examples above, but that's not strictly required.
async
/await
Promises, Error-first callback style:
- The first argument of the callback is reserved for an error if it occurs. Then
callback(err)
is called. - The second argument (and the next one if needed) are for the successful result. Then
callback(null, result, result2,...)
is called.
- The first argument of the callback is reserved for an error if it occurs. Then
Pyramid of doom or also known as callback hell.
The function passed to
new Promise
is called executor. Whennew Promise
is created, the executor runs automatically.After the executor is finished:
resolve(value)
reject(error)
The promise object returned by the
new Promise
constructor has these internal properties:- State - initially
"pending"
, then changes to either"fulfilled"
whenresolve
is called or"rejected"
whenreject
is called. - Result - initially
undefined
. Then changes tovalue
whenresolve(value)
is called orerror
whenreject(error)
is called.
- State - initially
There can be only a single result or an error.
The executor should call only one
resolve
or onereject
.Any state change is final. All further calls of
resolve
andreject
are ignored.resolve
/reject
expect only one argument (or none) and will ignore additional arguments.reject
can be used with any type of argument, but anError
object is recommended.We can call
resolve
orreject
immediately, without async operations.The state and result are internal. The properties
state
andresult
of the Promise object are internal. We can't directly access them.Consumers:
then
,catch
,finally
promise.then(function(result){...}, function(error) {...})
- The call
.catch(f)
is a complete analog of.then(null, f)
. It's just a shorthand. - The call
.finally(f)
is similar to.then(f, f)
in the sense thatf
always runs when the promise is settled: be itresolve
orreject
.
The
finally
isn't exactly an alias ofthen(f, f)
though. There are few subtle differences:- A
finally
handler has no arguments. Infinally
, we don't know whether the promise is successful or not. That's all right, as our task is usually to perform"general"
finalizing procedures. - A
finally
handler passes through results and errors to the next handler. We can use.then
or.catch
after.finally
.
- A
The
finally
is not meant to process a promise result. So it passes it through.If the promise is pending,
.then
/catch
/finally
handlers wait for it.If a
.then
(or.catch
/finally
doesn't matter) handler returns a promise, the rest of the chain waits until it settles. When it does, its result is passed further.Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler.
The code of a promise executor and handler has an
"invisible try...catch"
around it..catch
at the end of the chain is similar totry...catch
.If we
throw
inside.catch
, then the control goes to the next closest error handler. And if we handle the error and finish it normally, then it continues to the next closest successful.then
handler.new Promise(...).then(...).catch(...).then(...).catch(...);
Just like in
try...catch
, a similar thing happens with unhandled promises.Promisification - it's the conversion of a function that accepts a callback into a function that returns a promise.
function promisify(f, manyArgs = false) { return function (...args) { return new Promise((resolve, reject) => { function callback(err, ...results) { // our custom callback for f if (err) { reject(err) } else { // resolve with all callback results if manyArgs is specified resolve(manyArgs ? results : results[0]) } } args.push(callback) f.call(this, ...args) }) } } // Usage: // let f = promisify(originalCallbackFunction, true); // f(...).then(arrayOfResults => ..., err => ...);
Promisification is not a total replacement for callbacks.
A promise may have only one result, but a callback may technically be called many times.
So promisification is only meant for functions that call the callback once. Further calls will be ignored.
Microtasks
- Promise handlers (
.then
,.catch
,.finally
) are always asynchronous. - Asynchronous tasks need proper management - ECMA uses an internal queue: Promise Jobs.
- The queue is first-in-first-out: tasks enqueued first are run first.
- Execution of a task is initiated only when nothing else is running.
- Promise handlers (
Unhandled Rejection
- An
"unhandled rejection"
(withunhandledRejection
event) occurs when a promise error is not handled at the end of the microtask queue. unhandledRejection
is generated when the microtask queue is complete.
- An
Promise handling is always asynchronous, as all promise actions pass through the internal
"promise jobs"
queue (microtask queue).So
.then
/catch
/finally
handlers are always called after the current code is finished.The concept of microtask is closely related with the
"event loop"
.async
/await
The word
"async"
before a function means one simple thing: a function always returns a promise. Other values are wrapped in a resolved promise automatically.async function f() { return 1 } f().then(alert) // Shows 1
async
ensures that the function returns a promise, and wraps non-promises in it.await
literally suspends the function execution until the promise settles, and then resumes it with the promise result.Can't use
await
in a non-async
function; there would be a syntax error if used.await
only works inside anasync
function.await
won't work in top-level code directly, so we have to wrap it up in an anonymousasync
function or IIFE.await
accepts"thenables"
(objects with a.then
method).
Generators
Regular functions return only one, single value (or nothing).
Generators can return (
yield
) multiple values, one after another, on demand.They work great with iterables, allowing to create data streams with ease.
To create a generator, we use
function* f() { ... }
.Generators behave differently from regular functions. When such a function is called, it doesn't run its code. Instead, it returns a special object, called a
"generator object"
, to manage the execution ([object Generator]
).The main method of a generator is
next()
. When called, it runs the execution until the nearestyield <value>
.next()
always returns an object with two properties:value
: the yielded value.done
:true
if the function code has finished, otherwisefalse
.
Two syntaxes for generators, but the first is preferred:
function* f() { ... }
function *f() { ... }
Generators are iterables.
for (let value of generator) { alert(value) }
Looks a lot nicer than calling
.next().value
.for...of
iteration ignores the lastvalue
whendone: true
.If we want all results to be shown by
for...of
, we mustreturn
them withyield
.let sequence = [0, ...generatorSequence()] // Converts iterable to array
...generatorSequence()
turns the iterable generator object into an array of items."yield"
is a two-way street. It not only returns the result to the outside, but also can pass the value inside that generator.The first call
generator.next()
should always be made without arguments.To pass an error into a
yield
, we should callgenerator.throw(err)
.In JavaScript, generators are rarely used. But sometimes they come in handy, because the ability of a function to exchange data with the calling code during the execution is quite unique.
Generators are great for making iterable objects.
To use async iterables, use
Symbol.asyncIterator
instead ofSymbol.iterator
.To iterate over async iterables, we should use a
for await (let item of iterable) loop
.The spread syntax
...
doesn't work asynchronously.For async generators,
generator.next()
method is asynchronous; it returns promises.
Modules
- As our apps grow bigger, we want to split them into multiple files, so-called
"modules"
. - They may contain a class or a library of functions for a specific purpose.
- Some types of module systems (historically):
- AMD
- CommonJS
- UMD
- The language-level module system appeared in 2015.
- A module is just a file; one script is one module, as simple as that.
- Modules use special directives
export
andimport
. export
keyword labels variables and functions that should be accessible from outside the current module.import
allows the import of functionality from other modules.- As modules support special keywords and features, we must tell the browser that a script should be treated as a module, by using the attribute
<script type="module">
. - Modules work only via HTTP(s), not locally. So
file://
protocol won't work. - Modules always work in strict mode (
"use strict"
). - Each module has its own top-level scope.
- Two scripts on the same page, both
type="module"
, don't see each other's top-level variables. - To make a variable global, modules can use
window.variable = ...
, which can be then used by other modules. This is not recommended. - A module code is evaluated only the first time when imported.
- There's a rule: Top-level module code should be used for initialization/creation of module-specific internal data structures.
export let admin = { name: "John" };
- If this module is imported from multiple files, the module is only evaluated the first time. The
admin
object is created and then passed to all further importers. - Exports are generated and then they are shared between importers, so if something changes the
admin
object, other modules will see that. - Such behavior is actually very convenient, because it allows us to configure modules.
import.meta
object contains information about the current module. Its content depends upon the environment.- In a module, top-level
this
isundefined
. - Module scripts are deferred, same effect as
defer
attribute.<script type="module" src="...">
doesn't block HTML processing; they load in parallel with other resources.- Module scripts wait until the HTML document is fully ready.
- Relative order of scripts is maintained; scripts that go first execute first.
<script type="module"> ... </script>
vs<script>
: The second script will work first, as the module will load after HTML is fully loaded.
- External scripts with the same
src
run only once. - External scripts that are fetched from another origin require CORS headers.
- No bare modules are allowed. In browsers,
import
must get either a relative or absolute URL. Modules without any path are called"bare"
modules. Such modules are not allowed inimport
. - Most JS style guides don't recommend semicolons after function and class declarations.
- If there's a lot to import, we can use
import * as obj
. - We can
export
at the end or start of the script:export { funOne, funTwo };
. - If an exported function is not imported, the optimizer will remove that code from bundled code. This is called "tree-shaking".
"as"
can be used inexport
likeexport {sayHi as hi, sayBye as bye}
and inimport
likeimport { sayHi as hi, sayBye as bye } from './say.js';
.- Two kinds of modules:
- Modules that contain a library, pack of functions.
- Modules that contain a single entity.
- There may be only one
export default
per file. - When
export class User {...}
is used, we have to import likeimport { User } from ...
. - When
export default class User {...}
is used, we have to import likeimport User from ...
. - A module either has named exports or the default one.
- To export a function separately from its definition:
export { sayHi as default };
. - To import the default export along with named ones:
import { default as User, sayHi } from './user.js';
- To import everything
*
as an object, then thedefault
property is exactly the default export:import * as user from './user.js';
let User = user.default;
new User("John");
- Named exports are explicit; they exactly name what they import, so we can have that information from them; that's a good thing.
- While for a default export, we always choose the name when importing:
import User from './user.js';
import MyUser from './user.js';
- There's a rule that imported variables should correspond to the file name:
import User from './user.js';
import LoginForm from './loginForm.js';
- Re-exporting:
export {sayHi} from './say.js';
export {default as User} from './user.js';
export {login, logout} from './user.js';
- The syntax
export ... from ...
is just a shorter notation for suchimport
-export
. - Re-exporting the default export:
export User from './user.js';
won't work directly. That would lead to a syntax error. We have to writeexport {default as User} from './user.js';
.
- Summary of Exports:
export * from './user.js';
re-exports only named exports, but ignores the default one.export {default} from './user.js';
- Before declaration of a class/function/..:
export [default] class/function/variable
- Standalone export:
export {x [as y], ...}
- Re-export:
export { x [as y], ... } from "module"
export * from "module"
(doesn't re-export default)export {default [as y]} from "module"
(re-export default)
- Importing named exports:
import {x [as y], ...} from "module"
- Importing the default export:
import x from "module"
import {default as x} from "module"
- Import all:
import * as obj from "module"
- Import the module (its code runs), but do not assign any of its exports to variables:
import "module"
- The
import
/export
statements don't work inside{...}
code blocks. - The
import()
expression loads the module and returns a promise that resolves into a module object that contains all its exports. It can be called from any place in the code.import(modulePath).then(obj => {...}).catch(err => {...})
let module = await import(modulePath)
let {hi, bye} = await import('./say.js')
Anomalies
parseInt(0.00001) == parseInt(String(0.00001)) == parseInt('0.00001')
==>0
parseInt(0.00000001) == parseInt(String(0.00000001)) == parseInt('1e-8')
==>1
parseInt
/ parseFloat
Table
value | parseInt | parseFloat |
---|---|---|
'564.239' | 564 | 564.239 |
'+564.239' | 564 | 564.239 |
'-564.239' | -564 | -564.239 |
'564FGL' | 564 | 564 |
'FGL' | NaN | NaN |
'4px 9px' | 4 | 4 |
'(786)' | NaN | NaN |
'0xF' | 15 | 0 |
'056' | 56 | 56 |
564 | 564 | 564 |
Reference for some comparison behavior: https://javascript.plainenglish.io/interviewer-can-a-1-a-2-a-3-ever-evaluate-to-true-in-javascript-d2329e693cde
Comparison Table (A vs B)
B \ A | Undefined | Null | Number | String | Boolean | Object |
---|---|---|---|---|---|---|
Undefined | true | true | false | false | false | IsFalsy(B) |
Null | true | true | false | false | false | IsFalsy(B) |
Number | false | false | A === B | A === ToNumber(B) | A === ToNumber(B) | A === ToPrimitive(B) |
String | false | false | ToNumber(A) === B | A === B | ToNumber(A) === ToNumber(B) | ToNumber(A) === ToPrimitive(B) |
Boolean | false | false | ToNumber(A) === B | ToNumber(A) === ToNumber(B) | A === B | ToNumber(A) === ToPrimitive(B) |
Object | false | false | ToPrimitive(A) === B | ToPrimitive(A) === ToNumber(B) | ToPrimitive(A) === ToNumber(B) | A === B |
Proxy and Reflect
- Proxy: A special object that wraps another object (target) and intercepts fundamental operations applied to it.
new Proxy(target, handler)
:target
is the object to be proxied;handler
is an object containing "traps" (methods) to intercept operations.- Traps: Over 13 methods (
get
,set
,apply
,construct
,has
,deleteProperty
, etc.) that define custom behavior for operations. - Use Cases: Validation, logging, access control, data binding, memoization.
- Allows redefining almost any interaction with an object.
- Reflect: A built-in object that provides methods for interceptable JavaScript operations.
- Not a constructor; all its methods are static.
Reflect.get(target, propertyKey, receiver)
: Performs the default behavior for getting a property.Reflect.set(target, propertyKey, value, receiver)
: Performs the default behavior for setting a property.Reflect.apply(target, thisArgument, argumentsList)
: Performs the default behavior for function calls.- Purpose:
- Provides a functional API for internal JavaScript operations.
- Enables
Proxy
traps to easily call the original behavior of an operation. - Offers a safer way to perform operations that might otherwise throw errors in strict mode (e.g.,
Reflect.set
returnstrue
/false
).
- Often used in conjunction with
Proxy
to provide default or extended trap logic.
Eval: run a code string
eval(codeString)
: Executes a string as JavaScript code.- Dangers:
- Security Risk: Running code from untrusted sources is a major security vulnerability as it can execute malicious scripts.
- Performance: Code executed with
eval
cannot be optimized by JS engines as efficiently as regular code, leading to slower execution. - Debugging: Difficult to debug because the code is not known until runtime.
- Scope Pollution: In non-strict mode, variables declared with
var
or functions can leak into the surrounding scope. In strict mode, they are scoped to theeval
context.
- Alternatives:
new Function(args, codeString)
: A safer alternative for executing code strings, as it runs in its own global lexical environment and does not have access to local variables of the calling context.JSON.parse()
for parsing data (not code).- WebAssembly for performance-critical compiled code.
- General Advice: Avoid
eval
whenever possible. Only use if the input string is fully controlled and trusted.
Currying
Currying: A transformation of functions that converts a function callable as
f(a, b, c)
into a sequence of functions callable asf(a)(b)(c)
.It does not call the function; it transforms it into a new callable form.
Mechanism: Each call in the sequence returns a new function that expects the next argument, until all arguments are provided, at which point the original function is finally executed.
Benefits:
- Reusability: Allows creating specialized functions from more general ones by fixing some arguments.
- Composability: Easier to compose functions together, as each step returns a new function.
- Delayed Execution: Computation is deferred until all necessary inputs are available.
- Readability: Can lead to more readable code for certain patterns.
Implementation Example:
function curry(func) { return function curried(...args) { // If all arguments are collected, call the original function if (args.length >= func.length) { return func.apply(this, args) } else { // Otherwise, return a new function that collects more arguments return function (...args2) { return curried.apply(this, args.concat(args2)) } } } } // Example Usage function sum(a, b, c) { return a + b + c } let curriedSum = curry(sum) console.log(curriedSum(1, 2, 3)) // 6 console.log(curriedSum(1)(2, 3)) // 6 console.log(curriedSum(1)(2)(3)) // 6
func.length
: A function property that returns the number of arguments the function expects.
Reference Type
- Reference Type: A special internal JavaScript type that describes the result of a property access operation (e.g.,
obj.prop
orarr[index]
). - It is not directly accessible or manipulable by developers in their code. It's an internal specification mechanism of the engine.
- Components: A Reference Type value internally consists of:
base
: The object on which the property was accessed.propertyName
: The name of the property.strict
: A boolean flag indicating if the operation occurred in strict mode.
- Purpose & Significance:
- Crucial for correctly handling property assignments: When
obj.prop = value
is executed, the engine first evaluatesobj.prop
to a Reference Type, then uses itsbase
(obj
) andpropertyName
(prop
) to perform the assignment. - Fundamental to
this
binding in method calls: Whenobj.method()
is called, the expressionobj.method
evaluates to a Reference Type wherebase
isobj
. Thisbase
value is then used to setthis
inside themethod
function.
- Crucial for correctly handling property assignments: When
- It helps explain the subtle rules of
this
and property operations that might seem magical otherwise.
BigInt
- BigInt: A primitive data type in JavaScript introduced to represent whole numbers larger than
2^53 - 1
(which isNumber.MAX_SAFE_INTEGER
). - Declaration:
- By appending
n
to an integer literal:12345678901234567890n
- Using the
BigInt()
constructor:BigInt("123")
- By appending
- Key Characteristics:
- Can represent integers of arbitrary precision (limited only by available memory).
- Not interchangeable with
Number
type: MixingBigInt
andNumber
in most arithmetic operations will throw aTypeError
unless one is explicitly converted. - Comparison: Standard comparisons (
==
,===
,<
,>
) work betweenBigInt
andNumber
.===
requires both value and type to be the same. - Division (
/
):BigInt
division truncates any fractional part (e.g.,5n / 2n
results in2n
). - Supports bitwise operations (
&
,|
,^
,~
,<<
,>>
,>>>
).
- Use Cases: Critical for applications requiring very large integer IDs, high-precision timestamps, or cryptographic operations where standard
Number
precision is insufficient. - Performance: Operations on
BigInt
can be slower thanNumber
operations due to the complexity of handling arbitrary precision.
Unicode, String Internals
- String Internals (UTF-16): JavaScript strings are internally represented using UTF-16 encoding.
- Each character is stored as a 16-bit code unit.
- Characters in the Basic Multilingual Plane (BMP) (most common characters) use one 16-bit code unit.
- Characters outside the BMP (e.g., many emojis, ancient scripts) are represented by surrogate pairs, which are two 16-bit code units.
- Unicode: A universal character encoding standard that assigns a unique number (code point) to every character.
- String Length and Iteration:
str.length
: Returns the number of UTF-16 code units. For characters represented by surrogate pairs,length
will count them as 2.for...of
loop: Correctly iterates over actual Unicode characters (code points), handling surrogate pairs as a single character.str.charCodeAt(pos)
: Returns the UTF-16 code unit at the given position. For a surrogate pair, it returns only the first unit.str.codePointAt(pos)
: Returns the full Unicode code point at the given position, correctly handling surrogate pairs. This is the recommended method for character-based operations.String.fromCharCode(codeUnit1, ...)
: Creates a string from one or more UTF-16 code units.String.fromCodePoint(codePoint1, ...)
: Creates a string from one or more Unicode code points.
- Normalization (
str.normalize()
): Converts a string to a canonical form, resolving different Unicode representations of the same character (e.g.,é
can bee
+ combining acute accent or a single precomposed characteré
). Important for correct string comparisons.
WeakRef and FinalizationRegistry
- WeakRef (
Weak Reference
): A special object that allows you to hold a weak reference to another object.new WeakRef(object)
: Creates a weak reference toobject
.weakRef.deref()
: Returns the referenced object, orundefined
if the object has been garbage collected.- Key Concept: A weak reference does not prevent the referenced object from being garbage collected if there are no other strong references to it.
- Use Cases: Caching results or associating extra data with an object without preventing its garbage collection.
- Caution: The exact timing of garbage collection is non-deterministic; do not rely on a
WeakRef
for critical program logic.
- FinalizationRegistry: An object that allows you to register objects to be notified when they are garbage collected.
new FinalizationRegistry(callback)
: Creates a registry. Thecallback
function is invoked when a registered object is garbage collected.registry.register(object, heldValue, [unregisterToken])
: Registers anobject
for finalization.heldValue
: A value passed to thecallback
whenobject
is collected. This value must be a primitive or strongly referenced.unregisterToken
: An optional token to explicitlyunregister
this specific registration later.
registry.unregister(unregisterToken)
: Removes a registration associated with the token.- Key Concept: Provides a way to perform cleanup tasks or release external resources (e.g., closing a file handle) when an object becomes unreachable.
- Caution:
- The
callback
is called after the object is garbage collected, and the timing is non-deterministic. It might be called much later or not at all (e.g., if the program exits). - Avoid creating new strong references to the collected object within the
callback
, as this can resurrect it and cause memory issues. - Cleanup code in the
callback
should be minimal and avoid heavy operations.
- The
- Combined Use:
WeakRef
can be used to check if an object still exists;FinalizationRegistry
can be used to react when it is confirmed to be gone. Both are advanced, low-level tools primarily for library authors dealing with memory management.
Related Posts
Notes on Browser JavaScript
JavaScript in the browser is a dynamic and interactive programming language that runs in web browsers and other software applications that use the JavaScript engine.
Notes on Advanced JavaScript
JavaScript is a dynamic and interactive programming language that runs in web browsers and other software applications that use the JavaScript engine.
Notes on Vibe Coding
A comprehensive guide to understanding the rise of vibe coding in the tech industry.