Such Java,Much Script! – An introduction to JavaScript – Preface



Such Java,Much Script! – An introduction to JavaScript – Preface

1 3


such-java-much-script

An introduction to JavaScript.

On Github seutje / such-java-much-script

Such Java,Much Script!

An introduction to JavaScript

Steve De Jonghe

Mandatory introduction slide

Overview

  • Preface
  • Types
  • Type coercion
  • Truthy falsy
  • Arrays
  • Objects
  • Functions: Scope, 'this', async, chaining, instances, inheritance
  • The DOM: Traversal, doc.rdy
  • Events: Bubbling, propagation, delegating
  • Ajax: More async

Preface

Preface

Tooling

  • The browser is your #1 tool for writing JavaScript:
    • Fully fledged IDE with all the debugging tools you could possible want
    • Allows conditional breakpoint
    • Allows breaking on things non-code (subtree modification, etc)
    • Allows swapping remote files with locals ones at-runtime
    • Allows beautifying production code
    • Console API
    • much, much more! (like The Breakpoint)

Preface

Error prevention

  • The linter is you friend!
  • No, seriously, it will tell you to use semicolons and ===, which will save you countless hours!

Types

They see me coercing...

Everything is an object, except primitives:

Primitives vs Objects

  • Primitives cannot be broken down to an extended type and cannot be assigned custom properties.
  • Primitives are not passed by reference.
  • All the rest are essentially extended Objects (Array, Date, etc.) and are passed by reference.
  • Use literal notation where possible, avoid using new keyword on base type object wrappers
typeof 'foo'; // "string" - Good
typeof String('foo'); // "string" - Not so good
typeof new String('foo'); // "object" - Bad
typeof (new String('foo')).valueOf(); // "string" - Awkward
(new String('foo')) === 'foo'; // false - o.O

Cautionary note on typeof

It lies!

typeof null; // "object"
typeof []; // "object"
typeof /regex/; // "function" in some browsers

Dead set on matching the type?

Force the Object.toString method to do your bidding!

Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(/regex/); // "[object RegExp]"

(More on prototypes and function methods later!)

If you need this, you probably have a bigger problem.

Cautionary note on numbers

  • Floating point!
    0.2 + 0.2 + 0.2; // 0.6000000000000001 WAT?!
  • don't care about precision?
    • Number.toFixed() to determine float precision
      (0.2 + 0.2 + 0.2).toFixed(2); // 0.60
    • parseInt() with a radix
      parseInt(42.56789, 10); // 42(!)
  • Working with currency? Do everything in cents or use a library to provide bigger decimals with arbitrary precision

Bonus!

var foo = new Number(0);
foo === 0; // "false" - foo is an obj, 0 is an int.
foo == 0; // "true" - so foo is falsy?
!!foo; // "true" - I guess not o.O

Cautionary note on strings

  • Not as bad as PHP, but still pretty weird sometimes.
  • parseInt() if you want to do maths, always provide radix parameter!
    parseInt('042'); // Some browsers will default to octal 66.
    parseInt('042', 10); // Force parsing as decimal.
    parseInt('12foo34', 10); // 12 (!)

More caution

  • Unicode handling can be a bit wonky
    var poo = '💩';
    poo.length; // 2
    poo.split('').reverse().join(''); // "��"
    poo === '\uD83D\uDCA9'; // true
    var a = 'se\xF1orita'; // "señorita"
    var b = 'sen\u0303orita'; // "señorita"
    a == b; // false
    a.length; // 8
    b.length; // 9
  • Use Punycode.js or funky regex if you need to support astral plane characters.

Bonus!

  • They can be partially accessed like an array (mainly getters).
var foo = 'foo';
foo[0]; // 'f'
foo[0] = 'p'; // Nice try, no setters tho!
foo.split('f').join('p'); // 'poo' (like PHPs explode/implode)
foo.length; // 3
foo.bar = 'baz'; // Nice try, primitives can't have properties tho!
foo.bar; // undefined

This is often referred to as "Array-like", but should not be considered a fully-fledged array.

Bonus Bonus!

var foo = new String('foo');
// Secretly an object wrapper pretending to be a string
foo === 'foo'; // false
foo == 'foo'; // true
foo.lol = 'lol'; // now it actually sticks!
foo.lol; // 'lol'

Type coercion

The operator matters!

With strings and numbers, the + operator will coerce everything to strings

'123' + 4; // '1234'
1 + '234'; // '1234'

Likewise, the - operator will coerce everything to a number

'123' - 4; // 119
1 - '234' ; // -233

Type coercion

cheap casting!

Using the unary + operator, we can easily cast to a number.

This works similar to parseFloat(), except it will return NaN if any part of the string could not be parsed.

'1.23' + '1.23'; // '1.231.23' (string)
(+'1.23') + (+'1.23'); // 2.46 (parens optional)
+'1.23' + + '1.23'; // 2.46
+'1.23'; // 1.23
+'1.23.23'; // NaN (!!)
+'1.999999999999999'; // 1.999999999999999
+'1.9999999999999999'; // 2

This does not save you from NaNtyNaN problems though, always check user input!

Truthy falsy

What !!is and what !is?

The following values are considered not "truthy" and will coerce to false:undefined, null, NaN, 0, ''

Gotcha: !!'0'; // "true"

Truthy falsy

Shortcuts

// someInit will not be executed unless someVar is falsy
var foo = someVar || someInit();
// Useful for defaults!
var bar = function (x) {
  x = x || myDefaults;
};
// Or checks!
var ready = someInit() && attachThatNeedsToRunAfterInit();

The mighty Array!

  • Unlike in PHP, array keys can only be numeric, using string keys on an array will cause it to show its underlying object-like nature.
    var foo = ['poo'];
    foo.length; // 1
    foo.bar = 'bar';
    foo.length; // 1
    foo.bar; // 'bar' (foo['bar'] also works)
    foo; // ['poo']
  • For example: jQuery's $() query returns an array-like object of results that's augmented with properties and methods.

The mighty Array!

  • Avoid manually assigning keys, missing ones will be undefined. Instead, use Array.push().
    var foo = ['lala', 'hihi'];
    foo.push('hoho'); // 3 (Array.push returns new length)
    foo[7] = 'lolapi';
    foo.length; // 8
    foo[6]; // undefined

The mighty Array!

  • Never ever for...in loop on an array, use a regular for-loop!
    var foo = ['bar'];
    foo.lol = 'gotcha!';
    for (var i in foo) {
      console.log(foo[i]); // 'bar', 'gotcha!'
    }
    for (var i = 0, len = foo.length; i < len; i++) {
      console.log(foo[i]); // 'bar'
    }

Don't care about IE8?Use Array.forEach(callback) (note: unbreakable!)

The even mightier Object!

  • Basis for most types in JavaScript.
  • Changing its prototype affects nearly everything.
  • for..in loops loop over properties from the entire chain!
    Object.prototype.lol = 'lolapi';
    var foo = {'meh': 'poo'};
    for (var key in foo) {
      console.log(key); // 'meh', 'lol'
      if (foo.hasOwnProperty(key)) {
        console.log(key); // Just 'meh'
      }
    }
    More on inheritence later!

The mightiest: Function

Creator of scope

function foo() {
  var bar = 'lol';
}
foo();
console.log(bar); // undefined
var bar = 'lol';
function foo(bar) {
  return bar;
}
console.log(foo()); // undefined

Function: Creates scope

Immediately Invoked Function Expression

var x = 'bar';

(function (x) {

  x === 'foo'; // true

})('foo');

x === 'bar'; // true

// Roughly equivalent (without accessible leftovers)

var foo = function(x) {}
foo('foo');

Function: Creates scope

Immediately Invoked Function Expression

(function (x, y) {

  x === 'foo'; // true
  y === window; // true if in a browser and not hosed outside scope
  var foo = "I won't leak to the global scope!";
  y.bar = "I will!"; // same as window.bar

})('foo', this);
foo; // undefined
bar; // "I will!"

Function: IIFE

Why is this useful?

(function(window){
  // Scoped variable.
  var myPrivate = 'foo';
  // Intentionally leaked getter function to the global scope.
  window.getPrivate = function() {
    // function is defined within same scope so it can access the var
    return myPrivate;
  };
})(this);
getPrivate(); // 'foo'
myPrivate; // ReferenceError: myPrivate is not defined

The mightiest: Function

Can return functions

function compute (x) {
  return function (y) {
    return x*y;
  }
}
compute(2)(3); // 6

The mightiest: Function

And so much more!

function maths(x) {
  return {
    compute: function(y) {
      return x * y;
    },
    add: function(y) {
      return x + y;
    },
    abs: Math.abs(x)
  }
}
maths(2).compute(3); // 6
maths(2).add(3); // 5
maths(-2).abs; // 2

The mighties: Function

"this" is madness!

var foo = {
  a: 'lol',
  b: function() {
    return this.a;
  }
};
foo.b(); // 'lol'
var bar = foo.b;
bar(); // undefined

Here, bar is actually window.bar, so the window object becomes the context, and there is no window.a.

The mightiest: Function

Special methods and magic "this"!

var foo = function() {
  return this;
}
// Create a clone of the function with a fixed context of 'bar!'.
var bar = foo.bind('bar!');

foo(); // returns reference to the window object.
bar(); // 'bar!'
// Call foo in the context of 'lol'
foo.call('lol'/* , arg1, arg2 */); // 'lol'
foo.apply('lol'/* , [arg1, arg2] */); // 'lol'

The mightiest: Function

So how do I function?

// Defined at run-time.
foo(); // TypeError: undefined is not a function

var foo = function () {};
// Defined at parse-time for this block.
foo(); // no error

function foo() {};

The mightiest: Function

What is even going on here?

// Essentially an anonymous function stored in a variable.
var foo = function () { };

foo.name; // '' (empty string)
// A named function.
function foo() { };

foo.name; // 'foo'
// But wait!
var foo = function bar() { };

foo.name; // 'bar'
bar.name; // ReferenceError: bar is not defined
// WAT?!

The mightiest: Function

I don't even...?

var foo = function bar() {
  bar.meh = function() { return bar.name; };
};
foo(); // Calling it makes it give itself the "meh" method.
foo.meh(); // "bar"

The mightiest: Functions

Bonus points!

// We can return ourselves!
var foo = function bar() {
  // Do some things...

  return bar;
};

foo.name; // 'bar'
foo().name; // 'bar'
foo()()()()()()()(); // ...

Intermission

Hoisting

Variable and function declarations are invisibly moved to the top of their scope

one(); // TypeError "one is not a function"
two(); // Works.
three(); // TypeError "three is not a function"
four(); // ReferenceError "four is not defined"

var one = function() {}; // Anonymous function expression.
function two() {}; // Function declaration.
var three = function four() {}; // Named function expression.

one(); // Works.
two(); // Works.
three(); // Works.
four(); // ReferenceError "four is not defined"

Intermission!

Hoisting

Actual interpretation

var one, three; // Variable name is "hoisted" to top of scope.
function two() {}; // Entire function declaration gets hoisted.

one(); // TypeError "one is not a function"
two(); // Works.
three(); // TypeError "three is not a function"
four(); // ReferenceError "four is not defined"

one = function() {}; // Anonymous function expression.
three = function four() {}; // Named function expression.

one(); // Works.
two(); // Works.
three(); // Works.
four(); // ReferenceError "four is not defined"

The mightiest: Functions

Why is this even useful?

// Remember, we're also an object.
var foo = function bar(op) {
  // Fixed internal reference!
  bar.doStuff = function() {
    // Do stuff
    return bar;
  };
  bar.doMoreStuff = function() {
    // Do more stuff
    return bar;
  };
  if (op && bar[op]) { // if an op was passed and it exists
    return bar[op](); // call it and return the return
  }
  return bar; // Return ourselves
};
foo().doStuff().doMoreStuff(); // or foo('doStuff')('doMoreStuff')...

The mightiest: Functions

Callbacks & setTimeout/setInterval

// Logs 'HOLAS!' every second.
var foo = function() {
  console.log('HOLAS!');
  window.setTimeout(foo, 1000);
};
window.setTimeout(foo, 1000);
// Bad:
var bar = function() {
  console.log('What if I took more than 1 second to run?');
};
window.setInterval(bar, 1000);

The mightiest: Functions

AJAX

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      doStuffWithData(JSON.parse(xhr.responseText));
    }
  }
};
xhr.open('GET', '/lol', true);
// xhr.setRequestHeader('Content-Type', 'some/mime-type');
xhr.send();

The mightiest: Functions

So how do I instance?

// Define it as a constructor.
function Foo() { // capital is just convention
  this.bar = function() { return this; }; // Note the "this"
};
var one = new Foo(); // Note the "new"
var two = new Foo();
one === two; // false, they are 2 separate instances
one.bar() === two.bar(); // false
one.bar() === one; // true
one instanceof Foo; // true
Foo(); // note the absence of "new"
window.bar; // We just hosed the global scope

The mightiest: Functions

Inheritance

// Set up constructor.
var Foo = function() {
  this.bar = 0;
};
// Define its base object.
Foo.prototype = {
  addBar: function() { this.bar++; }
};
var foo = new Foo(); // foo > Foo.prototype > Object.prototype > null
foo.addBar();
foo.bar; // 1
Object.getPrototypeOf(foo).bar; // undefined (not available in IE<9)
Foo.prototype.meh = 'lol';
foo.meh; // 'lol'

The mightiest: Functions

Inheritance

var Foo = function() {
  this.bar = 0;
  this.addBar = function() { return ++this.bar; };
};
var Bar = function() {
  this.addBar = function(i) { this.bar = this.bar + i; return this.bar; };
};
Bar.prototype = new Foo(); // Set Bar's prototype to new instance of Foo
var foo = new Foo(); // Foo.prototype is essentially an empty object
var bar = new Bar(); // bar > Bar.prototype (Foo) > Foo.prototype ({}) > Object.prototype > null
bar.addBar(7); // 7
foo.addBar(7); // 1
bar.bar; // 7
Bar.prototype.meh = 'lol';
bar.meh; // 'lol'
foo.meh; // undefined

The mightiest: Functions

Inheritance

var Foo = function() {
  this.bar = 0;
  this.addBar = function() { return ++this.bar; };
};
var Bar = function() {
  this.addBar = function(i) { this.bar = this.bar + i; return this.bar; };
  this.callSuper = function(op, arg) {
    return Object.getPrototypeOf(this)[op].call(this, arg);
  };
}
Bar.prototype = new Foo();
var foo = new Foo();
var bar = new Bar();
bar.callSuper('addBar', 7); // 1

Who wants constructors anyway?

Keeping things light

function create(parent) { // aka "justMakeMeAnObjectThatInheritsFromThis"
  var Constructor = function () {}; // Empty constructor function
  Constructor.prototype = parent; // Attach prototype object
  // Return new instance, inheriting from parent
  return new Constructor();
}
var a = { foo: 'lol' };
var b = create(a);
b.foo; // 'lol'
a.bar = 'baz';
b.bar; // 'baz'
b.meh = 'nope';
a.meh; // undefined

Enter: Object.create

Syntax

Object.create(proto /*[, props ]*/)

Summary

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

Arguments

proto: The object which should be the prototype of the newly-created object.

props: An object whose enumerable own properties specify property descriptors to be added to the newly-created object, with the corresponding property names.

Enter: Object.create

Polyfill (IE<9)

if (typeof Object.create != 'function') {
    (function () {
        var F = function () {};
        Object.create = function (o) {
            if (arguments.length > 1) { throw Error('Second argument not supported');}
            if (o === null) { throw Error('Cannot set a null [[Prototype]]');}
            if (typeof o != 'object') { throw TypeError('Argument must be an object');}
            F.prototype = o;
            return new F();
        };
    })();
}

In the wild!

From BackBone source:

var extend = function(protoProps, staticProps) {
  var parent = this;
  var child;
  if (protoProps && _.has(protoProps, 'constructor')) {
    child = protoProps.constructor;
  } else {
    child = function(){ return parent.apply(this, arguments); };
  }
  _.extend(child, parent, staticProps);
  var Surrogate = function(){ this.constructor = child; };
  Surrogate.prototype = parent.prototype;
  child.prototype = new Surrogate;
  if (protoProps) _.extend(child.prototype, protoProps);
  child.__super__ = parent.prototype;
  return child;
};

The DOM

Object with many fingers in many things

  • Object-tree representation of entire document
  • Contains and exposes most of the APIs we use
  • Used to be really, really, really annoying, now just really really
  • Traversing this mess is the main purpose of libraries like jQuery
  • Manipulations (and some getters, even) are costly and should be kept to a minimum.

Traversal

All the nesting!

// [ <html element> ]
document.children;

// [ <head element>, <body element> ]
document.children[0].children;

// [ <element with class someClass>, <...> ]
document.getElementsByClassName('someClass');

// equivalent (note: IE8 only supports CSS2 selectors)
document.querySelectorAll('.someClass');

Caution!

Document ready

Remember: You cannot query for an element if it hasn't been parsed yet. Place scripts at the bottom or use things like jQuery's $(document).ready(function() { ... })

<html>
  <head>
    <script>
      //TypeError: Cannot read property 'querySelector' of null
      var foo = document.body.querySelector('.foo');
    </script>
  </head>
  <body>
    <div class="foo">foo</div>
    <script>
      // Works fine
      var foo = document.body.querySelector('.foo');
    </script>
  </body>
</html>

Caution!

As mentioned before, array-like does not mean it's an array.

var foo = document.querySelectorAll('.foo');
foo.forEach; // undefined - does not have array method.
// Invoke array method in the context of our collection.
Array.prototype.forEach.call(foo, function(el) { // Pass in anonymous function
  console.log(el); // logs out each element separately.
});

The driving force of it all: Events

Bubbles!

// Define a handler for our events.
var handler = function(event) { console.log(this, event.target); },
    body = window.document.body, // Cache some element references.
    form = body.querySelector('form'),
    button = form.querySelector('input[type="button"]');
// Add our event listener to the cached references.
body.addEventListener('click', handler);
form.addEventListener('click', handler);
button.addEventListener('click', handler);

The driving force of it all: Events

Event methods: stopPropagation!

// Handler that calls stopPropagation on the event.
var handler = function(event) {
      console.log(this, event.target);
      event.stopPropagation();
    },
    body = window.document.body, // Cache some element references.
    form = body.querySelector('form'),
    button = form.querySelector('input[type="submit"]');
// Add our event listener to the cached references.
body.addEventListener('click', handler);
form.addEventListener('click', handler);
button.addEventListener('click', handler);

The driving force of it all: Events

Event methods: preventDefault!

// Handler that calls preventDefault on the event.
var handler = function(event) {
      event.preventDefault();
    },
    form = window.document.body.querySelector('form');
// The handler will effectively prevent
// the native submit event from happening.
form.addEventListener('submit', handler);

The driving force of it all: Events

Advanced techniques: Delegating!

(function(window) {
  var handler = function(event) {
        // Only act if the actual target is '.foo'
        if (event.target.className === 'foo') {
          event.preventDefault();
          console.log('Do something!');
        }
      };
  // Attach the handler to the entire body element.
  window.document.body.addEventListener('click', handler);
})(this);

Like jQuery's $.fn.on(), $.fn.delegate, ...

The driving force of it all: Events

All the async!

// Bad:
var ajaxData = $.get('some/url', function(data) {
  return data;
});
console.log(ajaxData); // XMLHttpRequest object o.O
// Instead:
$.get('some/url', function(data) {
  console.log(data);
});
// Or use jQuery's new promises!
$.get('some/url').done(function(data) {
  console.log(data);
})

The driving force of it all: Events

But what if I need it?

(function($) {
  // initialize returnData in this scope
  var returnData,
      // Example function to shout out the data
      shoutOut = function() {
        if (returnData) {
          console.log(returnData);
        }
      };
  // Grab the data, assign the variable and call the function.
  $.get('some/url', function(data) {
    returnData = data; // Store it.
    shoutOut(); // Shout it.
  });
})(jQuery)

The driving force of it all: Events

What is actually going on?

var callback = function() { /* do something*/ };
$.get('some/url', callback);

What goes on internally:

  • Reference to callback function is stored.
  • When $.get is called, it creates and immediately returns the XMLHttpRequest object, it also stores it internally.
  • Time passes, interpreter continues.
  • If successfull -> callback.apply(XMLHttpRequest, [ data, textStatus, XMLHttpRequest])

Drupal specifics

Behaviors

Drupal.Foo = function(el) { this.el = el; /* do fancy stuff with this.el */ };
Drupal.Foo.prototype.destroy = function () { /* undo fancy stuff to this.el */ }
Drupal.behaviors.foo = {
  attach: function(context) {
    $('.some-selector', context).once('foo').each(this.attachHandler.bind(this));
  },
  attachHandler: function(i, el) {
    var $el = $(el),
        instance = new Drupal.Foo(el);
    $el.data('foo', instance);
    this.instances = this.instances || [];
    this.instances.push(instance);
  },
  detach: function(context) {
    $('.some-selector.foo-processed', context).each(this.detachHandler.bind(this));
  },
  detachHandler: function(i, el) {
    var $el = $(el),
        instance = $el.data('foo'),
        index = this.instances.indexOf(instance);
    instance.destroy();
    this.instances.splice(index, 1);
  }
};

Bonus!

Monkey-patching

// Probably the most annoying piece of JS in Drupal 7
Drupal.jsAC.prototype.hidePopup = function(keycode) {
    /* hidden some irrelevant stuff */
    // Hide popup.
    var popup = this.popup;
    if (popup) {
        this.popup = null;
        $(popup).fadeOut('fast', function() {
            $(popup).remove(); // <- doesn't hide, but actually removes!
        });
    }
    this.selected = false;
    $(this.ariaLive).empty();
};

Bonus!

Monkey-patching

// Remember, a function is just a variable holding a reference
// We can just change the reference to point to something else
Drupal.jsAC.prototype.hidePopup = function() { /* do nothing */ }
// Now it won't disappear and we can theme it!

Bonus!

Monkey-patching

// But what if we still wanna use the original?
(function(window){
  // Scope a reference!
  var _alert = window.alert;
  window.alert = function (arg) {
    // Do some extra stuff, maybe manipulate the arg
    console.log('Alert called!', arg);
    // Call the original function with proper arguments and "this" context
    _alert.call(this, arg);
  }
})(this)

Further reading

  • Mozilla Developer Network: Excellent reference for API functions and their level of support.
  • Web Platform: Excellent resource with examples of implementations.
  • The Breakpoint: Lovely series that focusses almost entirely on getting the most out of Chrome's DevTools.

The end!

Thanks for not throwing rotten fruit, much appreciated!