ShevaXu / notes

Aggregated notes repo, knowledge base :P

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Advanced JavaScript

ShevaXu opened this issue · comments

Advanced JavaScript

bind(), apply() & call()

It's all about this.

All three methods can set this value for a function.

bind()

fun.bind(thisArg[, arg1[, arg2[, ...]]])

The key of bind() is that it creates a new function for later calls, or no immediate invocation.

  • currying/partial functions - when provided with arguments
  • async callbacks - e.g., keep context in window.setTimeout()

apply() & call()

These two methods call a function with a given this value and arguments.

The fundamental difference is that call() accepts an argument list, while apply() accepts a single array of arguments.

fun.apply(thisArg, [argsArray])
fun.call(thisArg[, arg1[, arg2[, ...]]])
  • chain constructors
  • invoke build-in/anonymous functions
  • monkey patch

Prototypal Inheritance

Eric Elliott advocates prototypal inheritance as one of the two pillars of JavaScript (the other one is functional programming).
In the article How to Escape the 7th Circle of Hell he addressed that classical inheritance suffers from the Gorilla/Banana problem ("You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."), the too-tight coupling between parent and child, and your ending up building new classes for subtle differences.

In class inheritance, instances inherit from a blueprint (the class), and create sub-class relationships. In other words, you can’t use the class like you would use an instance. You can’t invoke instance methods on a class definition itself. You must first create an instance and then invoke methods on that instance.

In prototypal inheritance, instances inherit from other instances. Using delegate prototypes (setting the prototype of one instance to refer to an examplar object), it’s literally Objects Linking to Other Objects, or OLOO, as Kyle Simpson calls it. Using concatenative inheritance, you just copy properties from an exemplar object to a new instance.

-- Common misconceptions about inheritance in JS

As a quote from Design Patterns: Elements of Reusable Object-Oriented Software says, "favor object composition (assembling smaller functionalities, thinking can-do, has-a, or uses-a relationships) over class inheritance (is-a)".

Three Different Kinds Of Prototypal OO

There are three kinds of prototypal OO according to Eric and he built stampit (factory) as a sugar & abstraction on top of prototype. They are implemented as a factory - a function that creates and returns objects (but NOT a constructor).

1. Delegation / Differential Inheritance - Methods

A delegate prototype is an object that serves as a base for another object. Method delegation is a fantastic way to preserve memory resources, because you only need one copy of each method to be shared by all instances.

var proto = {
  hello: function () {
    return 'Hello, my name is ' + this.name
  }
}
 
var george = Object.create(proto) // the factory creates an instance

george.name = 'George' // JS has dynamic object extension
george.hello() // 'Hello, my name is George'

Object.create() - creates a new object with the specified prototype object and properties.

2. Cloning / Concatenative Inheritance / Mixins - States

Prototype cloning copy the properties from one object to another WITHOUT retaining a reference between the two objects.

var someone = Object.create(proto)
var george = Object.assign(someone, { name: 'George' })

george.hello() // 'Hello, my name is George'

Object.assign() - [ES6] copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.

3. Closure Prototypes / Functional Inheritance - Encapsulation / Privacy

Closure prototypes are functions that can be run against a target object in order to extend it. It’s a function prototype. Think of it as an alternative to a constructor or init function.

var age = function (value) {
  var privacy = value
    this.getAge = function () {
      return privacy
    }
    this.aging = function () {
      privacy++
    }
}
age.call(george, 20)

george.getAge() // 20
george.aging()
george.getAge() // 21

Takeaways

  • A class is a blueprint.
  • A prototype is an object instance.

Just don't use new, constructor, super and ES6 class.

Three types of prototypal OO:

  • Delegate - prototype link - methods
  • cloning/concatenate - deep copy - states
  • functional - closure - encapsulation

The stampit form:

const stamp = stampit({ methods, refs, init, props })
const instance = stamp({ count: 0 }) // factory with provided refs

(Here's the demonstration using stamps, but API outdated, see the lastest.)

Concatenative Inheritance

Concatenative inheritance is the easiest way to achieve JavaScript’s simplified form of object composition (this is what most people mean by "mixins" in JavaScript).

The process of inheriting features directly from one object to another by copying the source objects properties. In JavaScript, source prototypes are commonly referred to as mixins. Since ES6, this feature has a convenience utility in JavaScript called Object.assign(). Prior to ES6, this was commonly done with Underscore/Lodash’s .extend()jQuery’s $.extend(), and so on ...

-- The Heart & Soul of Prototypal OO: Concatenative Inheritance

const distortion = { distortion: 1 }
const volume = { volume: 1 }
const cabinet = { cabinet: 'maple' }
const lowCut = { lowCut: 1 }
const inputLevel = { inputLevel: 1 }

const GuitarAmp = (options) => {
  return Object.assign({}, distortion, volume, cabinet, options)
}

const BassAmp = (options) => {
  return Object.assign({}, lowCut, volume, cabinet, options)
}

const ChannelStrip = (options) => {
  return Object.assign({}, inputLevel, lowCut, volume, options)
}

Objects can be built by adding properties to existing instances ad-hoc.
Dynamic object extension is the foundation of concatenative inheritance.

Middleware & Decorator

From this blog. Also Aspect-oriented Programming AOP.

Middleware

The most flexible/controlled way.

function myComponentFactory() {
    let suffix = ''
    const instance = {
        setSuffix: suff => suffix = suff,
        printValue: value => console.log(`value is ${value + suffix}`),
        addDecorators: decorators => {
            let printValue = instance.printValue
            decorators.slice().reverse().forEach(decorator => printValue = decorator(printValue))
            instance.printValue = printValue
        }
    }
    return instance
}

function toLowerDecorator(inner) {
    return value => inner(value.toLowerCase())
}

function validatorDecorator(inner) {
    return value => {
        const isValid = ~value.indexOf('My')

        setTimeout(() => {
            if (isValid) inner(value)
            else console.log('not valid man...')
        }, 500)
    }
}

const component = myComponentFactory()
component.addDecorators([toLowerDecorator, validatorDecorator])
component.setSuffix('!')
component.printValue('My Value')
component.printValue('Invalid Value')

More famous examples: express, and Redux

Monkey Patching

function myComponentFactory() {
    let suffix = ''

    return {
        setSuffix: suf => suffix = suf,
        printValue: value => console.log(`value is ${value + suffix}`)
    }
}

function decorateWithToLower(inner) {
    const originalPrintValue = inner.printValue
    inner.printValue = value => originalPrintValue(value.toLowerCase())
}

function decorateWithValidator(inner) {
    const originalPrintValue = inner.printValue

    inner.printValue = value => {
        const isValid = ~value.indexOf('My')

        setTimeout(() => {
            if (isValid) originalPrintValue(value)
            else console.log('not valid man...')
        }, 500)
    }
}

const component = myComponentFactory()
decorateWithToLower(component)
decorateWithValidator(component)

component.setSuffix('!')
component.printValue('My Value')
component.printValue('Invalid Value')

It's consider harmful ... NO MONKEY PATCHING

Others

  • Plain closures
  • Prototypal inheritance (recommended for simple use)
  • ES6 Proxy (overkilled)

Misc

Scoping and Hoisting (Ref)

JavaScript has function-level scope, which is radically different from the C family. Blocks, such as if statements, do not create a new scope. Only functions do.

On runtime, all var and FunctionDeclarations function foo() {...} are moved by the interpreter to the beginning of each function (its scope) - this is known as Hoisting.

So the good practise is:

function foo (a, b, c) {
  var x = 1 // declare all the vars altogether on the first line
  var bar
  var baz = "something"
}

Immediately-Invoked Function Expression (IIFE)

When the parser encounters the function keyword in the global scope or inside a function, it treats it as a function declaration (statement), and not as a function expression, by default.

So, function foo(){ /* code */ }(); would causes a SyntaxError: Unexpected token ).

The most widely accepted way to fix this is just to wrap it in parentheses, because in JavaScript, parens can’t contain statements.

(function () { /* code */ } ()); 
(function () { /* code */ }) (); 

This is useful for saving state with closures.

Read this for more detail.

(module.)exports

if you want your module to be of a specific object type, use module.exports; if you want your module to be a typical module instance, use exports.
-- ref

  • exports is the recommended object unless you are planning to change the object type of your module from the traditional 'module instance' to something else.
  • As long are you don't overwrite the module.exports object with an assignment operation, anything attached to module.exports and exports will be available in the 'required' module.

If you want the root of your module's export to be a function (such as a constructor) or if you want to export a complete object in one assignment instead of building it one property at a time, assign it to module.exports instead of exports. -- nodejs api

(Non-)Blocking of Node.js

From hashnode

  • Node.js uses multiple threading strategies. The libuv thread pool size equals to the number of logical CPU cores.
  • Node.js has many different queues. When the thread pool exceeds, each additional IO tasks is appended to the queue (FiFO).
  • A slow Node.js app rarely has something to do with thread pool size.
  • setTimeout( {...}, 0) trick moves the slow code to the end of the message queue, allows other code to execute before our slow code blocks everything else, thus speed up synchronous code execution.
  • As a developer just work with the Event Loop which is single threaded and non-blocking. It's Node's responsibility to delegate the blocking tasks to worker threads.

Operator Precedence

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence

Hacks

source

  • Converting to boolean using !!
  • Converting to number using +
  • Short-circuits conditionals connected && login()
  • Default values with ||
  • Caching array.length in the loop (avoid large array length recalculation)
  • Detecting properties with in
  • Replace all str.replace(/this/g, 'that')
  • Merging array arr1.push.apply(arr1, arr2) instead of arr1.concat(arr2) (to save memory)
  • Shuffling array arr.sort(() => Math.random() - 0.5)

Parameters Handling

http://exploringjs.com/es6/ch_parameter-handling.html

function mandatory() {
    throw new Error('Missing parameter');
}
function foo(mustBeProvided = mandatory()) {
    return mustBeProvided;
}

Resources

Hub

News feed

Free eBooks

A complete list can be found at jsbooks

Selected

Node