- Data types
- Numbers
- Strings
- Objects
- Prototype
- Symbol
- Arrays
- Map and Set
- WeakMap and WeakSet
- Functions
- The
this
keyword - Closures
- Classes
- Iterations
- Promise
- Generator
- Async/await
- Event Loop
- ECMAScript
- Module Systems
- Error Handling
- Regular Expression
- Immutability
- Javascript: The Good Parts
- JS engine / Compilation
- Tricks
- Reference
- undefined
- if a variable is declared but not assigned, then its value is
undefined
; - if you want to assign an empty value to a variable, it's better using
null
;
- if a variable is declared but not assigned, then its value is
- null
- a special value representing nothing;
typeof null
isobject
, this is an error in the language;
- boolean
- number
- special numeric values:
Infinity
,-Infinity
,NaN
; - math operations are safe, you can do anthing: divide by zero, treat non-numeric strings as numbers, you may get
NaN
, but the script will not crash;
- special numeric values:
- bigint
- can represent numbers out of the -253 and 253 range;
- define it by adding an
n
to the end of an integer literal:const a = 1234567890123456789012345678901234567890n;
- string
- object
- symbol
- introduced in ES6
-
falsey values:
false
,null
,undefined
,''
,0
,NaN
-
trusey values:
'0'
,'false'
,[]
,{}
, ...
+"42" -> 42;
Number("42") -> 42;
// always use a radix here
parseInt("42", 10) -> 42;
see https://javascript.info/primitives-methods
JS allows you to use some primitives as objects, such as in 'abc'.toUpperCase()
, JS creates wrapper objects internally to accomplish this;
- Object wrappers:
String
,Number
,Boolean
andSymbol
; - They can be used to convert a value to the corresponding type:
Number('2.5')
; - But don't use them as constructors;
Example:
typeof 'abc'; // 'string'
typeof String('abc'); // 'string'
'abc' === String('abc'); // true
s = new String('abc'); // [String: 'abc']
typeof s; // 'object'
s === 'abc'; // false
-
can be:- unary, negate a number;
- binary, subtract one number from another;
+
can be:- unary, convert a value to number, same as
Number()
; - binary, sums two number;
- binary, concatenate two strings;
- unary, convert a value to number, same as
=
is an operator as well, it assigns a value to a variable and returns the value;,
is an operator dividing several expressions, the value of the last one is returned;
- JS uses double precision floating point numbers;
- It's 64-bit, 52 bits for digits, 11 for the position of the decimal point, and 1 for the sign, can represent numbers between -253 and 253;
const n = 4;
n.toString(2);
// 100
// opposite operation
parseInt('100', 2);
// 4
use str.localeCompare
to compare strings properly:
'Zealand' > 'Österreich';
// false
'Zealand'.localeCompare('Österreich');
// 1
JS uses UTF-16 as internal format for strings, most frequently used characters have 2-byte codes, that covers 65536 symbols, some rare symbols are encoded with a pair of 2-byte characters called a surrogate pair;
The first character of a surrogate pair has code in range 0xd800..0xdbff
, the second one must be in range 0xdc00..0xdfff
, these intervals are reserved for surrogate pairs, so they should always come in pairs, an individual one means nothing;
Surrogate pairs didn't exist when JS was created, so they are not processed correctly sometimes:
'a'.length;
// 1
const s = '𩷶';
// one symbol, but the length is 2
s.length;
// 2
// `slice` doesn't work properly
s.slice(0, 1);
// '�'
// #### `charCodeAt/fromCharCode` are not surrogate-pair aware, you can get each code in the pair
s.charCodeAt(0).toString(16);
// 'd867'
s.charCodeAt(1).toString(16);
// 'ddf6
The following functions work on surrogate pairs correctly:
// #### the newer functions `codePointAt/fromCodePoint` are surrogate-pair aware
s.codePointAt(0).toString(16);
// '29df6'
s.split(); // [ '𩷶' ]
Array.from(s); // [ '𩷶' ]
for (let char of s) {
console.log(char);
}
// 𩷶
// #### get the correct length of a string
Array.from(s).length; // 1
In Unicode, there are characters that decorate other characters, such as diacritical marks. For example, \u0307
adds a 'dot above' the preceding character, \u0323
means 'dot below'.
'S\u0307';
// 'Ṡ'
'S\u0307\u0323';
// 'Ṩ'
This causes a problem: a symbol with multiple decorations can be represented in different ways.
const s1 = 'S\u0307\u0323';
const s2 = 'S\u0323\u0307';
// s1 and s2 looks the same 'Ṩ', but they are not equal
s1 === s2; // false
There is a "unicode normalization" algorithm that brings each string to a single "normal" form.
// #### 'Ṩ' has its own code \u1e68 in Unicode
const normalizedS = 'S\u0307\u0323'.normalize();
normalizedS.length; // 1
normalizedS.codePointAt(0).toString(16); // '1e68'
// #### 'Q̣̇' don't have its own code, normalization will put \u0323 before \u0307
const normalizedQ = 'Q\u0307\u0323';
normalizedQ.length; // 3
normalizedQ.codePointAt(0).toString(16); // '51'
normalizedQ.codePointAt(1).toString(16); // '323'
normalizedQ.codePointAt(2).toString(16); // '307'
-
Object literal
let circle = { radius: 2; };
-
The
Object
constructorlet circle = new Object(); circle.radius = 2;
-
Constructor function
let Circle = function(radius) { this.radius = radius; this.area = function() { return Math.PI * this.radius * this.radius; }; }; let circle = new Circle(2);
- It's a good practice to capitalize the first letter of constructor name and always call it with
new
; - constructor function return
this
if no explicitreturn
- It's a good practice to capitalize the first letter of constructor name and always call it with
-
Object.create
let shape = { x: 0 }; // #### create an object using shape as the prototype let circle = Object.create(shape, { radius: { value: 10 } }); c.radius; // 10 // #### copy all properties and the right [[Prototype]] let clone = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) );
a = { 64: 'NZ', 1: 'US', name: 'gary', age: 20 };
Object.keys(a);
// ["1", "64", "name", "age"]
Integer properties are ordered, others appear in creation order, so 1
comes before 64
when iterating through all the properties;
A property key can only be a string or a symbol, when you use a number as property key, it's converted to a string;
writable
enumerable
configurable
whether the property can be deleted and flags can be modified:- If a property is non-configurable, you can't change it back;
- A non-configurable property can still be writable;
You can get these flags by Object.getOwnPropertyDescriptor
and change them using Object.defineProperty
There are Object.getOwnPropertyDescriptors
and Object.defineProperties
, which allows you to define and access multiple properties' flags at once;
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
This allows you to copy all properties of an object, including symbolic properties and all property flags.
Object.preventExtensions(obj)
: forbids adding new properties;Object.seal(obj)
: forbids adding/removing of properties, setsconfigurable: false
for all properties;Object.freeze(obj)
: forbids adding/removing/changing of properties, setsconfigurable: false, writable: false
for all properties;
There are methods to check an objects
There are two kinds of properties:
Data properties | Accessor properties | |
---|---|---|
Unique Descriptors | value, writable | get, set |
- A property can be either a data property or an accessor property, not both;
Usages:
- Use an accessor property as a wrapper for a data property, so you can control what value is allowed for the data property;
- For compatiblity;
'use strict';
// #### define fullName as an accessor property
var person = {
firstName: 'Gary',
lastName: 'Li',
age: 20,
get fullName() {
return this.firstName + ' ' + this.lastName;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(' ');
}
};
person.fullName;
// 'Gary Li'
person.fullName = 'Joe Doe';
// 'Joe Doe'
// #### can't define a getter function for an existing data property, this has no effect
Object.defineProperty(person, 'age', {
get age() {
return 100;
}
});
// #### use defineProperty
let o = {};
Object.defineProperty(o, 'name', {
get: function() {
// ...
},
set: function(value) {
// ...
}
});
Refactor the person object above, add a birthday
property, and convert age
to an accessor property for compatibility, so it's still available:
var person = {
firstName: 'Gary',
lastName: 'Li',
birthday: new Date('2000-01-01'),
get age() {
return new Date().getFullYear() - this.birthday.getFullYear();
}
// ...
};
Use Object.entries
and Object.fromEntries
to convert an object to and from an array:
const ages = { gary: 20, jack: 30 };
const newAges = Object.fromEntries(
Object.entries(ages).map(([key, value]) => [key, value + 1])
);
// { gary: 21, jack: 31 }
- In JS, objects have a special hidden property
[[Prototype]]
, which can benull
or another object; __proto__
is an accessor property ofObject.prototype
, a historical getter/setter for[[Prototype]]
, you should use newer functionsObject.getPrototypeOf/Object.setPrototypeOf
when possible;- Although you can get/set
[[Prototype]]
at anytime, but usually we only set it once at the object creation time, changing an object's prototype is very slow and will break internal optimizations;
Douglas Crockford's video course: Prototypal Inheritance
function Gizmo(id) {
this.id = id;
}
Gizmo.prototype.toString = function() {
return 'gizmo ' + this.id;
};
let g = new Gizmo(1);
-
Gizmo
is a function object, which has aprototype
property; -
Gizmo.prototype
has aconstructor
property pointing back toGizmo
,[[Prototype]]
pointing toObject.prototype
; -
g
is a plain object, does not have aprototype
property, its[[Prototype]]
points toGizmo.prototype
g.__proto__ === Gizmo.prototype; // true Gizmo.prototype.__proto__ === Object.prototype; // true
In general:
-
Every function (not arrow functions) has a
prototype
property, this is a normal property, not the hidden[[Prototype]]
property; -
Foo.prototype
is used to build the prototype chain,(new Foo).__proto__ === Foo.prototype
; -
When using
new Foo()
to create an object, the functionFoo
will always be run, althoughFoo.prototype
may have been changed to point to something else;function Animal(name, age) { this.name = name; this.age = age; } Animal.prototype.constructor === Animal; // 'constructor' points to the function now // true Animal.prototype = { x: 10 }; // 'prototype' can be changed to point to another object Animal.prototype.constructor === Animal; // Animal.prototype.constructor does not point to Animal anymore // false a = new Animal('Snowball', 5); // Animal is always used to create the object // { name: 'Snowball', age: 5 } a.__proto__; // a.__proto__ always points to Animal.prototype // { x: 10 }
Another illustration created by myself:
let user = {
name: 'John',
surname: 'Smith',
set fullName(value) {
[this.name, this.surname] = value.split(' ');
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
// admin inherits from user
let admin = {
__proto__: user,
isAdmin: true
};
admin.fullName = 'Gary Li';
// 'Gary Li'
admin;
// { isAdmin: true, name: 'Gary', surname: 'Li' }
user;
// { name: 'John', surname: 'Smith', fullName: [Getter/Setter] }
When you call admin.fullName
, this
refers to admin
, not user
, so name
and surname
is added to admin
;
No matter where the method is found: in an object or its prototype. In a method call, this
is always the object before the dot.
Native constructors have their own prototypes:
Object.prototype
Function.prototype
Array.prototype
Date.prototype
Number.prototype
String.prototype
Boolean.prototype
Symbol.prototype
- For primitive values such as numbers, strings and booleans, when you try to access their properties, temporary wrapper objects are created using built-in constructors
String
,Number
andBoolean
; null
andundefined
don't have wrapper objects and prototypes;
It's generally a bad idea to modify a native prototype except for polyfilling:
if (!String.prototype.repeat) {
// if there's no such method add it to the prototype
String.prototype.repeat = function(n) {
// actually, the code should be a little bit more complex than that
// (the full algorithm is in the specification)
// but even an imperfect polyfill is often considered good enough
return new Array(n + 1).join(this);
};
}
alert('La'.repeat(3)); // LaLaLa
// #### o is an array-like object
// #### it doesn't have methods from Array.prototype
let o = { 0: 'Hello', 1: 'world', length: 2 };
// #### 1) call the prototype method directly
Array.prototype.join.call(o, ' | ');
// 'Hello | world'
// #### 2) borrow the prototype method
o.join = Array.prototype.join;
o.join(' | ');
// 'Hello | world'
Normally an object has [[Prototype]]
, and you can access it thru the accessor property __proto__
. But, if you want to use an object as a dictionary, then __proto__
can't be used as a key, to avoid this, either:
-
Use
Map
; -
Or use a "very plain" or "pure dictionary" object:
// #### use null as [[Prototype]] to create a very plain object let obj = Object.create(null); obj.__proto__ = 10; // now __proto__ becomes a plain property obj.toString(); // but it doesn't have access to Object.prototype methods anymore // TypeError
ref: ES6 Symbols in Depth
Symbol is a new primitive value type in ES6, there are three different flavors of symbols - each flavor is accessed in a different way:
-
Local symbols
Create a local symbol:
let s = Symbol('gary symbol'); console.log(s.description); // 'gary symbol'
-
'gary symbol' is a description of the symbol, it's just for debugging purpose;
-
you can NOT use
new Symbol()
to create a symbol value -
local symbols are immutable and unique
Symbol() === Symbol(); // false
-
-
Global symbols
these symbols exist in a global symbol registry, you can get one by using
Symbol.for()
(create if absent):let s = Symbol.for('Gary');
-
it's idempotent, which means for any given key, you will always get the exactly same symbol:
Symbol.for('Gary') === Symbol.for('Gary');
-
get the key of a symbol:
let key = Symbol.keyFor(s);
-
-
"Well-known" symbols
-
They exist across realms, but you can't create them and they're not on the global registry;
-
These actually are NOT well-known at all, they are JS built-ins, and they are used to control parts of the language, they weren't exposed to user code before ES6;
-
Refer to this MDN - Symbol page to see a full list:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.match
Symbol.prototype
Symbol.replace
Symbol.search
Symbol.species
Symbol.split
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables
-
-
As property keys
as each symbol is unique, it can be used to avoid name clashes: if you do
obj[Symbol('id')] = 1;
, you are guaranteed it won't overwrite anything; -
Privacy ?
symbol keys can not be accessed by
Object.keys
,Object.getOwnPropertyNames
,JSON.stringify
, andfor..in
loopslet obj = { [Symbol('name')]: 1, [Symbol('name')]: 2, [Symbol.for('age')]: 10, color: 'red' }; Object.keys(obj); // [ 'color' ] console.log(Object.getOwnPropertyNames(obj)); // [ 'color' ] console.log(JSON.stringify(obj)); // {"color":"red"} for (let key in obj) { console.log(key); } // color
but you can access them thru
Object.getOwnPropertySymbols
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(name), Symbol(name), Symbol(age) ]
-
Defining Protocols
just like there's
Symbol.iterator
which allows you to define how an object can be iterated
One of the most useful symbols, can be used to make an object iterable, it's just like implementing the Iterable
interface in other languages, see the Iterations section
see https://javascript.info/object-toprimitive
When an object is used in a context where a primitive value is expected, JS tries to:
- Call
obj[Symbol.toPrimitive](hint)
, if such method exists; - Otherwise if hint is "string"
try
obj.toString()
andobj.valueOf()
, whatever exists; - Otherwise if hint is "number" or "default"
try
obj.valueOf()
andobj.toString()
, whatever exists;
const gary = {
name: 'Gary',
age: 20,
[Symbol.toPrimitive](hint) {
console.log(hint);
return hint === 'string' ? `{name: ${this.name}}` : this.age;
}
};
console.log(`${gary}`);
// string
// {name: Gary}
console.log(gary + 30);
// default
// 50
console.log(gary * 2);
// number
// 40
-
An array is a special kind of object, the syntax
arr[index]
is esentially the same asobj[key]
; -
JS engines do optimizations for arrays, but if you use an array as a regular object, those optimizations will be turned off, so don't do:
// add a non-numeric property arr.test = 5; // make holes const arr2 = []; arr2[100] = 100;
-
splice
can be used to remove, insert, replace elements of an array
arr.splice(index[, deleteCount, elem1, ..., elemN])
a = ['Amy', 'Gary', 'Jack', 'Zoe']; // #### removing a.splice(1, 1); // [ 'Gary' ] a; // [ 'Amy', 'Jack', 'Zoe' ] // #### replacing a.splice(2, 1, 'Zolo'); // [ 'Zoe' ] a; // [ 'Amy', 'Jack', 'Zolo' ] // #### inserting a.splice(2, 0, 'Nick', 'Peter'); // [] a; // [ 'Amy', 'Jack', 'Nick', 'Peter', 'Zolo' ]
-
length
is not actually the count of values in the array, but the greatest numeric index plus one;const a = []; a[99] = 'nighty nine'; // NOTE: we should not leave holes in an array like this a.forEach(x => console.log(x)); // nighty nine a.length; // 100
-
length
is writable, so you can clear an array by setting itslength
to 0const a = ['gary', 'jack', 'nick']; a.length = 1; a; // [ 'gary' ] a.length = 0; a; // []
If an object has indexed properties and length
is an array-like object, such as strings and arguments
;
You can create one yourself, you can access it's property like an array arrLike[0]
, but it doesn't have methods like pop
, push
etc;
// #### Create an array-like object
const arr = {
0: 'gary',
1: 'jack',
length: 2
};
const names = ['amy'];
names.concat(arr);
// [ 'amy', { '0': 'gary', '1': 'jack', length: 2 } ]
// #### Make the array-like object spreadable in concatenation
arr[Symbol.isConcatSpreadable] = true;
names.concat(arr);
// [ 'amy', 'gary', 'jack' ]
Array.from()
can turn an array-like or iterable object into a real array, see below.
- https://itnext.io/heres-why-mapping-a-constructed-array-doesn-t-work-in-javascript-f1195138615a
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
let a = Array(100)
.fill()
.map((e, i) => i);
/*
`Array(100)` creates an empty array, which don't have any value, but a `length` property;
`fill()` creates all the elements, all of them are `undefined`;
`map()` creates a new array;
*/
// or
let a = Array.from({ length: 100 }, (e, i) => i);
-
Map is a collection of keyed data items, like
Object
, the main difference is thatMap
allows keys of any type;const gary = { name: 'Gary' }; const myMap = new Map(); myMap.set(gary, 1); myMap.get(gary); // 1
-
Map uses the algorithm SameValueZero to test keys for equivalence, roughly the same as strict equality
===
, but it considersNaN
equal toNaN
as well; -
Iteration
map.keys()
map.values()
map.entries()
gets an array of[key, value]
for..of
iterates over[key, value]
pairsforEach((value, key, map) => {...})
-
Maps from/to objects
const obj = { name: 'Gary', age: 20 }; const myMap = new Map(Object.entries(obj)); // Map { 'name' => 'Gary', 'age' => 20 } const newObj = Object.fromEntries(myMap); // { name: 'Gary', age: 20 }
-
For compatiblity, all iteration methods on map are also available for set, values are used as keys:
map.keys()
gets valuesmap.values()
gets values as wellmap.entries()
gets an array of[value, value]
for..of
iterates over[value, value]
pairsforEach((value, value, map) => {...})
JS engines clear unreachable objects from memory.
let gary = { name: 'Gary' };
// overwrite the reference
gary = null;
// then there is no reference to the object, it's unreachable, so the object will be cleared from the memory
If an object is in an array, or used as a map key, while the array/map is alive, it won't be cleared:
let gary = { name: 'Gary' };
let jack = { name: 'Jack' };
let myArray = [gary];
let myMap = new Map();
myMap.set(jack, 1);
// overwrite the reference
gary = null;
jack = null;
// the array and map are still alive, so the objects won't be cleared
- WeakMap keys must be objects;
- WeakMap does not support
size
,keys()
,values()
andentries()
, so you can't get all keys or values from it; - For WeakMap keys, if there are no other references to them, they will be garbage collected;
// cache.js
let cache = new WeakMap();
// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculate the result for */ obj;
cache.set(obj, result);
}
return cache.get(obj);
}
// main.js
let obj = {
/* some object */
};
let result1 = process(obj);
let result2 = process(obj); // the result is cached
// when obj set to null, it will be cleared from the WeakMap cache as well
obj = null;
- Only allow objects;
- Supports
add
,has
anddelete
, but notsize
and iteration methods such askeys()
, etc; - If an element doesn't have other references, it will be cleared from the WeakSet;
Usage: keep track those who visited a site:
let visitedSet = new WeakSet();
let john = { name: 'John' };
let pete = { name: 'Pete' };
let mary = { name: 'Mary' };
visitedSet.add(john); // John visited us
visitedSet.add(pete); // Then Pete
visitedSet.add(john); // John again
// check if John visited?
visitedSet.has(john); // true
// check if Mary visited?
visitedSet.has(mary); // false
john = null;
// John will be cleared from the set
- A function is a value representing an "action";
typeof
a function isfunction
, but it's just a special type ofobject
;
For a function statement, its definition is hoisted to the top.
console.log(typeof statementFoo); // function
statementFoo(); // NOTE this function runs fine here
console.log(typeof expressionFoo); // undefined
expressionFoo(); // NOTE throws an error, expressionFoo is still undefined here
// function statement/declaration
function statementFoo() {
console.log('an statement function');
}
// function expression
var expressionFoo = function() {
console.log('an expression function');
};
If a function statement/declaration is inside a code block (e.g. if
block):
- In unstrict mode, the function name is hoisted, it's visible outside of the code block, but it's value would be empty until the declaration, like
var
; - In strict mode, the function is block-scoped, it's only visible inside the block, like
let
;
let foo = function(a, b, ...rest) {
// do something
};
Object.getOwnPropertyNames(X);
// [ 'length', 'name', 'arguments', 'caller', 'prototype' ]
foo.length; // the ...rest parameter doesn't count
// 2
foo.name;
// 'foo'
let foo = function hello(name) {
if (name) {
console.log(`Hello ${name}`);
} else {
hello('Guest');
}
};
foo.name;
// 'hello'
foo();
hello
is the name of the function;- It allows the function to call itself;
- It is only visible inside the function;
There is no way to add an "internal" name for a function statement.
Each function receives two pseudo parameters: arguments
and this
;
argument |
...rest parameters |
---|---|
all arguments | only rest arguments |
array-like, iterable | array |
Always use ...rest parameters when possible
// use arguments to create a function with variable length parameters
function sum() {
var i,
n = arguments.length,
total = 0;
for (i = 0; i < n; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1, 2, 3, 4));
// with rest syntax
function sum(...args) {
return args.reduce((total, e) => total + e, 0);
}
console.log(sum(1, 2, 3, 4));
- Do not have
this
orsuper
; - Do not have
arguments
; - Can't be called with
new
;
They don't have their own "context", but rather work in the current one.
function defer(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms); // `this` and `arguments` come from outer context
};
}
function sayHi(who) {
alert('Hello, ' + who);
}
let sayHiDeferred = defer(sayHi, 2000);
sayHiDeferred('John'); // Hello, John after 2 seconds
Without an arrow function, it would look like:
function defer(f, ms) {
return function(...args) {
let ctx = this;
setTimeout(function() {
return f.apply(ctx, args); // pass in `this` and `arguments` from outer context
}, ms);
};
}
Every function receives an implicit this
parameter, which is bound at invocation time
Four ways to call a function:
-
Function form
foo(arguments);
this
binds to the global object, which cause problems- in ES5/Strict,
this
binds toundefined
- outer
this
is not accessible from inner functions, usevar that = this;
to pass it
-
Method form
thisObject.methodName(arguments); thisObject['methodName'](arguments);
this
binds tothisObject
CAUTION if you assign the method to a variable and call it, it doesn't have access to
this
const a = { name: 'gary', sayHi() { console.log('Hi ' + this.name); } }; // {name: "gary", sayHi: ƒ} a.sayHi(); // Hi gary const foo = a.sayHi; foo(); // `this` is undefined in foo
So, although
foo === a.sayHi
, buta.sayHi()
hasa
asthis
,foo()
doesn't, see https://javascript.info/object-methods#internals-reference-type -
Constructor form
new Foo(arguments);
a new object is created and assigned to
this
, if not an explicit return value, thenthis
will be returned -
Apply form
foo.apply(thisObject, arguements); foo.call(thisObject, arg1, arg2, ...);
explicitly bind an object to 'this'
-
this
scope examplevar person = { name: 'Gary', hobbies: ['tennis', 'badminton', 'hiking'], print: function() { // when run person.print(), `this` is person here this.hobbies.forEach(function(hobby) { // but 'this' is undefined here console.log(this.name + ' likes ' + hobby); }); }, // use '_this' to pass the correct context this in print2: function() { var _this = this; console.log("// use '_this' to pass the correct context this in"); this.hobbies.forEach(function(hobby) { console.log(_this.name + ' likes ' + hobby); }); }, // use 'bind' to get the correct this print3: function() { console.log("// use 'bind' to get the correct this"); this.hobbies.forEach( function(hobby) { console.log(this.name + ' likes ' + hobby); }.bind(this) ); }, // recommended way: use arrow function, which uses `this` from the outer context print4: function() { console.log('// use arrow function syntax'); this.hobbies.forEach(hobby => { console.log(this.name + ' likes ' + hobby); }); } };
When a function gets declared, it contains a function definition and a closure. The closure is a collection of all the variables in scope at the time of creation of the function.
Think of a closure as a backpack, it is attached to the function, when a function get passed around, the backpack get passed around with it.
var digit_name = (function() {
var names = [
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine'
];
return function(n) {
return names[n];
};
})();
console.log(digit_name(2));
A Tricky JavaScript Interview Question Asked by Google and Amazon
// interviewer: what will the following code output?
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}, 300);
}
output:
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
when the anonymous function executes, the value of i
is 4
You can fix this by:
-
Adding a separate closure for each loop iteration, in this case, the
i
is separate for each closure;const arr = [10, 12, 15, 21]; for (var i = 0; i < arr.length; i++) { setTimeout( (function(i) { return function() { console.log('The index of this number is: ' + i); }; })(i), 300 ); }
-
Or using
let
, which creates a new block binding for each iterationlet
is block scoped, a new 'backpack' is created for each iteration, in contrast,var
is function scoped, so thei
is shared in the first example;- Although
let i
is outside of{...}
, butfor
construct is special, the declaration is still considered a part of the block; - Read more here: http://exploringjs.com/es6/ch_variables.html#sec_let-const-loop-heads
const arr = [10, 12, 15, 21]; for (let i = 0; i < arr.length; i++) { setTimeout(function() { console.log('The index of this number is: ' + i); }, 300); }
See let - MDN for details
var
declarations will be hoisted to the top of function scope, but the value assignment is not, so the value isundefined
before assignment;let
bindings are created at the top of the block scope, but unlikevar
, you can't read or write it, you get aReferenceError
if using it before the declaration;
function do_something() {
console.log(bar); // undefined
console.log(baz); // undefined
console.log(foo); // ReferenceError, in 'Temporal Dead Zone'
var bar = 1;
if (false) {
var baz = 10; // this will never be executed, but the `baz` declaration is still hoisted
}
let foo = 2;
}
the foo
in (foo + 55)
is the foo
in the if
block, not the foo
declared by var
function test() {
var foo = 33;
if (true) {
let foo = foo + 55; // ReferenceError
}
}
test();
-
In JS, every function, code block
{...}
and the script as whole have an internal associated object known as theLexical Environment
, which saves all local variables and a reference to the outer lexical environment; -
All functions have a hidden property
[[Environment]]
, which remembers the Lexical Environment in which the function was created; -
After
makeCounter
finishes, the lexical environment is still available thrucounter.[[Environment]]
, so it won't be garbage collected, if you docounter = null;
, then the lexical environment will be cleared; -
For
for (let i = 0; i < 10; i++){ }
, a new lexical environment is created for every run of the code in{...}
, each one has its owni
variable; -
In theory, all outer variables of a function should be available as long as the function is alive, but some JS engines (V8) try to optimize that, a side effect is that such variable will become unavailable in debugging;
class MyClass {
consturctor(name) {
this.name = name;
}
show() {
console.log(this.name);
}
}
typeof MyClass; // MyClass is actually a function
// 'function'
MyClass.prototype.constructor === MyClass;
// true
MyClass.prototype.show; // class method is on the class prototype
// [Function: show]
The class MyClass {}
construct actually creates a function, it's very similar to creating a constructor function function MyClass () {}
, with a few differences:
- A class function is labelled by a special internal property
[[FunctionKind]]:"classConstructor"
, it must by called withnew
; - Class methods are non-enumerable;
- All code inside the class construct is always in strict mode;
class User {
age = 20;
constructor(name) {
// invokes the setter
this.name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
alert('Name is too short.');
return;
}
this._name = value;
}
}
let user = new User('John');
user;
// User { age: 20, _name: 'John' }
- Class property
age
is not inUser.prototype
, instead it is created bynew
before calling the constructor, it's a property of the created objectuser
; name
is an accessor property ofUser.prototype
, if an accessor only has the getter, not the setter, then it's read-only;_name
is in the created object;
class Animal {
constructor(name) {
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.name = name;
}
// ...
}
Rabbit.prototype.__proto__ === Animal.prototype;
// true
Rabbit.__proto__ === Animal;
// true
Animal.__proto__ === Function.prototype;
// true
Not only Rabbit.prototype
extends Animal.prototype
, Rabbit
itself extends Animal
as well !
Although Date.prototype
extends Object.prototype
, but Date
doesn't extend Object
, so there is Object.keys()
but no Date.keys()
;
An inheriting class doesn't need to define a constructor explicitly, but if it does, then it must call super()
and do it before using this
, because a derived constructor has a special internal label: [[ConstructorKind]]:"derived"
, which affects its behavior with new
:
- When a regular function is executed with
new
, it creates an empty object and assigns it tothis
; - But when a derived consturctor runs, it expects the parent constructor to create
this
;
- JS has another internal property for functions called
[[HomeObject]]
, when a function is specified as a class or object methods, its[[HomeObject]]
points to that object,super
uses this to resolve the parent prototype; - Function properties don't have
[[HomeObject]]
; - Arrow functions don't have
super
;
let animal = {
name: 'Animal',
sleep: function() {
// this is a function property, not a method
},
eat() {
// animal.eat.[[HomeObject]] == animal
alert(`${this.name} eats.`);
},
run() {
console.log('Running');
}
};
let rabbit = {
__proto__: animal,
name: 'Rabbit',
sleep: function() {
// this is a function property, not a method,
// no [[HomeOjbect]], you can't call `super.sleep()` here
},
eat() {
// rabbit.eat.[[HomeObject]] == rabbit
super.eat();
},
run() {
// arrow functions don't have its own `this` or `super`, it's gettings `super` from the context
setTimeout(() => super.run(), 1000);
}
};
class Article {
static publisher = 'Foo Books';
constructor(title, date) {
this.title = title;
this.date = date;
}
static createTodays() {
// remember, this = Article
return new this("Today's digest", new Date());
}
}
let article = Article.createTodays();
- Static properties/methods belong to the class (constructor function), not the created object instance;
- It's a good way to create factory method, use
new this()
in a static method to create an instance; - When
class Child extends Parent { }
, thenChild.__proto__ === Parent
, so all static properties/methods are inherited byChild
;
In JS, a class can only extends one other class, if there is something else you want to extend, you can use a "mixin".
let sayMixin = {
say(phrase) {
alert(phrase);
}
};
// **sayHiMixin extends sayMixin**
let sayHiMixin = {
__proto__: sayMixin, // (or we could use Object.create to set the prototype here)
sayHi() {
// call parent method
super.say(`Hello ${this.name}`); // (*)
},
sayBye() {
super.say(`Bye ${this.name}`); // (*)
}
};
class User {
constructor(name) {
this.name = name;
}
}
// ** copy the methods **
Object.assign(User.prototype, sayHiMixin);
// now User can say hi
new User('Dude').sayHi(); // Hello Dude!
super
in sayHiMixin
methods always refers to sayMixin
Iterations over any iterables: Objects, Arrays, strings, Maps, Set etc.
-
Array.from()
converts any iterable or array-like value into an array; -
...
spread operator works on any iterable; -
Object.keys
,Object.values
andObject.entries
let o = { 5e5: '$500K', 1e6: '$1M', 2e6: '$2M' }; Object.keys(o); // [ '500000', '1000000', '2000000' ] Object.values(o); // [ '$500K', '$1M', '$2M' ] Object.entries(o).forEach(([k, v]) => { console.log(k, v); }); // 500000 $500K // 1000000 $1M // 2000000 $2M
-
for..of
andfor..in
'use strict'; let characters = ['Jon', 'Sansa', 'Arya', 'Tyrion', 'Cercei']; for (let c of characters) { console.log(c); } // Jon // Sansa // Arya // Tyrion // Cercei // for..in for (let c in characters) { console.log(c); } // 0 // 1 // 2 // 3 // 4 // loop an object const obj = { name: 'gary', age: 20, [Symbol('a')]: 'a symbol' }; obj.__proto__.job = 'IT'; for (let k in obj) { console.log(k); } // name // age // job
Note:
for..in
is optimized for generic objects, not arrays, it's slower thanfor..of
on arrays;for..in
iterates over all properties, it gets keys from the prototype chain as well, but not symbol properties;- If a property is not enumerable, it will not be listed, that's why you don's see properties from
Object.prototype
; for..of
works on any iterable value;
-
Custom iterator
You can add a custom iterator to an object:
-
Using the
Symbol.iterator
property, which should be a function, this function executes once when the iteration starts, and returns an object containing anext
method; -
This
next
method should instead return an object that contains two properties:done
andvalue
, thedone
property is checked to see if the iteration finished;
Example
// NOTE you can define a custom iteration function for an object 'use strict'; // a custom id maker that generates ids from 100 to 105 let idMaker = { [Symbol.iterator]() { let currentId = 100; let maxId = 105; return { next() { return { done: currentId > maxId, value: currentId++ }; } }; } }; for (let id of idMaker) { console.log(id); } // 100 // 101 // 102 // 103 // 104 // 105 // NOTE another way to iterate through the id maker object let iter = idMaker[Symbol.iterator](); let next = iter.next(); while (!next.done) { console.log(next.value); next = iter.next(); } // 100 // 101 // 102 // 103 // 104 // 105
-
JS uses callbacks a lot, if not handled properly, it will lead to Callback Hell, Promise was introduced in ES6, it's a way to simplify asynchronous programming by making code look synchronous and avoid callback hell.
A Simple Guide to ES6 Promises
When there are multiple nested callbacks, the code becomes quite hard to read and understand
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
});
}
});
}
});
rewrite loadScript
to return a promise:
loadScript('1.js')
.then(() => loadScript('2.js'))
.then(() => loadScript('3.js'))
.catch(e => {
// handle error
});
Please see here for detailed examples about when a promise is resolved or rejected: https://github.com/garylirocks/js-es6/tree/master/promises
Take note:
- It is recommended to only pass the resolved callback to
.then()
, use.catch()
to handle errors; - Always use a
.catch()
;
'use strict';
// NOTE define an infinite generator
let idMaker = function*() {
let nextId = 100;
while (true) {
yield nextId++;
}
};
// NOTE generator function returns an iterable
for (let id of idMaker()) {
if (id > 105) {
break;
}
console.log(id);
}
you can even yield into another iterable within a generator:
// NOTE yield another iterable in a generator
let myGenerator = function*() {
yield 'start';
yield* [1, 2, 3]; // <- yield into another iterable
yield 'end'; // <- back to the main loop
};
for (let i of myGenerator()) {
console.log(i);
}
// start
// 1
// 2
// 3
// end
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
// yay, can use await!
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
(async () => {
let generator = generateSequence(1, 5);
for await (let value of generator) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
})();
- Use
async
; - Use
for await..of
to iterate thru the results;
-
Sync iterators
let range = { from: 1, to: 5, *[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*() for (let value = this.from; value <= this.to; value++) { yield value; } } }; alert([...range]); // 1,2,3,4,5
-
Async iterables
let range = { from: 1, to: 5, async *[Symbol.asyncIterator]() { // same as [Symbol.asyncIterator]: async function* () for (let value = this.from; value <= this.to; value++) { // wait 1 second await new Promise(resolve => setTimeout(resolve, 1000)); yield value; } } }; (async () => { for await (let value of range) { alert(value); // 1, then 2, then 3, then 4, then 5 } })();
async function f() {
return 1;
}
f().then(alert); // 1
async
makes the following function return a promise.
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function f1() {
try {
// the compiler pauses here, when the promise resolves, the value is assigned to x,
// if the promise is rejected, an error is thrown
var x = await resolveAfter2Seconds(10);
console.log(x); // 10
} catch (e) {
console.log(e);
}
}
f1();
See the Pen async/await
await
can only be used inasync
functionsawait
is followed by a Promise, if it resolves, it returns the resolved value, or it can throw an error
Refs:
- Concurrency model and Event Loop - JavaScript | MDN
- Event loop: microtasks and macrotasks - Javascript.info
- What the heck is the event loop anyway? | Philip Roberts | JSConf EU
- Jake Archibald: In The Loop
- Jake Archibald: The compositor thread
- Further Adventures of the Event Loop - Erin Zimmer - JSConf EU 2018
Event loop pseudo code (browser only, it's different in Node):
while (true) {
// macro tasks, run one
queue = getNextQueue();
task = queue.pop();
execute(task);
// microtass queue, run until exhausted including newly added microtasks
while(microtaskQueue.hasTasks()) {
doMicrotask();
}
// render steps and requestAnimationFrame queue
if (isRepaintTime()) {
// run everything currently in the queue
// newly added ones will be run next time
rafQueue = rafQueue.copy();
for (task in rafQueue) {
doRafTask(task)
}
repaint();
}
}
Differences in Node:
- No script parsing events;
- No user interactions;
- No render steps and
requestAnimationFrame
; - In browser it runs infinitely, in Node, it exits if nothing left;
- JS is single threaded, it relies on environment provided APIs to handle asynchronous actions.
- The browser provides APIs for DOM, network request and timer, etc.
- JS runtime uses a message queue, each message has an associated function which gets called(put in the stack) to handle the message.
- Once the stack is empty, the oldest item in the message queue is put in the stack.
- Run-to-completion: each task is processed completely before any other message is processed, so it won't be interrupted by other async tasks.
- When a new script is loaded, it's added as a new task to the queue to be parsed.
-
The time argument for
setTimout
only indicates the minimum delay after which the message will be pushed into the queue, it only runs when other messages before it have been cleared;const s = new Date().getSeconds(); setTimeout(function() { // prints out "2", meaning that the callback is not called immediately after 500 milliseconds. console.log('Ran after ' + (new Date().getSeconds() - s) + ' seconds'); }, 500); while (true) { if (new Date().getSeconds() - s >= 2) { console.log('Good, looped for 2 seconds'); break; } }
-
Zero-delay timeout, is not actually 0ms, there's a minimal delay of around 4ms, and subject to whether there are other tasks in the queue
setTimeout(func, 0);
-
Nested
setTimeout
vs.setInterval
Nested
setTimeout
can set the execution delay more precisely thansetInterval
:// setInterval let i = 1; setInterval(function() { func(i++); }, 100);
// nested setTimeout let j = 1; setTimeout(function run() { func(j++); setTimeout(run, 100); }, 100);
-
Garbage collection
- When a function is passed in
setTimeout/setInterval
, an internal reference is created, so it won't be garbage collected; - For
setInterval
the function will be cleared whenclearInterval
is called; - Since a function references the outer lexical environment, that takes memory, so it's better to cancel a timer when it's not needed;
- When a function is passed in
- Apart from the task queue(aka mactotask queue), there is a microtask queue;
- Microtasks come solely from our code, usually created by
- promises:
.then/catch/finally
orawait
, queueMicrotask(func)
: a special function that queuesfunc
in the microtask queuenew MutationObserver(func)
- promises:
- Microtask queue has higher priority than macrotask queue and rendering, once the stack is empty, the microtask queue gets executed immediately until it's empty, so newly added microtasks get executed as well
Example:
function foo() {
console.log('sync: start');
setTimeout(() => console.log('macrotask: timeout'), 0);
const promise = Promise.resolve();
const promiseRejected = Promise.resolve().then(() => {throw new Error('Rejected!')});
promise
.then(x => console.log('microtask: promise'))
.then(() => {
queueMicrotask(() => console.log('microtask: added by queueMicrotask'));
Promise.resolve().then(() => console.log('microtask: nested promise'));
})
console.log('sync: end');
window.addEventListener('unhandledrejection', e => {
console.log('macrotask: unhandledrejection')
});
}
foo();
// sync: start
// sync: end
// microtask: promise
// microtask: added by queueMicrotask
// microtask: nested promise
// macrotask: timeout
// macrotask: unhandledrejection
As you can see,
- synchronous code is executed first,
- then microtasks, including those newly added microtasks, until the microtasks queue is empty,
- then macrotask executes
Note:
- a promise handler is always put into the microtask queue when the promise settles, even for already resolved promises
- After the microtask queue is complete, if there is any rejected promise, the
unhandledrejection
event is triggered
There are three queues, they are processed differently:
- Tasks: one at a time, new items enqueued
- Microtasks: all items are processed, including new items just enqueued
- Animation callbacks: all existing items are processed, new items just enqueued will wait for next turn
-
Render steps include style calculation, layout and painting, they happen at the begining of each frame, which lasts 16.6ms for a 60Hz display
-
Since tasks are run-to-completion, so if you have a long running task, it will delay the render steps, causing unresponsive UI, so if there are long running tasks, you should either break them up to small chunks or handle them in a web worker;
-
If you use a zero-delay timeout loop for DOM updating, in each frame the callback can be run around 4 times, but 3 of them are wasted
-
You can use the
requestAnimationFrame
to schedule some style/DOM updating actions, they will be picked up immediately in each frame
Jake Archibald: Multiple Event Loops
-
Tabs
Usually each tab has its own event loop
-
Iframe
- Same-origin: using the same event loop as the parent frame, so the parent can access the iframe's DOM
- Cross-origin: has its own event loop
-
Web worker
Has its own event loop, which is much simpler, since there is no script tags, user interactions and DOM manipulations
-
window.open()
Similar to iframe
-
<a href="//example.com" target="_blank">
- Same-origin: same event loop, last tab is available as
window.opener
, new tab can access last tab's DOM - Cross-origin: new tab has a new event loop, can't access last tab's DOM, but still can change its location by
window.opener.location = ...
, this is a security risk, so you should add therel="noopener"
attribute, thenwindow.opener
isnull
- Same-origin: same event loop, last tab is available as
A web worker or a cross-origin iframe has its own stack, heap, and message queue. Two distinct runtimes can only communicate through sending messages via the postMessage
method. This method adds a message to the other runtime if the latter listens to message events.
The language specification is managed by ECMA's TC39 committee now, the general process of making changes to the specification is here: TC39 Process
There are 5 stages, from 0 to 4, all finished proposals (reached stage 4) are here: https://github.com/tc39/proposals/blob/master/finished-proposals.md
https://www.airpair.com/javascript/posts/the-mind-boggling-universe-of-javascript-modules
asynchronous, unblocking
// this is an AMD module
define(function() {
return something;
});
synchronous, blocking, easier to understand
// and this is CommonJS
module.exports = something;
// mod-a.js
const person = {
name: 'gary',
age: 30
};
export const a = 20; // one syntax
const b = 30;
export default person;
export { b }; // another way
app.js
:
// main.js
import theDefault from './mod-a';
import * as all from './mod-a';
console.log('theDefault:', theDefault);
console.log('all:', all);
theDefault: { name: 'gary', age: 30 }
all: [Module] { a: 20, b: 30, default: { name: 'gary', age: 30 } }
-
For both default and named exports, you can put
export
,export default
directly before the variable definition or do it at the end of file, in the above example, botha
,b
are exported; -
Or you can import everything on one line:
import theDefault, { a as myA, b } from './mod-a';
-
If you just want to trigger the side effect, do not actually import any binding:
import './mod-a';
export { login, logout } from './helpers.js';
export { default as User } from './user.js';
export * from './foo.js'; // to re-export named exports
export { default } from './foo.js'; // to re-export the default export
- Rexporting is a good way to consolidate multipe exports into a single entry point;
import * from './foo.js';'
includesdefault
, butexport * from './foo.js';
only re-exports named exports, not includingdefault
, some people don't useexport default
to avoid this oddity;
If you want to use ES modules directly (without Webpack) in broswer:
<!DOCTYPE html>
<script type="module">
import { sayHi } from './say.js';
import { foo } from 'foo'; // ** no path, not allowed
document.body.innerHTML = sayHi('John');
let name = 'module 1'; // ** module-scope
alert(this); // ** undefined
</script>
<script type="module">
alert(name); // Error, name not defined
</script>
- Use
<script type="module">
to indicate it's a module; - Strict mode is implied for modules;
- Top level variables is in module scope;
- A module is evaluated only the first time when imported;
this
is undefined, insted ofwindow
;- Module scripts are always deferred, for both external and inline scripts:
- Downloading external module scripts doesn't block HTML processing, they load in parallel with other resources;
- They are run after HTML is fully ready, this has implications:
- Module scripts can access the whole document;
- User may see the page before JS app is ready, you need 'loading indicators';
- They are executed according to their order in the document;
- If an inline module script has
async
attribute, it loads independently of other scripts or the HTML, runs immediately when ready; - External scripts from another origin requires CORS headers;
- A moudle must have a path;
-
Common builtin Errors in JS
a; // ReferrenceError: not defined @@; // SyntaxError: invalid or unexpected token 'a'.foo(); // TypeError: not a function Array(-2); // RangeError: bad arguments
-
You can create your own custom Error classes extending the builtin ones:
class MyError extends Error { consturctor(message) { super(message); this.name = 'MyError'; } }
-
A
throw
statement terminates current code block (likereturn
,break
,continue
), and passes control to the firstcatch
block (you can throw any value, not justError
object, but it should be avoided);
-
Catch should only process expected errors and "rethrow" all others:
try { let user = JSON.parse(json); if (!user.name) { throw new SyntaxError('Imcomplete dat: no name'); } notExistingFunction(); // unexpected error } catch (e) { if (e instanceof SyntaxError) { // **handle known errors** console.log(e.name + ': ' + e.message); } else { throw e; // **rethrow unexpected errors here** } }
-
It only catches run-time errors, not parse-time errors;
-
It only catches synchronous errors, not async ones (you need
try..catch
inside the async code, or a promise chain):try { setTimeout(() => { console.log('in setTimeout'); throw new Error('throw in setTimeout'); // this error is not caught }); console.log('in try'); } catch (e) { console.log('in catch'); }
-
finally
block always executes, if it returns a value, it becomes the entire function's return value, regardless of any return statement or error thrown intry
andcatch
blocks;function foo() { try { throw new Error('xx'); return 1; } catch (e) { console.log('in catch'); throw e; return 2; } finally { console.log('in finally'); return 3; // this will suppress the error } return 100; console.log('after try...catch'); } console.log(foo());
outputs:
in catch in finally 3
-
In a browser, when there is an unhandled error, it goes to
window.onerror
, it can be used for error logging;
javascript.info - Promise error handling
new Promise((resolve, reject) => {
reject('reject it');
})
.finally(() => {
console.log('in first finally');
})
.then(res => {
console.log('in then: ', res);
})
.catch(e => {
console.log('in catch: ', e);
throw e;
})
.finally(() => {
console.log('in last finally');
});
outputs:
in first finally
in catch: reject it
in last finally
Uncaught (in promise) reject it
- A
finally
block always executes, it doesn't have access to the resolved result or the rejection error; - A
catch
block returns a resolved promise, unless it throws an error it self; - You should always add a
catch
to your promise chain; - In a browser, any unhandled rejection goes to the
unhandledrejection
event handler onwindow
, it can be used for error logging;
const loadSomething = async () => {
try {
// **wrap try..catch around await
const data = await fetchSomeData();
return doSomethingWith(data);
} catch (error) {
logAndReport(error);
}
};
// **top level .catch
loadSomething().catch(() => {
// ...
});
- The promise after
await
either resolves returning a value or throws an error; - You should use normal
try...catch..finally
block to handle errors inasync
function; - At the top level,
await
is not allowed, so you still need a.catch
there to handle falling-through errors;
-
str.match
// ** with 'g', returns an array of all matches, no capturing groups 'hello world'.match(/(.)o/g); // [ 'lo', 'wo' ] // ** without 'g', returns an array, containing the first match , capturing groups, and additional properties 'hello world'.match(/(.)o/); // [ 'lo', 'l', index: 3, input: 'hello world', groups: undefined ] // ** no match, returns null 'hello world'.match(/x/); // null
-
str.matchAll
const matches = 'hello world'.matchAll(/(.)o/g); // Object [RegExp String Iterator] {} [...matches]; // [ // [ 'lo', 'l', index: 3, input: 'hello world', groups: undefined ], // [ 'wo', 'w', index: 6, input: 'hello world', groups: undefined ] // ]
This is an improved version of
match
, it:- Returns an iterable object, instead of an array, this is for optimization purpose: it doesn't perform the search initially, only do it each time you iterate over it;
- Contains the full result of each match, including capturing groups;
-
Use
(?<groupName>)
for named groupsconst date = '2018-05-16'; const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const result = re.exec(date); console.log(result); //[ '2018-05-16', // '2018', // '05', // '16', // index: 0, // input: '2018-05-16', // groups: { year: '2018', month: '05', day: '16' } ] console.log(result.groups.year); // get the value of a matched group //2018
-
Use
\k<groupName>
for back referencingconst re = /(?<fruit>apple|orange) == \k<fruit>/; console.log( re.test('apple == apple'), // true re.test('apple == orange') // false );
- Use
$<groupName>
in replacing string
const re = /(?<firstName>[a-zA-Z]+) (?<lastName>[a-zA-Z]+)/; console.log('Arya Stark'.replace(re, '$<lastName>, $<firstName>')); // Stark, Arya
- Use
Use (?:)
for a non-capturing group, you can still have quantifiers on it, but it won't be captured in a separate result item
'gogogo gary'.match(/(?:go)+ (gary)/);
// [
// 'gogogo gary',
// 'gary',
// index: 0,
// input: 'gogogo gary',
// groups: undefined
// ]
The u
flag enables full unicode support:
-
Handle 4-byte characters correctly,
'😄'.match(/./g); // [ '�', '�' ] '😄'.match(/./gu); // with `u`, 4-byte characters are processed correctlyk // [ '😄' ]
-
Make Unicode property search available:
Every character in Unicode has a lot of properties, such as
Letter
,Number
,Punctuation
etc., we can use\p{..}
to match characters with paticular properties.Common character categories:
- Letter
Lettter
L
- lowercase
Ll
- uppercase
Lu
- lowercase
- Number
Number
N
- Punctuation
Punctuation
P
- dash
Pd
- ...
- dash
- Hexadecimal
Hex_Digit
- ...
'H-'.match(/\p{Letter}\p{Pd}/gu); // [ 'H-' ] // match chinese hieroglyphs '你好'.match(/\p{Script=Han}/gu); // [ '你', '好' ]
- Letter
/\d\/\d/.test('2/3');
// true
new RegExp('\\d/\\d').test('2/3');
// true
- Forward slash '/' needs to be escaped in a regex literal as
\/
, not innew RegExp()
; \d
in a regex literal needs to be'\\d'
innew RegExp()
construct, since'\d' === 'd'
;
- Immutability in React: There’s nothing wrong with mutating objects
- Immutability in JavaScript: A Contrarian View
the string
primitive type is immutable in JS, whenever you do any manipulation on a string, a new string get created
but the String
object type is mutable
const s = new String('hello');
//undefined
s;
//[String: 'hello']
// add a new property to a String object
s.name = 'gary';
s;
// { [String: 'hello'] name: 'gary' }
two references are equal when they refer to the same value if this value is immutable:
var str1 = 'abc';
var str2 = 'abc';
str1 === str2; // true
var n1 = 1;
var n2 = 1;
n1 === n2; // also true
but if the value is mutable, the two references are not equal:
var str1 = new String('abc');
var str2 = new String('abc');
str1 === str2; // false
var arr1 = [];
var arr2 = [];
arr1 === arr2; // false
You need to use your custom methods or something like _.isEqual
from Lo-Dash to check value equality on objects.
-
Updating Nested Objects
if you want to update deeply nested state, it's become quite verbose and hard to read:
function updateVeryNestedField(state, action) { return { ...state, first: { ...state.first, second: { ...state.first.second, [action.someId]: { ...state.first.second[action.someId], fourth: action.someValue } } } }; }
it's recommended to keep your state flattened, and compose reducers as much as possible, so you only need to update flat objects or arrays
-
Appending/Prepending/Inserting/Removing/Replacing/Updating Items in arrays
// append item function appendItem(array, action) { return array.concat(action.item); } // prepend item function prependItem(array, action) { return action.item.concat(array); } // insert item function insertItem(array, action) { let newArray = array.slice(); newArray.splice(action.index, 0, action.item); return newArray; } // remove item function removeItem(array, action) { let newArray = array.slice(); newArray.splice(action.index, 1); return newArray; } // remove item (alternative way) function removeItem(array, action) { return array.filter((item, index) => index !== action.index); } // replace item function replaceItem(array, action) { let newArray = array.slice(); newArray.splice(action.index, 1, action.item); return newArray; } // update item function removeItem(array, action) { return array.map((item, index) => { if (index !== action.index) { return item; } // update the one we want return { ...item, ...action.item }; }); }
- Fully-featured data structures library;
- Using persistent data structures;
- Using structural sharing via hash maps tries and vector tries;
- Provides data structures including:
List
,Stack
,Map
,OrderedMap
,Set
,OrderedSet
andRecord
;
-
A tiny package, based on the
copy-on-write
mechanism; -
Idea: all changes are applied to a temporary draftState (a proxy of the currentState), once all mutations are done,
Immer
will produce the nextState; -
Auto freezing
- Immer automatically freezes any state trees that are modified using
produce
, this protects against any accidental modifications of the state tree outside of a producer; - It's a deep freeze, while
Object.freeze
only does a shalow freeze; - It impacts performance, by default it is turned on during local develpoment, off in production;
- Use
setAutoFreeze(true/false)
to control it explicitly;
- Immer automatically freezes any state trees that are modified using
-
Read the doc for:
- Limitations;
- TypeScript or Flow;
- Patches;
this
,void
;- Performance;
- More examples;
-
API
produce(currentState, producer: (draftState) => void): nextState
-
basic usage:
import produce from 'immer'; cosnt a = {name: 'Gary', age: 20}; // { name: 'Gary', age: 20 } // update draft in whatever way you like, and no need to return anything const b = produce(a, draft => { draft.name = 'Federer'; }); // { name: 'Federer', age: 20 } console.log(`${a.name} vs. ${b.name}`); // Gary vs. Federer
-
React
setState
increaseAge = () => { this.setState( produce(draft => { draft.user.age += 1 }); ); }
-
Redux reducers
import produce from 'immer'; const byId = produce( (draft, action) => { switch (action.type) { case RECEIVE_PRODUCTS: action.products.forEach(product => { draft[product.id] = product; }); return; } }, { 1: { id: 1, name: 'product-1' } } );
-
Provides a simple immutability helper,
update()
; -
It's syntax is inspired by MongoDB's query language;
-
Commands:
{$push: array}
push()
all the items inarray
on the target.{$unshift: array}
unshift()
all the items inarray
on the target.{$splice: array of arrays}
for each item inarrays
callsplice()
on the target with the parameters provided by the item. Note: The items in the array are applied sequentially, so the order matters. The indices of the target may change during the operation.{$set: any}
replace the target entirely.{$toggle: array of strings}
toggles a list of boolean fields from the target object.{$unset: array of strings}
remove the list of keys in a`rray from the target object.{$merge: object}
merge the keys of object with the target.{$apply: function}
passes in the current value to the function and updates it with the new returned value.{$add: array of objects}
add a value to aMap
orSet
. When adding to a Set you pass in an array of objects to add, when adding to a Map, you pass in[key, value]
arrays like so:update(myMap, {$add: [['foo', 'bar'], ['baz', 'boo']]})
.{$remove: array of strings}
remove the list of keys in array from aMap
orSet
.
-
You can define you own commands;
-
Baisc examples:
// push const initialArray = [1, 2, 3]; const newArray = update(initialArray, { $push: [4] }); // => [1, 2, 3, 4] // nested const collection = [1, 2, { a: [12, 17, 15] }]; const newCollection = update(collection, { 2: { a: { $splice: [[1, 1, 13, 14]] } } }); // => [1, 2, {a: [12, 13, 14, 15]}] // merge const obj = { a: 5, b: 3 }; const newObj = update(obj, { $merge: { b: 6, c: 7 } }); // => {a: 5, b: 6, c: 7} // update based on current value const obj = { a: 5, b: 3 }; const newObj = update(obj, { b: { $apply: function(x) { return x * 2; } } }); // => {a: 5, b: 6}
-
var
var a = 0; //local to function scope b = 0; //global scope
the following statement:
var a = (b = 0);
equals to:
b = 0; // b becomes global !!! var a = b;
-
variable scope
JS is function scoped, not block scoped, so:
// declaration of variable i will be hoisted to the beggining of the function // so, it is available at any place inside foo, not just the for loop function foo() { ... for(var i=0; ...) {} ... }
you should put variable declaration in the begining of a function:
function foo() { var i = 0; ... for(i=0; ...) {} ... }
-
let
statementlet
statement respect block scoping, so the following code does what it seems to do:foo(let i=0; ...} {}
-
numbers
- javascript only has one number type, which is 64bit double;
NaN
is a number;NaN
is not equal to anything, includingNaN
itself;- any arithmetic operation with
NaN
will result inNaN
;
0.1 + 0.2 !== 0.3; // this can cause problems when dealing with money a + b + c === a + (b + c); // can be false, this is not a js specific problem Infinity + 1 === Infinity; // true Number.MAX_VALUE + 1 === Number.MAX_VALUE; // true
-
null
isn't anythingtypeof null === 'object'; // null's type is 'object'
-
undefined
: default value for uninitialized variables and parameters-
Always use
typeof x === 'undefined'
to check if a variable exists or not; -
Comparison with
null
:undefined
is a super global variable, you can override it:let undefined = 'foo'
, whilenull
is a keyword;undeined
is of typeundefined
,null
is of typeobject
;
-
-
typeof
var a = [1, 2]; typeof a === 'object'; // typeof array returns 'object' Array.isArray(a); // true, use this to check arrays
-
+
if both operands are numbers then add them else convert to string and concatenate end
2 + '3' -> '23'
-
%
%
is a remainder operator, takes sign from the first operator, not a modulo operator, which takes sign from the second operator-1 % 8 -> -1;
-
&&
,||
not necessarily return boolean values, just return values of one operand
-
!!
convert truesy value to
true
, falsy value tofalse
JS optimizing compiler - JSConf
-
Just In Time (JIT) compilation
Generate machine code during runtime, not ahead of time (AOT)
-
Optimizing compilation
-
There are two compilers, baseline complier and optimizing compiler, which optimizes 'hot' functions based on gathered info (e.g. property types) from execution.
-
If types change, then it need to 'de-optimize' the code, which impacts the performance.
-
Performance tip: write code that looks like statically typed, it would be much easier for optimizing
-
in the following code, the updateLayout
function will only run after the resize
event stopped 250ms
// debounce the resize event
$(window).on('resize', function() {
clearTimeout(window.resizedFinished);
window.resizedFinished = setTimeout(function() {
updateLayout();
}, 250);
});
If you bind a function multiple times, for each parameter(inclding this
) in the original function, only the first bound value is used, any later bound values will be disarded, put it in another way, you can only bind a value to each parameter once
function foo(arg1) {
console.log(this);
console.log(arg1);
}
const fooBound = foo.bind({ name: 'gary' }); // this bound
const fooBound2 = fooBound.bind({ name: 'jack' }, 'hello'); // 'hello' bound to arg1
const fooBound3 = fooBound2.bind({}, 'hola'); // both {} and 'hola' are discarded
fooBound('bar');
// {name: "gary"}
// bar
fooBound2('bar');
// {name: "gary"}
// hello
fooBound3();
// {name: "gary"}
// hello
console.log('foo.name: ' + foo.name + ', foo.length: ' + foo.length);
// foo.name: foo, foo.length: 1
console.log(
'fooBound.name: ' + fooBound.name + ', fooBound.length: ' + fooBound.length
);
// fooBound.name: bound foo, fooBound.length: 1
console.log(
'fooBound2.name: ' +
fooBound2.name +
', fooBound2.length: ' +
fooBound2.length
);
// fooBound2.name: bound bound foo, fooBound2.length: 0
console.log(
'fooBound3.name: ' +
fooBound3.name +
', fooBound3.length: ' +
fooBound3.length
);
// fooBound3.name: bound bound bound foo, fooBound3.length: 0
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args); // enough parameters, run
} else {
// otherwise return a function
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
curriedSum(1, 2, 3); // 6, still callable normally
curriedSum(1)(2, 3); // 6, currying of 1st arg
curriedSum(1)(2)(3); // 6, full currying
It's identical to ===
in most cases, except:
NaN === NaN;
// false
Object.is(NaN, NaN);
// true
Object.getOwnPropertyNames(obj)
returns non-symbol keys;Object.getOwnPropertySymbols(obj)
returns symbol keys;Object.keys/values()
returns non-symbol keys/values with enumerable flag;for..in
loops over non-symbol keys with enumerable flag, and also prototype keys;
-
If an object has a custom
toJSON
method, it's used to convert the object to JSON string:const o = { id: 202, name: 'Gary', toJSON() { return this.id; } }; JSON.stringify(o); // '202'
-
Use replacer function in
JSON.stringify
to deal with circular referencing issue:const member = { name: 'Gary' }; const team = { name: 'Dev' }; member.team = team; team.members = [member]; team; // { name: 'Dev', members: [ { name: 'Gary', team: [Circular] } ] } JSON.stringify(team); // throws an error: circular references // #### using replacer function to ignore the 'team' key JSON.stringify(team, (key, value) => (key === 'team' ? undefined : value)); // '{"name":"Dev","members":[{"name":"Gary"}]}'
-
Use a reviver function to parse a string to a
Date
object// #### JSON.stringify converts a Date to a string const s = JSON.stringify({ name: 'JS Conf', date: new Date() }); // '{"name":"JS Conf","date":"2019-12-29T08:16:31.262Z"}' // #### JSON.parse doesn't convert a Date to a string JSON.parse(s); // { name: 'JS Conf', date: '2019-12-29T08:16:31.262Z' } // #### JSON.parse accepts a reviver function to do any data conversions JSON.parse(s, (key, value) => (key === 'date' ? new Date(value) : value));
JavaScript Symbols, Iterators, Generators, Async/Await, and Async Iterators — All Explained Simply : Read this one to fully understand the relations between Symbols, Iterators, Generators, Async/Await and Aync Iterators