Javascript – It Makes Sense, Mainly – Auto-boxing



Javascript – It Makes Sense, Mainly – Auto-boxing

0 0


js-preso

Javascript: Filling in the cracks

On Github SerpentJoe / js-preso

Javascript

It Makes Sense, Mainly

Peter Jones for Big Fish Games

You probably already know Javascript

  • You've built features with it
  • You've made edits to huge JS codebases
  • You know what a prototype is, what $.ready does, and why not to edit native prototypes

But maybe you hate it?

  • Features from your favorite language are missing
    • Classical OO
    • Private members
    • Interfaces
    • Threads
    • import
    • etc ...
  • Everything is global, nobody's in charge, code executes at random times in random contexts
  • this makes no @#*(% sense!

It doesn't have to be a mess.

  • JS provides little, but enables much
  • Some (most?) of the "missing" structural features can be implemented
  • Once you know the techniques, if you write bad code, it's on you. No pressure!

The language

How is a JS Object implemented?

  • As an associative array?
  • Close - as a linked list of associative arrays.
  • Setting and deleting only affects the "top" hashmap
  • Getting can potentially walk the whole list
  • If there's something you find weird about Javascript, it's probably because of this.

What's all this?

this is a function parameter.

  • Invocation is all that matters
  • Only difference is, it usually gets passed implicitly, syntactically
  • // Greeter object
    var greeter = {
        name : 'world',
        greet : function () {
            alert('Hello ' + this.name + '!');
        }
    };
    
    // Passing `this` syntactically
    greeter.greet(); // 'Hello world!';
    
    // Forgetting to pass `this`
    var greetFxn = greeter.greet;
    greetFxn(); // 'Hello undefined!' (or worse in strict mode)
    
    // Passing `this` explicitly
    greet.call({ name : 'my ragtime gal' }); // 'Hello my ragtime gal!'

No block scoping

var name = 'myself'; // Original values

var names = ['alvin', 'simon', 'theodore'];
for (var i=0, len=names.length; i<len; i++) {
    var name = names[i]; // Write to scoped variable in a loop
    console.log(name + ' was here');
}

// value from loop has leaked out because there's no block scoping
alert(name); // 'theodore'

Speaking of which …

Hoisting

  • Interpeters treat all variable and function declarations as though they occurred at the top of the scope.
  • This is not mentioned in any specification, but all implementations do it.

That means the following gets evaluated pretty ridiculously:

var myValue = 0;
function incrementMyValue() {
    myValue++;
    return;
    var myValue; // After the return?!?!
}

incrementMyValue();
alert(myValue);
Try it!

Auto-boxing

  • Not *everything* is an object in Javascript
  • Like Java, a few things are primitive: numbers, strings and booleans
  • But … you can still call methods on them?
    Number.prototype.introduceSelf = function () {
        alert('Hi there, nice to meet you, I\'m the number ' + this);
    };
    
    (57).introduceSelf(); // This is like code the guy from Saw would write
    Try it!

Autoboxing

It gets weirder …

var myString = 'weirdness ahoy';
alert(typeof myString); // 'string'

String.prototype.alertType = function () {
    alert(typeof this);
};
myString.alertType(); // 'object'

What the heck is going on???

Autoboxing

  • An object wrapper is automatically added to primitives when needed.
  • var person = 'obama';
    person.age = 52; // Autoboxed just in time for this to succeed
  • But it gets thrown out immediately!
  • alert(person.age); // 'undefined'

Autoboxing

You can also create boxed types manually.

var myValue = 7;
var myBoxedValue = new Number(7);
typeof myBoxedValue; // 'object'

But there is no reason to do this, and you shouldn't.

myBoxedValue == 7; // true
myBoxedValue === 7; // false
myBoxedValue == new Number(7); // false

If you never do this, then you'll never need valueOf

myBoxedValue.valueOf() === 7; // true

Recap: don't use

  • new Number
  • new String
  • new Boolean

eval

  • eval is evil! Crockford said so!
  • If you're using evalin 2014, it might be because you really need it.
  • Example: writing a module loader
  • But if there's literally any other way, you should still use that.

eval mechanics

OK, stick with me here

If eval is invoked directly, the script is executed in the current scope.

var myVar = 'outer';
(function () {
    var myVar = 'inner';
    alert(eval('myVar')); // 'inner'
}());

If invocation is indirect, the global scope is applied instead.

var myVar = 'outer';
(function () {
    var myVar = 'inner';
    var myReferenceToEval = eval; // this makes it indirect!
    alert(myReferenceToEval('myVar')); // 'outer'
}());

There's much more to it. Check out the excellent Perfection Kills post on the subject.

eval Conclusions

OK, so maybe eval really is evil.

Special values

null

null indicates a known unknown: we know something can go here, but we don't have a value for it.

undefined

undefined indicates an unknown unknown: we didn't even know something was supposed to be here.

Never set anything to undefined.

Special values

Infinity

Infinity is a value in JS.

1/0 === Infinity; // true

You can make it negative.

var negZero = 1 / (-Infinity); // -0
negZero === 0; // true
1 / negZero; // -Infinity

You can do math with it.

Math.log(Infinity); // Infinity
1/0; // Infinity
1/Infinity; // 0

But you can't serialize it.

JSON.stringify([Infinity]); // '[null]'

So you might want to use it sparingly.

Special values

NaN

NaN is not a number.

parseInt('what type of number am I'); // NaN

Except it is.

typeof NaN; // 'number'

It doesn't "do" comparisons.

NaN == NaN; // false
NaN < Infinity; // false

So you need this handy builtin function to test for it:

isNaN(NaN); // true

Special values

NaN

But be careful:

isNaN('i mean … i am, technically, not a number'); // true

You must say what you mean:

function isExactlyNaN(value) {
    return (typeof value === 'number') && isNaN(value);
}
isExactlyNaN(NaN); // true
isExactlyNaN('just a string'); // false

But you should rarely need to be this precise.

Truthiness

This one's easy. Here are all the falsy values:

  • false
  • 0
  • ''
  • null
  • undefined
  • NaN

If you're not on this list, then (!!you) === true.

Strict mode

(function () {
    'use strict';
    alert('I am in strict mode');
}());

Basically no-BS mode - we should move to this increasingly

What does strict mode do?

Prevents implicit globals

var listsOfNames = [
    ['beyonce', 'madonna', 'cher'],
    ['goku', 'vegeta', 'krillin'],
    ['romney', 'dole', 'kerry']
];

for (var i=0, len=listsOfNames.length; i<len; i++) {
    printAllNames(listsOfNames[i]);
}

function printAllNames(listOfNames) {
    //  'use strict';
    // We're about to forget to declare `var i`
    // Would have been an error in strict mode
    for (i=0, len=listOfNames.length; i<len; i++) {
        console.log(listOfNames[i]);
    }
}

Strict mode

Another implicit global example:

// Note: be careful to make no changes to the passed object!
function addFieldsToIllustrationDescriptor(myIllustrationDescriptor) {
    // Clone the original to prevent modifications
    mylIlustrationDescriptor = _.clone(myIllustrationDescriptor, true);
    // Make changes
    myIllustrationDescriptor.hello = 'world';
    myIllustrationDescriptor.goodnight = 'moon';
    // Return modified copy
    return myIllustrationDescriptor;
}

Did you catch it?

Strict mode

It does more too:

  • No more arguments.callee or myFunction.caller
  • Prevents eval from introducing new vars
  • Throws an exception when mutating non-mutable properties
  • No more with
  • And more small changes that we won't go into. But MDN does

What's not included?

  • The DOM and BOM
  • location, window, document, etc.
  • This includes the Timers API (setTimeout, setInterval)
  • This means the entire concept of the event loop is provided by the browser!

What's the event loop?

  • The reason Javascript is useful for anything at all.
  • Javascript is single threaded
  • Your code blocks while it runs, but you can push more tasks onto the queue

Who pushes onto the event queue?

  • Network requests
  • setTimeout, setInterval, requestAnimationFrame
  • user events (click, keypress, etc)
  • postMessage
  • Basically everything interesting

None of this is in the spec.

  • Spidermonkey doesn't have an event loop!
  • But you can invent your own

Patterns

Method borrowing

  • Methods aren't tied to an instance - they have to be told what this is
  • Might as well abuse their trust and manipulate them for our own gain, right?

Method borrowing

  • Take somebody else's method and use it on something related but distinct
  • For example, NodeLists don't have the Array forEach method
    var allEls = document.querySelector('*');
    allEls.forEach; // undefined
  • But that doesn't stop us
    // Use empty array's forEach method, but apply it to allEls
    [].forEach.call(allEls, function (el) {
        console.log(el);
    });

Method borrowing

  • Another example: varargs
  • Get all params past the second:
    function doAThing(thing1, thing2/*, more things ...*/) {
        // arguments doesn't have a slice method, so this fails
        var moreThings = arguments.slice(2);
    }
  • Don't do that, do this
    function doAThing(thing1, thing2/*, more things ...*/) {
        // We borrowed the method
        var moreThings = [].slice.call(arguments, 2);
    }

Bound functions

  • Borrowable methods are flexible.
  • What if you don't want to be flexible?
  • What if you want to be predictable?
MyClass.prototype.getAndBroadcast = function (resourceName) {
    this.fetchResource(resourceName, function (data) {
        this.triggerEvent('getResource', data);
        // Above fails because `this` has wrong value!
    });
};
  • this is a parameter, remember?
  • Why are we trusting doNetworkRequest to invoke our callback the way we want?

Bind it yourself!

MyClass.prototype.getAndBroadcast = function (resourceName) {
    this.fetchResource(resourceName, function (data) {
        this.triggerEvent('getResource', data);
        // Everything's peachy now
    }.bind(this));
};

Function.prototype.bind is available natively in IE8 and up.

But it's also easy to implement yourself.

Function.prototype.bind = function (ctx) {
    var myself = this;
    return function (/*arguments*/) {
        return myself.apply(ctx, arguments);
    };
};

Bindings are forever

  • Function.prototype.bind creates a new function that invokes the old one
  • However you invoke the new one, its invocation of the old one is locked in place
  • .bind all you want, .call all you want, .apply all you want, but bindings are forever
“Bindings are forever” — K. West

Wrapped functions

Problem: users of your classical OO implementation want to use super in their methods.

Solution: wrap their methods!

// save old method
var originalMyMethod = MyClass.prototype.myMethod;
// wrap and overwrite
MyClass.prototype.myMethod = function () {
    // Set this._super so the user's method can use it
    this._super = MyParentClass.prototype.myMethod;
    // Invoke the original
    var result = originalMyMethod.apply(this, arguments);
    // Unset this._super
    this._super = null;
    // Done
    return result;
};

Wrapped functions

Be careful: a wrapped function is a bound function.

When you control invocation of a function, you'd better get it right!

Get used to writing .apply(this, arguments)

Strategy pattern

What's wrong with switch?

  • Code in case statements can have side effects on code above and below
  • Lack of block scope makes it even worse
  • New cases must go into the same file
  • Fallthrough is occasionally useful but almost always perilous

Strategy pattern

Instead of this:

var myValue;
switch (type) {
    case 'absolute':
        myValue = parseInt(value);
        break;
    case 'random':
        myValue = Math.random();
        break;
    default:
        console.log('WARNING: unknown type');
        myValue = null;
        break;
}

Strategy pattern

Consider this:

// Define strategies
var STRATEGIES = {
    absolute : function (value) {
        return parseInt(value);
    },
    random : function (value) {
        return Math.random();
    }
};
function DEFAULT_STRATEGY(value) {
    console.log('WARNING: unknown type');
    return null;
}
// Choose strategy
var strategy = STRATEGIES[type] || DEFAULT_STRATEGY;
// Execute strategy
myValue = strategy.call(null, value);

Strategy pattern

Advantages:

  • Code is explicit: pick a strategy, then run it
  • Strategies share no scope
  • Strategies can easily be contributed from elsewhere
  • Case fallthrough is impossible

Higher order functions

What does this snippet do?

var myIds = [];
for (var i=0, len=nodes.length; i<len; i++) {
    if (!(nodes[i] instanceof MyClass)) {
        continue;
    }
    myIds.push(nodes[i].id);
}

How about this one?

var myIds = nodes
        .filter(function (node) { return (node instanceof MyClass); })
        .map(function (node) { return node.id; });

Take it further with Underscore

Underscore is a library that provides an extended API for chainable functional programming

_(document.querySelectorAll('script[src]'))
        // Get the src attribute
        .pluck('src')
        // Ensure fully qualified URLs
        .map(function (url) {
            var linkEl = document.createElement('a');
            linkEl.href = url;
            return url;
        })
        // make all caps for some reason
        .invoke('toUpperCase')
        // And on and on
        …

Lo-Dash is a fork of Underscore that improves performance and extends the API.

Named params

Which code would you rather come across?

doNetworkRequest('casino', 'default', 'myTestAlert', 480, true);
doNetworkRequest({
    gameType : 'casino',
    viewSet : 'default',
    viewName : 'myTestAlert',
    size : 480,
    retina : true
});
  • Named arguments make code self-documenting.
  • They also make it easier to deprecate parameters later.
  • Consider using them if your function has more than two or three parameters

Asynchrony with promises

  • SCENARIO: You need to write a function to accomplish something asynchronously.
  • QUESTION: What is the minimum number of arguments it can take?
  • ANSWER: One
    // Even in the minimal case, it needs a callback to run upon completion
    doNetworkRequest(callbackFxn);

Isn't this weird?

  • What is this function going to do with my callback?
  • Will it be invoked once, or multiple times?
  • Will it be invoked immediately, or after the current event loop task finishes?
  • What if I don't provide the callback? Will it blow up trying to run an undefined function?
  • What will happen if my callback throws an exception?
  • Why do I have to trust this other function so much?

Can't we be like Cheesecake Factory?

Promises

  • A promise is a tiny little disposable object returned by an async function
  • It is that function's promise to complete the task … soonish
  • Calling code can specify as many handlers as it wants, AS LATE AS IT WANTS
    var requesting = doNetworkRequest();
    // Specify one handler immediately
    requesting.done(function (data) {
        console.log('Look at all this data!');
        console.log(data);
    });
    // Wait a while and then specify another
    var ONE_HOUR = 60 * 60 * 1000; // millis
    setTimeout(function () {
        requesting.done(function (data) {
            console.log('Guys, remember when we got this sweet data?');
            console.log(data);
        });
    }, ONE_HOUR);

Basic example: ImageAsset

function ImageAsset(url) {
    this._url = url;
}

ImageAsset.prototype.fetch = function () {
    var url = this._url;
    return new Promise(function (resolve, reject) {
        var img = new Image;
        img.onload = function () {
            resolve(img);
        };
        img.onerror = function () {
            reject(img);
        };
        img.src = url;
    });
};

What if we want to fetch earlier?

function ImageAsset(url) {
    this._url = url;
    this.fetch(); // Start fetching now so we won't have to wait later
}

ImageAsset.prototype.fetch = function () {
    // Same implementation as before
};

Problem: the result of that fetch disappears!

Solution: idempotence

Idempotence (/ˌaɪdɨmˈpoʊtəns/ eye-dəm-poh-təns) is the property of certain operations in mathematics and computer science, that can be applied multiple times without changing the result beyond the initial application.

Add idempotence

ImageAsset.prototype.fetch = function () {
    if (!this._fetching) {
        var url = this._url;
        this._fetching = new Promise(function (resolve, reject) {
            // Same implementation as before
        });
    }
    return this._fetching;
};

Now the first promise to fetch the texture gets saved, and subsequent requests receive the very same promise.

That means the actual work (the network request) is guaranteed to happen exactly once, no matter how many times it's requested.

Now create a subclass: Animation

We assume it has a method play which returns a promise

// subclass ImageAsset
function Animation(url) {
    ImageAsset.apply(this, arguments); // call super
}
Animation.prototype = Object.create(ImageAsset.prototype);

// define play method
Animation.prototype.play = function () {
    …
    // Return a promise that resolves when the animation completes
};

Problem: Race conditions

Calling code is getting race conditions because it's trying to play an animation that hasn't been fetched.

No problem!

Animation.prototype.play = function () {
    return this.fetch()
            .then(function (texture) {
                …
                // Original logic goes here
            });
};

Calling fetch is always safe because it's idempotent.

But what if we want to go crazy?

You can do that. You can go crazy.

var anim = new Animation();
anim.fetch()
        // play
        .play()
        // post to Facebook
        .then(function () {
            return new Promise(function (resolve, reject) {
                FB.ui({ … }, resolve);
            });
        })
        // Redirect and show a reward for sharing
        .then(function () {
            location.pathname = '/one/million/chips';
        })

What's the point of promises?

  • Better idiom for chains of async logic
  • Each task in the chain is nicely decoupled from all others
  • Solves the "pyramid of doom"
    var anim = new Animation();
    anim.fetch(function () {
        // When done fetching, play
        this.play(function () {
            // When done playing, post to Facebook
            FB.ui({ … }, function () {
                // When done posting, show a reward for sharing
                location.pathname = '/one/million/chips';
            });
        });
    });

Modules

What's wrong with this source file?

// alertHelloWorld.js
var myMessage = 'hello world';
setTimeout(function () {
    alert(myMessage);
}, 1e3);

Nothing, unless this file loads on the same page:

// alertBeAManHulk.js
var myMessage = 'be a man, hulk';
setTimeout(function () {
    alert(myMessage);
}, 1e3);

myMessage was meant as private in both cases, but they overwrote each other!

Modules

Operating in top-level scope means all our implementation details are exposed!

How can we control what we expose?

Modules

First idea: immediate functions

// alertHelloWorld.js
(function () {
    var myMessage = 'hello world';
    setTimeout(function () {
        alert(myMessage);
    }, 1e3);
}());
// alertBeAManHulk.js
(function () {
    var myMessage = 'be a man, hulk';
    setTimeout(function () {
        alert(myMessage);
    }, 1e3);
}());

Now they don't interfere!

Modules

First idea: immediate functions

  • The basic idea of modules is to create a function scope where we can hide things that shouldn't be public.
  • Immediate functions are the most basic implementation of the module pattern.
  • They also give the most bang for the buck.
  • You can start using them right now, the second you get back to your desk.

Modules

Drawback: if I want to expose something to other modules, I need to have knowledge of where to put it.

// Logger.js
(function () {
    var Logger = {
        log : function (msg) {
            console.log(msg);
        }
    };
    // export
    window.Logger = Logger;
}());

Now what if I want to put it somewhere else?

// Logger.js
(function () {
    // … impl …

    // export
    window.sag.Logger = Logger; // At least until it needs to move again
}());

Modules

Second idea: AMD

Asynchronous Module Definition makes use of two top level functions, define and require.

Define a module:

// util/Logger.js
define([], function () {
    return {
        log : function (msg) {
            console.log(msg);
        }
    };
});

Require a module:

// Logger.js
require(['util/Logger'], function (Logger) {
    Logger.log('this is so much better than using console.log directly');
});

Modules

Second idea: AMD

  • Now we export nothing onto the global namespace!
  • We return our module to the define function, which associates it with the path where it was found.
  • We also specify our own dependencies!
  • The AMD implementation will fetch dependencies for us
  • We don't even need to know the order to load modules in
  • We can just load:
    <script src="main.js"></script>
    and main.js will require games/casino.js , which will require minigames/plinko.js , which will require util/Logger.js.
  • Actually this sounds kind of slow … but we can do it quicker.

Modules

Second idea: AMD

Drawback: the syntax is bizarre.

Corollary: long lists of dependencies are hideous.

// Break it onto two lines
require(['look', 'at', 'how', 'many', 'modules', 'i', 'need'],
function (look, at, how, many, modules, i, need) {
    // Actual work goes here
});

Imagine having to fix a merge conflict in that list!

Modules

Third idea: CommonJS

What I really want to write is more like:

var look    = require('look');
var at      = require('at');
var how     = require('how');
var many    = require('many');
var modules = require('modules');
var i       = require('i');
var need    = require('need');

That will look way better in a diff, and it makes more sense too.

Performance

Not much to say!

  • Keep local references to:
    • object properties used more than once
    • closure variables
  • Avoid eval and new Function
  • Minimize lookups on deep prototype chains
  • Consider prototypes locked at runtime
  • try/catch is expensive
  • Use console.time and console.timeEnd to test performance of a unit
  • Use Chrome profiler to test performance of a system

Questions?