Reading Notes: Secrets of the Javascript Ninjia, 2nd Edition
Let me become a ninjia with greate Javascript skills.
Why
TODO: check private property in closure????
What
-
JS has two pillars: a>
function
; b>object
。 -
GUI
application’s life cycle repeating two setps infinitiely: a>Page Building
; b>Event Handling
. -
While page building, brower will switch between parsing HTML to DOM and executing javascript code (when script node encountered) as necessary.
-
HTML code is just a blueprint, DOM built from it might not be exactly the same as its original HTML.
-
Browser expose global object
window
- through which all other global objects, global variables and brower APIs are accessible - to Javascript engine. -
Browser has an environment with
single-threaded
execution model. All events, whether coming from user or server, are queued up and processed one by one.
Functions
Functions are first-class
objects in Javascript. They can even posess properties:
Like, adding tag to functions:
var store = {
next: 1,
cache: {},
add: function (fn) {
if (!fn.id) {
fn.id = this.next++;
this.cache[fn.id] = fn;
return fn.id;
}
}
}
function test() {}
console.log(store.add(test)); // 1
console.log(store.add(test)); // undefined
// Remember results
Or serve as cache:
function isPrime(value) {
if (!isPrime.answers) {
isPrime.answers = {};
}
if (isPrime.answers[value] !== undefined) {
return isPrime.answers[value];
}
var prime = value !== 1;
for (var i = 2; i < value; i++) {
if (value % i === 0) {
prime = false;
break;
}
}
return isPrime.answers[value] = prime;
}
console.log(isPrime(5)); // true
console.log(isPrime.answers[5]); // true, cached
There are 4 ways to define
a function:
// 1. Function declaration / expression
function fun1() {
return 1;
}
// 2. Arrow function
arg => arg * 2
// 3. Function constructor
new Function('a', 'b', 'return a + b')
// 4. Generator
function* gen() {
yield 1;
}
Functions are tolerant with arguments, we can pass in more or less arguments than prameters.
Use ...
to collect rest parameters:
function multiplyMax(first, ...rest) {
var sorted = rest.sort((a, b) => a - b);
return first * sorted[sorted.length - 1];
}
console.log(multiplyMax(3, 4, 5, 2, 1) === 15);
Default
argument is supported as well:
function perform(ninja, action = 'walking') {
return ninja + " " + action;
}
arguments
is an array-like object accessible inside functions that contains the values of the arguments passed to that function.
this
refers to the context where a piece of code, such as a function’s body, is supposed to execute.
When invoked:
As a function: this
is either window
(nonstrict mode) or undefined
(strict mode):
function f1() => this;
function f2() { 'use strict'; return this; }
f1(); // Window
f2(); // undefined
As a method, this
refers to owner of the function:
function f1() => this;
f1(); // window
let k1 = {
func: f1
};
k1.func(); // k1
let k2 = {
func: f1
};
k2.func(); // k2
As a constructor, calling a function with keyword new
will create a new empty object referenced by this
and return the newly constructed object as the new
operator’s return value. (Explicit return value will be ignored if it’s non-object.)
function Ninja() {
this.shout = () => this;
}
let n1 = new Ninja();
Via the function’s apply or call methods, this
is set explicitly.
function juggle() {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
this.result = result;
}
var ninja1 = {};
juggle.apply(ninja1, [1, 2, 3, 4]);
juggle.call(ninja1, 5, 6, 7, 8);
arrow
functions inherit context from where it’s defined.
bind
method create a brand-new function with this
set explicitly.
Closures
Closure
is a “safety bubble” around the function and the variables in scope when the function is defined, so that it has everything to execute.
Javascript does not support private variables natively. But we can achieve it via closures:
function Tiger() {
var teeth = 0;
this.getTeeth = function() {
return teeth;
}
}
const t1 = new Tiger();
console.log(t1.teeth); // undefined
TODO: Closures are not finished. But they are language-specific. I’d like to drop. (2024.7.10)
Generators
Generators
are able to produce multiple values, on a per request basis, suspending their execution between these requests.
One merit is that its cntext is reserved and resumed between each execution. This creates an isolated environment.
Another merit is allowing treat asynchronous code in synchronous way, since it emits one value at a time. This means a generator must be one of following states: a> idle; b> executing; c> suspended; d> completed.
Notice, return
only ends the geneartor (in complete state), the value after return is not yielded.
One way to consume a sequence of generated values is using for-of
loop:
function* g1() {
yield 'a'
yield 'b'
yield 'c'
}
for(let weapon of g1()) {
console.log(weapon)
}
Making a call to a generator does not mean the body of generator function will be executed immediately. Instead, an iterator object is created.
function* g1() {
yield 'a'
yield 'b'
yield 'c'
}
const iterator = g2();
iterator.next(); // a
iterator.next(); // b
iterator.next(); // c
iterator.next(); // undefined
next
and yield
are zipped together and next always comes first. And, starting from the second next, you can provide arugment so it will serve as the return value of previous yield
.
function* g3(name) {
const r1 = yield('good' + ' ' + name);
yield('bad' + ' ' + r1);
}
const iterator = g3('John');
iterator.next('A'); // good John
iterator.next('B'); // bad B
By using yield*
, we yield (transfer control) to another generator.
function* g1() {
yield 'a';
yield 'b';
yield* g2();
yield 'c';
}
function* g2() {
yield 1;
yield 2;
yield 3;
}
throw
is used to pass error from inside and outside of a generator.
function *g4() {
try {
yield 123;
throw 'from inside'
} catch(e) {
cnsoel.log(e);
}
}
const iterator = g4();
iterator.throw('from outside');
Promises
A Promise
is an placeholder for the result of an asynchronous task. It’s either in pending or resolved state.
const p1 = Promise((resolve, reject) => {
resolve('Good');
// Or
// reject('An error happened');
});
p1.then(result => { console.log('this is good.') }, err => { console.log('something bad happend') });
catch()
method will catch any error produced in a chain of promises.
async
keyboard actually converts a function into a geneartor; while await
mark wating for a promise to complete.
// Raw implementation of async & await
function async(generator) {
var iterator = generator();
function handle(result) {
if (result.done) { return; }
const value = result.value;
if (value instanceof Promise) {
value.then(r => handle(iterator.next(r))).catch(e => iterator.throw(e));
}
}
try {
handle(iterator.next());
} catch(e) {
iterator.throw(e);
}
}
// Usage
async(function* () {
try {
const task1 = yield getTask1();
const task2 = yield getTask2();
const task3 = yield getTask3();
} catch(e) {
console.log(e);
}
});
// Using async and await keywords
async function () {
try {
const task1 = yield getTask1();
const task2 = yield getTask2();
const task3 = yield getTask3();
} catch(e) {
console.log(e);
}
}()
Prototyping
prototype
is an object to whcih the search for a particular property can be delegated to. Every object has a prototype, which also has a prototype, forming a prototype chain
.
Every function has a prototype property. When the function is used as a constructor (instead as a “normal” function) with the new
operator. It will become the new object’s prototype.
function Tiger() {}
Tiger.prototype.good = 31;
const t1 = Tiger();
assert(t1 === undefined, 't1 is undefined');
const t2 = new Tiger();
assert(t2 && t2.good === 31, 't2 is a Tiger');
It makes sense to place instance methods
only on the function’s prototype, since in that way we have a single method shared by all instances.
Every object has a constructor
property referencing its original constructor function, which means we can use it to instantiate more same type objects.
const t1 = new Tiger();
const t2 = new t1.constructor();
instanceof
operator checks whether the prototype of the right-side function is in the prototype chain of the lef-side object.
To achieve object-oriented-style inheritance, we need to tamper with constructor function’s prototype and monkey-patch that prototype’s constructor.
function Animal() {}
function Tiger() {}
Tiger.prototype = new Animal();
Object.defineProperty(Tiger.prototype, "constructor", {
enumerable: false,
value: Tiger,
writable: true
});
var tiger = new Tiger();
assert(tiger.constructor === Tiger, "tiger.constructor === Tiger");
assert(tiger instanceof Animal, "tiger instanceof Animal");
A constructor itself acts like the class object
in ruby. The constructor constructs a new instance, who maintains a reference to the prototype. The prototype maintains a reference to the constructor as well, which is used by instanceof
operator.
instance(.__proto__)
|
V
prototype(.constructor) ---
| ^
V |
Constructor(.prototype) ---
The ES6 keyword class
is based on the prototype inheritance implementation.
class Tiger {
constructor(name) {
this.name = name;
this.teeth = 30;
}
roar() {
console.log('awwww');
}
static fight(tiger1, tiger2) {
return tiger1.teeth - tiger2.teeth;
}
}
class ChinaTiger extends Tiger {
constructor(name, fur) {
super(name);
this.fur = fur;
}
smile() {
console.log('haha');
}
}
// Is equal to
function Tiger(name) {
this.name = name;
this.teeth = 30;
}
Tiger.prototype.roar = function() {
console.log('awwww');
}
Tiger.fight = function(tiger1, tiger2) {
return tiger1.teeth - tiger2.teeth;
}
function ChinaTiger(name, fur) {
this.fur = fur;
this.constructor.prototype.name = name;
}
ChinaTiger.prototype = new Tiger();
Object.defineProperty(ChinaTiger.prototype, "constructor", {
enumerable: false,
value: ChinaTiger,
writable: true
});
Access Control Redefined
Define getter
and setter
methods by literals:
const list = {
colors: ['red', 'blue', 'green'],
get firstColor() {
return colors[0];
},
set firstColor(value) {
colors[0] = value;
}
}
assert(list.firstColor === 'red', 'The first color is red');
list.firstColor = 'yellow';
assert(list.firstColor === 'yellow', 'Now the first color is yellow');
Or using Object.defineProperty
method:
function Tiger() {
let _teeth = 12; // Create a private property using closure.
Object.defineProperty(this, 'teeth', {
get: () => _teeth,
set: (value) => {
if (value < 0) {
throw new Error('Invalid number of teeth');
}
_teeth = value;
}
});
}
const t1 = new Tiger();
t1.teeth = 10;
assert(t1.teeth === 10, 't1.teeth === 10');
try {
t1.teeth = -1;
assert(false, 't1.teeth = -1 should throw an error');
} catch(e) {
assert(e.message === 'Invalid number of teeth', 'e.message === "Invalid number of teeth"');
}
A proxy
is a surrogate through which we control access to another object.
const emperor = { name: 'Komei' };
const representative = new Proxy(emperor, {
get: (target, key) => {
return key in target ? target[key] : 'The emperor is too busy to meet you now';
},
set: (target, key, value) => {
target[key] = value;
}
});
console.log(representative.name); // Komei
console.log(representative.age); // The emperor is too busy to meet you now
representative.age = 16;
console.log(representative.age); // 16
Handler functions, like “get” & “set”, also called traps
. Because they trap calls to the underlying target object.