JavaScript


Goals:

  • Answer questions, sort out confusions.
  • Expose everybody to some core features & quirks of the language, knowledge that will help when reading & writing JS.
  • Upgrade understanding in preparation for other people looking at and working on NUI (which is JS-heavy)

JavaScript

  • Why named JavaScript? (Boring and doesn't matter.)

  • ECMAScript vs JavaScript

  • ES3, ES5, ES6

  • Babel (transpilation)

Types

  • Primitives

    • Boolean

    • String

    • Number

    • "nonvalues": null undefined

    • Symbol (in ES6)

  • Object (includes plain objects, arrays, functions)


You do not create new Types, even if you are implementing a "class"-like, "inheritance"-like pattern.

Variables are dynamically (not statically) typed

var thing; // undefined
thing = null; // null
thing = { foo: 1 }; // Object
thing.bar = 3; // 3
thing = 7; // 7
thing = 'seven'; // 'seven'
thing = function() { console.log('zowee'); }; // Function
thing(); // zowee
thing = true; // true
thing = [thing, thing, false]; // Array [true, true, false]

Objects (and Arrays) can hold any values

But object keys are always strings (or coerced to strings).

var anArray = [1, 'one', true, { valid: 'yes' }, [0, 0, 0]];

var anObject = {
  bool: true,
  num: 2,
  obj: { valid: 'yes' },
  arr: [1, 2, 3],
  func: function() {},
  // ES6 Shorthand Methods
  func() {}
};

// Key coercion example
anObject[true] = 'yes';
anObject['true']; // 'yes'

More on Types

  • Primitives are immutable & assigned by value

  • Objects are mutable & assigned by reference

var strA = 'horse';
var strB = 'horse';
strA === strB; // true

var objA = { things: 7 };
var objB = { things: 7 };
objA === objB; // false

var strACopy = strA;
strACopy= 'cow';
strACopy; // 'cow'
strA; // 'horse'

var objACopy = objA;
objA.horses = 8;
objACopy.horses; // 8

Use Object and Array Literals​

// Add stuff on initialization
var anObject = {
  first: 1,
  second: 2
};

// Add stuff later
anObject.third = 3;

var anArray = [1, 2, 3, 4];
anArray[2] = 6; // Insert at index
anArray; [1, 2, 6, 4];
anArray.push('blue'); // Add to end
anArray; [1, 2, 6, 4, 'blue']

Explicit Type Coercion

​The obvious ways:

var a = 8; // 8
typeof a; // 'number'

var b = String(a); // '8'
typeof b; // 'string'

var c = Boolean(c); // true
typeof c; // 'boolean'

var d = '8';
var e = Number(d); // 8
typeof e; // 'number'

var f = {};
Boolean(f); // true
Number(f); // NaN
String(f); // '[object Object]' -- not very useful

Explicit Type Coercion

​Less obvious ways:

var a = 8;
var b = a + 'px'; // '8px'
'' + a; // '8' (string)

var c = '12';
+c; // 12 (number)
// More common is to use parseInt() or parseFloat()
parseInt(c); // 12 (number)
// Parse functions get a number and exclude non-numbers
parseInt(b); // 8 (number)

!a; // false
!!a; // true

Boolean Coercion: 

Truthy & Falsy

Explicit and implicit coercion to Boolean has these rules:

  • Falsy
    • undefined & null
    • false
    • 0, NaN
    • "" (empty string)
  • Truthy
    • everything else (includes {}, [], 'false', -1000, function() {})

Equality and coercion

===  (and !==) does not perform type coercion: strict equality

== (and !=) does (implicitly) coerce: loose equality

 

Loose equality's (implicit) coercion algorithm is kind of obscure and complicated --- usually best to stay away from == and use strict equality, explicitly coercing as needed.

// More "legit" use-cases for ==

// Comparing numbers and string-numbers
let a = 42;
let b = "42";
a === b; // false
a == b; // true

// Checking if value is null or undefined at once
var c = null;
var d = undefined;
c == null; // true
d == null; // true
d === null; // false
// Tricky behaviors, illustrating why you
// should avoid ==

1 == true;
2 == false;
"" == 0;
"" != null;
false != null;
42 == [42];
{} == true;
[] == ![];
// [].toString() === ""; ![] === false

Equality and coercion

 

// Instead of ...
const a = '4';
a == 4;

// Do ...
a === '' + 4;
'' + a === '' + 4;
String(a) === String(4);

// Or write a function ...

equalNumberOrString(a, 4);

// Instead of ...
a == null;

// Do ...
a === undefined || a === null;

// Or write a function ...
nullOrUndefined(a);

Implicit Boolean Coercion

WARNING: "empty" values are not always falsy.

if (/* test exp */) {}
for (..; /* test exp */; ..) {}
while (/* test exp */) {}
/* test exp */ ? a : b
/* test exp */ || a
/* test exp */ && a

var foo = { bar: 7 };
if (foo) { console.log('foo is truthy'); } // foo is truthy
if (foo.bar) { console.log('bar is truthy'); } // foo.bar is truthy
if (foo.baz) { console.log('baz is truthy'); } // (nothing)

if ([]) { console.log('[] is truthy'); } // [] is truthy
if ({}) { console.log('{} is truthy'); } // {} is truthy
if (function() { return false; }) {
  console.log('function returning false is truthy'); // function returning false is truthy
}

if (0) { console.log('0 is truthy'); } // (nothing)
if (10) { console.log('10 is truthy'); } // 10 is truthy

Short Circuiting & Logical Operators

Do not return Booleans: return evaluated expressions.

a || b (not quite "or")

  • `a` is checked as Boolean
  • if `a` is truthy, return value of expression `a`
  • if `a` is falsy, return value of expression `b`

a && b (not quite "and")

  • `a` is checked as Boolean
  • if `a` is truthy, return i `b`
  • if `a` is falsy, return value of expression `a`
var a = 0;
a || 1; // 1
a && 1; // 0

function eff() { return false; }
function yarg() { return 'yarg'; }
eff() || yarg(); // 'yarg'
eff || yarg; // function eff() { return false; }

Short Circuiting cont.

Default parameters are typical use case for ||

function say(thing) {
  thing = thing || 'blergh';
  console.log(thing);
}

say(); // blergh
say('nothing'); // nothing

Second term in is lazily evaluated, and code compressor (Uglify) can understand that:

// ES6 Default Parameters
// handle this nicely

function say(thing='blergh') {
  console.log(thing);
}

say(); // blergh
say('nothing'); // nothing
// from constantEnum.js

process.env.DEV && u.errorUnless(hasActions,
  `The enum '${name}' does not have any 'actionTypes',` +
  `so invocation of the action '${key}' failed`
);

// Uglify can remove this altogether if run in
// non-DEV environment, where process.env.DEV is
// never truthy

Polymorphic Parameters

Instead of Java's method "overloading".

// All functions have an "arguments" list that you can use
// to access unnamed arguments.
function dotJoin(/* arguments */) {
  var firstArg = arguments[0];
  if (_.isString(firstArg)) { /* handle string list */}
  if (_.isArray(firstArg)) { /* handle array */ }
  if (_.isPlainObject(firstArg)) { /* handle plain object */ }
  if (_.isFunction(firstArg)) { /* handle function */ }
}

// e.g. cssClassAccessor.js

// With ES6 Rest Parameters, "arguments" is deprecated
function dotJoin(first, ...rest) {}

// Signatures like (val[, options], callback) are common
fs.readFile('foo.txt', { encoding: 'utf8' }, myCallback);
fs.readFile('foo.txt', null, myCallback);
fs.readFile('foo.txt', myCallback);

Variable Scope

`var`: function scoped

if (true) {
  var foo = 4;
}
foo; // 4

var bar = 7;
function oooooo() {
  var baz = 8;
  console.log(bar); // 7
  bar = 12;
  console.log(bar); // 12
}
oooooo(); // 12
console.log(bar); // 12
console.log(baz); // Error: baz is not defined

Variable Scope

ES6 `const` & `let`: block scoped

 

`const` also means "final" -- cannot be reassigned

if (true) {
  let foo = 4;
}
foo; // Error

let bar = 8;
if (true) {
  let bar = 14;
  bar; // 14
}
bar; // 8

const fozzy = 'yep';
fozzy = 'nope'; // Error

const fancy = { pants: true };
fancy.hat = true; // (no error)

Hoisting

Declarations are "hoisted" to the top of their scope (during compilation phase).

Assignment expression are not evaluated until execution phase.

console.log(bar); // undefined
var bar = 'abc';
console.log(bar); // abc

// Because of engine behavior,
// above is equivalent to this
var foo;
console.log(foo); // undefined
var foo = 'abc';
console.log(foo); // abc
hoisted(); // pow
// Function declaration
function hoisted() {
  console.log('pow');
}  

nothoisted(); // TypeError
// Function expression
var nothoisted = function() {
  console.log('bam');
}

Functional Patterns

Functions are "first-class" objects: they can be

  • Passed as arguments to other functions
  • Returned from other functions
  • Assigned to variables, object properties, array slots --- anywhere other values can go

Higher-Order Functions

Functions receiving other functions

Just call the passed function:

// from Eloquent JavaScript

function unless(test, then) {
  if (!test) then();
}
function repeat(times, body) {
  for (var i = 0; i < times; i++) body(i);
}

repeat(3, function(n) {
  unless(n % 2, function() {
    console.log(n, "is even");
  });
});
// → 0 is even
// → 2 is even

Higher-Order Functions

Functions receiving other functions

Callbacks for asynchronous operations:

function fetchPage(url, callback) {
  var result;
  // do whatever's necessary to make
  // result = the page that's fetched
  callback(result);
}

// Anonymous function argument (lambda)
fetchPage('http://google.com', function(result) {
  console.log(result);
});

function processPage(result) {
  // do things to process the result page
}

// Named function argument
fetchPage('http://mozilla.org', processPage);

Higher-Order Functions

Functions receiving other functions

"Iteratees" (?) that process data

var arr = [1, 2, 3, 4];

var logEm = arr.forEach(function(val) {
  console.log(val);
});

var squared = arr.map(function(val) {
  return val * val;
});
console.log(squared); // [1, 4, 9, 16]

var dataObj = arr.reduce(function(result, val) {
  result[val] = {
    squared: val * val,
    isLessThan3: val < 3
  };
  return result;
}, {});
console.log(dataObj); // {"1":{"squared":1,"isLessThan3":true}, ...

Higher-Order Functions

Functions receiving other functions

Predicates

(truth-values)

 

const people = [{ name: 'steve'}, { name: 'don' }, { name: 'ed' }];

function find(arr, fn) {
  for (let i = 0, l = arr.length; i < l; i++) {
    let item = arr[i];
    if (fn(item)) {
      return item;
    }
  }
}

const steve = find(people, function(obj) {
  return obj.name === 'steve';
});
console.log(steve); // Object { name: 'steve' }

const firstNonSteve = find(people, function(obj) {
  return obj.name !== 'steve';
});
console.log(firstNonSteve); // Object { name: 'don' }

const first36YearOld = find(people, function(obj) {
  return obj.age === 36;
});
console.log(first36YearOld); // undefined

Higher-Order Functions

Functions returning other functions

Partial application

// from Eloquent JavaScript

function greaterThan(n) {
  return function(m) { return m > n; };
}
var greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true

Higher-Order Functions

Functions returning other functions

Partial application

const people = [{ name: 'steve'}, { name: 'don' },
  { name: 'ed' }];

function find(arr, fn) {
  for (let i = 0, l = arr.length; i < l; i++) {
    if (fn(arr[i])) { return arr[i]; }
  }
}

function findByName(arr, name) {
  return find(arr, function(item) {
    return item.name === name;
  });
}

function findNameInPeople(name) {
  return findByName(people, name);
}

function findDonInPeople() {
  return findNameInPeople('don');
}

console.log(findByName(people, 'steve'));
// Object { name: 'steve' }
console.log(findNameInPeople('ed'));
// Object { name: 'ed' }
console.log(findDonInPeople());
// Object { name: 'don' }

Higher-Order Functions

Functions returning other functions

Function factory

function cssClassChecker(allowedClasses) {
  return function(classname) {
    if (allowedClasses.indexOf(classname) !== -1) {
      return classname;
    }
    throw new Error(`The class "${classname}" is not available`);
  }
}

var checkNumberClass = cssClassChecker(['one', 'two', 'three']);
console.log(checkNumberClass('one')); // "one"
console.log(checkNumberClass('horse')); // Error: The class "seven" is not available

var checkAnimalClass = cssClassChecker(['horse', 'cow', 'mule']);
console.log(checkAnimalClass('horse')); // "horse"
console.log(checkAnimalClass('one')); // Error: The class "one" is not available 

Closures

Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

(the Scopes & Closures book)

var a = 2; // outer scope

function foo() {
  console.log(a);
}

foo(); // 2

function bar() {
  var b = 222; // inner scope
  return b;;
}

console.log(bar()); // 222 -- closure!
b; // ReferenceError: b is not defined

Closures

Closures can create real data privacy & encapsulation.

 

As opposed to common _ convention ...

// _ prefix = conventional privacy

var foo = {
  _secret: null,
  _private: 'dont look at me',
  setSecret(val) {
    this._secret = val;
  },
  getSecret() {
    return this._secret;
  }
}

foo.setSecret(7);
foo.getSecret(); // 7
foo._secret; // 7 -- revealed!
foo._private; // 'dont look at me' -- but we looked

Closures

Closures can create real data privacy & encapsulation.

 

e.g. store.js

// "module pattern"
function bar(id) {
  id = id || 'nobody';
  var secret = null;
  var priv = 'you cant see me';
  
  function setSecret(val) {
    secret = val;
  }

  function getSecret() {
    return secret;
  }

  function revealId() {
    return `My ID is "${id}"`;
  }

  return {
    setSecret: setSecret,
    getSecret: getSecret,
    revealId: revealId
  };
  // ES6 shorthand
  // return { setSecret, getSecret };
}

var barInstance = bar('horse');
barInstance.setSecret(7);
barInstance.getSecret(); // 7
barInstance.revealId(); // 'My ID is "horse"'

Closures

Closures can create real data privacy & encapsulation.


e.g. store.js

function secretHolder(name, secret) {
  return {
    getName() {
      return name;
    },
    guessSecret(str) {
      return str === secret;
    }
  };
}

var foo = secretHolder('ed', 'eats horse');
foo.name; // undefined
foo.secret; // undefined
foo.getName(); // 'ed'
foo.guessSecret('wears shoes on hands'); // false
foo.guessSecret('eats horse'); // true

Object-Oriented Patterns

  • Almost everything is an Object
    • Arrays and functions are just special objects, can still be assigned arbitrary properties
    • Primitives are "boxed" as needed
      (e.g. "horse".length; // 5)
  • Objects can hold anything in properties (including functions [~ methods])
  • Objects can refer to their own data.
  • Objects can be reused/extended (~ "inheritance")

Namespace Objects

To group related data & functionality (and prevent scope pollution). e.g. Math native object (Math.PI, Math.SQRT2, Math.floor(x), Math.abs(x))

var someSet = {};

someSet.members = [1, 2, 3, 4];

someSet.sumOfSet = function() {
  var result = 0;
  this.members.forEach(function(num) {
    result += num;
  });
  return result;
};

someSet.addMember = function(mem) {
  this.members.push(mem);
}

someSet.sumOfSet(); // 10
someSet.addMember(5);
someSet.sumOfSet(); // 15

Namespace Objects

Our JS "packages" in NUI:

// form.js

exports.formConstants = require('./formConstants');

exports.flux = {};
exports.flux.formStore = require('./flux/formStore');

exports.components = {};
exports.components.FormElement = require('./components/FormElement');
exports.components.FormFull = require('./components/FormFull');
exports.components.FormSubmit = require('./components/FormSubmit');
exports.components.Checkbox = require('./components/Checkbox');
exports.components.Select = require('./components/Select');
exports.components.Label = require('./components/Label');

exports.mixins = {};
exports.mixins.FormControllerMixin = require('./mixins/FormControllerMixin');
const form = require('form');
form.component.FormElement;
form.mixins.FormControllerMixin;
form.formConstants;

Objects can refer to their own data

this - the magic keyword

var servant = {
  master: 'Baal',
  praiseMaster() {
    return 'Praise ' + this.master + '!';
  }
};

servant.praiseMaster(); // 'Prase Baal!'

var peon = _.clone(servant);

peon.master = 'King Whoever';
peon.praiseMaster(); // 'Praise King Whoever!'
servant.praiseMaster(); // 'Prase Baal!'

this is runtime scoped (rather than in "lexical scope"), referring to different objects at different times ---- a big subject ... for another time (?)

(Fake) Classes

It's not real "classical inheritance"

function Person(title) {
  this.title = title;
};

Person.prototype.tellTitle = function() {
  return 'My job title is ' + this.title;
};

var stacy = new Person('Boss');
stacy.tellTitle(); // 'My job title is Boss'
stacy.pantSize = 10;
stacy.buyPants = function() {
  console.log('I will buy pants of size ' + this.pantSize);
}
stacy.buyPants() // I will buy pants of size 10 

var julia = new Person('Underling');
julia.tellTitle(); // 'My job title is Underling'

stacy.title; // 'Boss'
julia.buyPants(); // TypeError: julia.buyPants is not a function (undefined)

Object.getPrototypeOf(stacy) === Object.getPrototypeOf(julia) // true
person.isPrototypeOf(stacy); // true
typeof stacy; // object (not Person)
stacy; // Object { title: "Boss", pantSize: 10, buyPants: stacy.buyPants() }

(Fake) Classes

Why are they not "real"? Because they only partially agree with prior conceptions (from e.g. Java) of how "classes" work:

  • New "types" are not really created.
  • Properties of the "class" are not in fact copied to "instances".
  • The "class" exists as an actual object that you can use, mutate, etc., on the same level as the "instances".
  • Extending classes to create inheritance chains becomes pretty weird.
  • You have to use this weird mysterious "prototype" syntax ...

ES6 (Fake) Classes

Syntactic sugar over the `Person.prototype` stuff, with new keywords like super, extends, static:

class Person {
  constructor(title) {
    this.title = title;
  }
  tellTitle() {
    return 'My job title is ' + this.title;
  }
  static stateMission(x) {
    console.log('The purpose of people is ' + x);
  }
}

const a = new Person('Master');
Person.stateMission('uncertain'); // The purpose of people is uncertain

class Servant extends Person {
  constructor() {
    super('Servant');
  }
  serve() {..}
}

const b = new Servant();
b.tellTitle(); // My job title is Servant

Prototypes!

What's really happening:

Behavioral delegation up the prototype chain

a.prop: try a > try a's prototype, b > try b's prototype, c ...

 

OLOO: Objects Linked to Other Objects (prototypal instead of classical OO)

// What happened in prior example?

b.tellTitle(); // My job title is Servant

// tries on b --- undefined
// tries on b's prototype Servant --- undefined
// tries on b's prototype's prototype (Servant's prototype), Person -- got it

b.toString();
// goes all the way up the chain to Object.prototype

Embracing Prototypes

Object.create() creates a new object and sets its prototype.

var person = {
  tellTitle() { return 'My job title is ' + this.title; }
};

var stacy = Object.create(person); // set prototype of new object
stacy.title = 'Boss';
stacy.tellTitle(); // 'My job title is Boss'
stacy.pantSize = 10;
stacy.buyPants = function() {
  console.log('I will buy pants of size ' + this.pantSize);
}
stacy.buyPants() // I will buy pants of size 10 

var julia = Object.create(person);
julia.title = 'Underling';
julia.tellTitle(); // 'My job title is Underling'

stacy.title; // 'Boss'
julia.buyPants(); // TypeError: julia.buyPants is not a function (undefined)

Object.getPrototypeOf(stacy) === Object.getPrototypeOf(julia) // true
person.isPrototypeOf(stacy); // true
typeof stacy; // object (not person)
stacy; // Object { title: "Boss", pantSize: 10, buyPants: stacy.buyPants() }
// (no tellTitle() on stacy, only on person!)

Modules

Currently we're using CommonJS syntax,

the (current) node way:

const React = require('react'); // node modules
const moment = require('moment');
const Pikaday = require('pikaday');
const _ = require('customLodash');
const u = require('util'); // alias
const common = require('common');
const formConstants = require('../formConstants'); // relative paths
const StandardizedInput = require('./StandardizedInput');

const Icon = common.components.Icon;

/**
 * A standardized input for dates.
 */
const DateInput = React.createClass({
  // ...
});

module.exports DateInput;

Modules

Advantages:

  • Scope control: scope is contained within the module, so no danger of polluting the global scope or that of other modules.
  • Explicit dependencies
  • Explicit APIs: only add to `exports` what other modules will use (public)

 

How it works: Browserify

Javascript

By David Clark

Javascript

  • 822