Future JavaScript – Fixing the Annoying Parts



Future JavaScript – Fixing the Annoying Parts

0 0


future-js-fixing-the-annoying-parts

A presentation on ES6, showing JavaScript parts that annoy us in ES3/ES5 and how they are fixed in newer versions of the language.

On Github bswierczynski / future-js-fixing-the-annoying-parts

Future JavaScript

Fixing the Annoying Parts

by Bartek Swierczynski / @bswierczynski

After this presentation…

I hope you'll be grateful we'll have ES6.

ToC, a.k.a. JavaScript, the embarrassing parts

  • Global by default
  • No array foreach loop
  • Broken isNaN()
  • Verbose function syntax
  • Loss of the this context
  • Only functions scope, no block scope
  • Lack of string methods
  • Hard time printing strings
  • No dymic keys in object literals
  • Lack of support for declaring object shape
  • Prototypes and private state don't mix
  • 'arguments' sux
  • No maps or other generic containers
  • Repeating 'use strict' all over again

Global by default

function init() {
    var foo = 100;
    bar = 42;
}
init();
                    
                    
console.log(bar);
                    
                  //Logs: 42
                    

Forgetting var declares a global variable.

Global by default

Declaration required

function init() {
    "use strict";
    var foo = 100;
    bar = 42;
}
init();
console.log(bar);
                    
        //ReferenceError: bar is not defined
                    
                    

Forgetting var throws in Strict Mode.

No Array foreach loop

We only have the good ol' for loop:

var arr = ["foo", "bar", "baz"];
for (var i = 0; i < arr.length; i++) {
    var value = arr[i];
    console.log("#" + i + ": " + value);
}
                    
var arr = ["foo", "bar", "baz"];
for (var i in arr) {
    var value = arr[i];
    console.log("#" + i + ": " + value);
}
                        

Don't use for..in on arrays!

  • Does not guarantee order
  • Iterates over all enumerable properties, not only indexes
  • Iterates over inherited properties, too

And I mean it! Don't!

No Array foreach loop

Array extras: .forEach()

var arr = ["foo", "bar", "baz"];
arr.forEach(function(value, i) {
    console.log("#" + i + ": " + value);
});
                    

Still much slower than a for loop.

No Array foreach loop

for-of

var arr = ["foo", "bar", "baz"];
for (var [i, value] of arr) {
    console.log("#" + i + ": " + value);
}
                    

Or, when you just need the value:

var arr = ["foo", "bar", "baz"];
for (var value of arr) {
    console.log(value);
}
                        

Broken isNaN()

isNaN(NaN);   // true
isNaN(123);   // false
isNaN("123"); // false
                    
isNaN("abc");
                        
                                      // true
                        

The global isNaN(v) function returns true not only if v is NaN, but also if it cannot be parsed as a Number.

To make things even more funny…

var n = NaN;
n == NaN;  // false
n === NaN; // false
                        

NaN is not equal to anything, including NaN.

Broken isNaN()

Number.isNaN()

Number.isNaN(NaN);   // true
Number.isNaN(123);   // false
Number.isNaN("123"); // false
Number.isNaN("abc"); // false
                    

Returns true only for NaN.

Verbose function syntax

var decentRates = employees
        .map(function(emp) {
            return emp.getDailyRate();
        })
        .filter(function(rate) {
            return rate >= 1500;
        });
                    
describe("Stack", function() {
    given(function() { this.stack = new Stack() })
    when (function() { this.stack.push("foo")   })
    then (function() { return this.stack.length === 1  })
})
                    

Verbose function syntax

Arrow functions

var decentRates = employees
        .map( (emp) => emp.getDailyRate() )
        .filter( (rate) => rate >= 1500 )
;
                    
describe("Stack", () => {
    given(() => this.stack = new Stack() )
    when (() => this.stack.push("foo")   )
    then (() => this.stack.length === 1  )
})
                    
  • Compact definition
  • Automaticcaly return the value of the last statement
  • Parens around argument list are required
  • Braces around the body are optional if there's only one statement
  • Cannot be used as constructors (with new)

Loss of the this context

function AutoSlidingGallery($slides) {
    var slideIndex = 0;
    
    setInterval(function() {
        this.switchToSlideAt(slideIndex);
        slideIndex = (slideIndex + 1) % $slides.length;
    }, 2000);
    
    this.switchToSlideAt = function() { /* ... */ };
}
                    

Boom! Cannot call this.switchToSlideAt() in the anonymous function since this no longer points to the gallery.

It points to the global object in sloppy mode and to undefined in strict mode.

Loss of the this context

Fortunately, there are some fixes (still, mildly annoying):

function AutoSlidingGallery($slides) {
    var that = this;
    var slideIndex = 0;
    
    setInterval(function() {
        that.switchToSlideAt(slideIndex);
        slideIndex = (slideIndex + 1) % $slides.length;
    }, 2000);
    
    this.switchToSlideAt = function() { /* ... */ };
}
                    
function AutoSlidingGallery($slides) {
    var slideIndex = 0;
    
    setInterval(function() {
        this.switchToSlideAt(slideIndex);
        slideIndex = (slideIndex + 1) % $slides.length;
    }.bind(this), 2000);
    
    this.switchToSlideAt = function() { /* ... */ };
}
                    

Loss of the this context

Arrow functions have lexical context

function AutoSlidingGallery($slides) {
    var slideIndex = 0;
    
    setInterval( () => {
        this.switchToSlideAt(slideIndex);
        slideIndex = (slideIndex + 1) % $slides.length;
    }, 2000);
    
    this.switchToSlideAt = function() { /* ... */ };
}
                    

In arrow functions, this always points to the this of the outer scope.

Only functions scope, no block scope

function init(slideElems) {
    var numberOfSlides = 3;
    for (var i = 0; i < numberOfSlides; i++) {
        var index = i;
        slideElems[index].addEventListener("click", function() {
            switchToSlideAt(index);
        });
    }
    
    function switchToSlideAt(slideIndex) {
        // ...
    }
}
                    

Bad! Clicking on any slide will call switchToSlideAt(3) because 3 is the last value of the index variable.

There's only 1 copy of index per call to init().

Only functions scope, no block scope

let has block scope

function init(slideElems) {
    var numberOfSlides = 3;
    for (var i = 0; i < numberOfSlides; i++) {
        let index = i;
        slideElems[index].addEventListener("click", function() {
            switchToSlideAt(index);
        });
    }
    // index is not visible here
    
    function switchToSlideAt(slideIndex) {
        // ...
    }
}
                    

Good! There's one copy of the index variable per block (per each turn of the for loop).

ES6 let: Temporal dead zone

var declarations are hoisted.

function init() {
    console.log(foo); //Logs: undefined
    var foo = 42;
    console.log(foo); //Logs: 42
}
                        

let-declared variables cannot be used in their block before the declaration .

function init() {
    //console.log(foo); //ReferenceError: foo is uninitialized
    let foo = 42;
    console.log(foo); //Logs: 42
}
                        

ES6 let: Temporal dead zone

If there's a let variable in a block, you can only use it in the block after its declaration, even if there's another such variable in the outer scope.

function init() {
    let foo = 123;
    if (true) {
        // consle.log(foo); //ReferenceError: foo is uninitialized
        let foo = 456;
        console.log(foo); //Logs: 456
    }
    console.log(foo); //Logs: 123
}
                    

Lack of string methods

How to check whether a string starts/ends with a substring? … or whether it contains a substring? Repeat a string N times?
// 1
"shop.example.com".indexOf("shop") === 0;

var haystack = "foo.bar.example.com";
var needle = "example.com"
haystack.slice(-needle.length) === needle;


// 2
" item message error ".indexOf(" message ") >= 0
~" item message error ".indexOf(" message ") // returns a truthy/falsy value

// 3
(new Array(4)).join("la") // lalala
                    

Lack of string methods

extra string methods

"shop.example.com".startsWith("shop.")        // true

"foo.bar.example.com".endsWith("example.com") // true

" item message error ".contains(" message ")  // true

"la".repeat(3)                                // "lalala"


// Some unicode / UTF-16 methods, mostly unimplemented yet:
"abc".normalize();
"∑".codePointAt(0);
String.fromCodePoint(0x2F804);                // A japanese character
                    

Hard time printing strings

var slideIndex = 0;
var slideCount = 5;
$slide.append(
    "<div>" +
        "<p>Slide " + (slideIndex + 1) + " of " + slideCount + "</p>" +
    "</div>"
);
                    

Hard time printing strings

template strings with interpolation

var slideIndex = 0;
var slideCount = 5;
$slide.append(`
    <div>
        <p>Slide ${slideIndex + 1} of ${slideCount}</p>
    </div>
`);
                    

Yes, they are multiline.

No dynamic keys in object literals

function createClient(name, isOrganization) {
    var nameKey = isOrganization ? "orgName" : "fullName";

    return {
        nameKey: name,
        isOrganization: isOrganization
    };
}
var astronaut = createClient("Neil Armstrong", false); 
var agency = createClient("NASA", true);
                    

Property keys are just static!

console.log(astronaut);
// { nameKey: "Neil Armstrong", isOrganization: false }

console.log(agency);
// { nameKey: "NASA", isOrganization: true }
                        

No dynamic keys in object literals

function createClient(name, isOrganization) {
    var nameKey = isOrganization ? "orgName" : "fullName";

    var client = {
            isOrganization: isOrganization
        };
    client[nameKey] = name;
    return client;
}
var astronaut = createClient("Neil Armstrong", false); 
var agency = createClient("NASA", true);
                    

Works, but is a bit annoying.

console.log(astronaut);
// { fullName: "Neil Armstrong", isOrganization: false }

console.log(agency);
// { orgName: "NASA", isOrganization: true }
                        

No dynamic keys in object literals

bracket notation in object literals

function createClient(name, isOrganization) {
    var nameKey = isOrganization ? "orgName" : "fullName";

    return {
        [nameKey]: name,
        isOrganization: isOrganization
    };
}
var astronaut = createClient("Neil Armstrong", false); 
var agency = createClient("NASA", true);
                    
console.log(astronaut);
// { fullName: "Neil Armstrong", isOrganization: false }

console.log(agency);
// { orgName: "NASA", isOrganization: true }
                    

Lack of support for declaring object shape

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

Person.prototype.introduce = function() {
    console.log("Hi, I'm " + this.name + "!");
};


function Superhero(name, tagline, powers) {
    Person.call(this, name);
    this.tagline = tagline;
    this.powers = powers;
}
Superhero.prototype = Object.create(Person.prototype);
Superhero.prototype.constructor = Superhero;

Superhero.prototype.hasManyPowers = function() {
    return this.powers.length >= 2;
};

Superhero.prototype.introduce = function() {
    Person.prototype.introduce.call(this);
    console.log(this.tagline);
};
                    
                    
var superman = new Superhero(
        "Clark Kent",
        "Up, up and away!",
        ["flight", "heat vision"]
);
superman.introduce();
/* Logs:
Hi, I'm Clark Kent!
Up, up and away! */
                    
                    

Lack of support for declaring object shape

Classes

class Person {
    constructor(name) {
        this.name = name;
    }
    introduce() {
        console.log("Hi, I'm " + this.name + "!");
    }
}

class Superhero extends Person {
    constructor(name, tagline, powers) {
        super( name);
        this.tagline = tagline;
        this.powers = powers;        
    }
    hasManyPowers() {
        this.powers.length >= 2;
    }
    introduce() {
        super.introduce();
        console.log(this.tagline);
    }
}
                    
                    
var superman = new Superhero(
        "Clark Kent",
        "Up, up and away!",
        ["flight", "heat vision"]
);
superman.introduce();
/* Logs:
Hi, I'm Clark Kent!
Up, up and away! */
                    
                    

No Maps or other generic containers

Sample: storing data related to a DOM Node, out of the DOM.

function createSafeScopeManager() { // for something like Angular's $el.scope()
    var scopes = {};
    return {
        setScope: function(elem, scope) {
            scopes[elem] = scope;
        },
        getScope: function(elem) {
            return scopes[elem];
        },
        scopes: scopes // published for debugging purposes
    }
}

var sm = createSafeScopeManager();
var elemOne = document.getElementById("one");
var elemTwo = document.getElementById("two");

sm.setScope(elemOne, { name: "scopeOne" });
sm.setScope(elemTwo, { name: "scopeTwo" });

console.log( sm.getScope(elemOne) );
console.log( sm.getScope(elemTwo) );
                        
                        
// { name: "scopeTwo" } // { name: "scopeTwo" } console.log( Object.keys(sm.scopes) ); // ["[object HTMLSpanElement]"]

Object keys are always converted to strings.

No Maps and other generic containers

Map, Set, WeakMap…

function createSafeScopeManager() {
    var scopes = new Map();
    return {
        setScope: function(elem, scope) {
            scopes.set(elem, scope);
        },
        getScope: function(elem) {
            return scopes.get(elem);
        }
    }
}

var sm = createSafeScopeManager();
var elemOne = document.getElementById("one");
var elemTwo = document.getElementById("two");

sm.setScope(elemOne, { name: "scopeOne" });
sm.setScope(elemTwo, { name: "scopeTwo" });

console.log( sm.getScope(elemOne) );
console.log( sm.getScope(elemTwo) );
                    
                        
                        
// { name: "scopeOne" } // { name: "scopeTwo" }

Difference between Maps and WeakMaps

  • Maps allow you to iterate over all entries (e.g. through for-of)
  • WeakMaps don't give you any API for grabbing all entries…
  • …so if you want to grab a value, you need to know its key.

 

So what?

  • So the JS engine can be sure that if there's no references to the key, it can safely garbage-collect the value (optimization!).

Prototypes and private state don't mix

(function(global) {
    var ADULT_AGE = 18; // private, but common to all instances

    function Person(name) {
        this._name = name;
    }

    Person.prototype.introduce = function() {
        console.log("Hi, I'm " + this._name + "!");
    };
    
    global.Person = Person;
})(window);
                    
                    
  • _name is not really private
  • We could make it private if we dropped prototypes in favor of closures

Prototypes and private state don't mix

Symbols

(function(global) {
    var nameKey = Symbol();
    
    function Person(name) {
        this[nameKey] = name;
    }

    Person.prototype.introduce = function() {
        console.log("Hi, I'm " + this[nameKey] + "!");
    };
    
    global.Person = Person;
})(window);
                    
                    
  • Each Symbol is guaranteed to be unique
  • Symbols are skipped by: for-in, for-of, Object.keys()
  • Symbols are still listed by (ES6): Reflect.ownKeys() and Object.getOwnPropertySymbols()

    …so not fully private – accessible through reflection (like Java)

Prototypes and private state don't mix

WeakMaps

(function(global) {
    var NAMES = new WeakMap();
    
    function Person(name) {
        NAMES.set(this, name);
    }

    Person.prototype.introduce = function() {
        console.log("Hi, I'm " + NAMES.get(this) + "!");
    };
    
    global.Person = Person;
})(window);
                    
                    

The NAMES map stores all names, each stored for a particular Person.

This time, really private, as no one else has access to NAMES.

'arguments' sux

function fixture(collectionName/*, items*/) {
    var items = [].slice.call(arguments, 1);
    items.forEach(function() {
        db.insert(collectionName, items);
    });
}
                    
  • Another magic, implicit identifier
  • The parameters are not listed in the function header
  • Array.isArray(arguments) === false
  • Setting arguments[0] would mutate collectionName
  • No support to grab "all parameters after the n-th one" (like here)

'arguments' sux

Rest parameters

function fixture(collectionName, ...items) {
    items.forEach(function() {
        db.insert(collectionName, items);
    });
}
                    
  • We pick any parameter name we want
  • Array.isArray(items) === true

No native import/require

Only universal way: global variables, possibly namespaced.

// myHelper.js
var MYAPP = MYAPP || {};
MYAPP.myHelper = {
    help: function() {
        // ...
    },
    meaningOfLife: 42
}

// myComponent.js
var MYAPP = MYAPP || {};
MYAPP.MyComponent = function() {
    MYAPP.myHelper.help();
    // ...
};
                    

No native import/require

AMD (mostly client-side)

// myHelper.js
define("myHelper", function() {
    return {
        help: function() {
            // ...
        },
        meaningOfLife: 42
    };
});

// myComponent.js
define("myComponent", ["meHelper.js"], function(myHelper) {
    function MyComponent() {
        myHelper.help();
        // ...
    }
    
    return MyComponent;
});                    
                    

No native import/require

CommonJS (mostly server-side)

// myHelper.js
exports.help = function() {
    // ...
};
exports.meaningOfLife = 42;

// myComponent.js
var myHelper = require("myHelper.js");
exports = MyComponent;

function MyComponent() {
    myHelper.help();
    // ...
}
                    

No native import/require

Modules

// myHelper.js
export function help() {
    // ...
}
export const meaningOfLife = 42;

// myComponent.js
import * as myHelper from "myHelper.js";

export default function MyComponent() {
    myHelper.help();
    // ...
}
                    

Or:

// myComponent.js
import { help, meaningOfLife } from "myHelper.js";

export default function MyComponent() {
    help();
    // ...
}
                    

Repeating 'use strict' all over again

// Some non-strict lib, like jQuery
(function() {
    // ...
})();

// Our strict component 1
(function galleryModule() {
    "use strict";
    // ...
})();

// Our strict component 2
(function carouselModule() {
    "use strict";
    // ...
})();

                    

Repeating 'use strict' all over again

Modules & classes

Module and class bodies are automatically in strict mode.

\o/

Other awesome stuff in ES6

  • Proxies
  • Iterators / Generators
  • Destructuring
  • Splats
  • More array methods
  • Typed arrays

 

And for ES7 (and later)

  • Object.observe()
  • Async functions
  • Value objects and operator overloading
  • SIMD

The end

Thank you!

Keep Icebergin'! Bartek Swierczynski / @bswierczynski