On Github bswierczynski / future-js-fixing-the-annoying-parts
by Bartek Swierczynski / @bswierczynski
function init() { var foo = 100; bar = 42; } init(); console.log(bar); //Logs: 42
Forgetting var declares a global variable.
function init() { "use strict"; var foo = 100; bar = 42; } init(); console.log(bar); //ReferenceError: bar is not defined
Forgetting var throws in Strict Mode.
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!
And I mean it! Don't!
var arr = ["foo", "bar", "baz"]; arr.forEach(function(value, i) { console.log("#" + i + ": " + value); });
Still much slower than a for loop.
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); }
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.
Number.isNaN(NaN); // true Number.isNaN(123); // false Number.isNaN("123"); // false Number.isNaN("abc"); // false
Returns true only for NaN.
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 }) })
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 ) })
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.
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() { /* ... */ }; }
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.
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().
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).
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 }
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 }
// 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
"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
var slideIndex = 0; var slideCount = 5; $slide.append( "<div>" + "<p>Slide " + (slideIndex + 1) + " of " + slideCount + "</p>" + "</div>" );
var slideIndex = 0; var slideCount = 5; $slide.append(` <div> <p>Slide ${slideIndex + 1} of ${slideCount}</p> </div> `);
Yes, they are multiline.
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 }
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 }
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 }
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! */
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! */
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) );
Object keys are always converted to strings.
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) );
So what?
(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);
(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);
…so not fully private – accessible through reflection (like Java)
(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.
function fixture(collectionName/*, items*/) { var items = [].slice.call(arguments, 1); items.forEach(function() { db.insert(collectionName, items); }); }
function fixture(collectionName, ...items) { items.forEach(function() { db.insert(collectionName, items); }); }
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(); // ... };
// myHelper.js define("myHelper", function() { return { help: function() { // ... }, meaningOfLife: 42 }; }); // myComponent.js define("myComponent", ["meHelper.js"], function(myHelper) { function MyComponent() { myHelper.help(); // ... } return MyComponent; });
// myHelper.js exports.help = function() { // ... }; exports.meaningOfLife = 42; // myComponent.js var myHelper = require("myHelper.js"); exports = MyComponent; function MyComponent() { myHelper.help(); // ... }
// 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(); // ... }
// Some non-strict lib, like jQuery (function() { // ... })(); // Our strict component 1 (function galleryModule() { "use strict"; // ... })(); // Our strict component 2 (function carouselModule() { "use strict"; // ... })();
Module and class bodies are automatically in strict mode.
\o/
Keep Icebergin'! Bartek Swierczynski / @bswierczynski