CHAPTER 4

image

Understanding the Runtime

The difference between a bad programmer and a good programmer is understanding. That is, bad programmers don’t understand what they are doing and good programmers do.

—Max Kanat-Alexander

Once your TypeScript program has been compiled to plain JavaScript, you can run it anywhere. JavaScript happily runs in a browser or on a server; you just have to bear in mind that the available features differ depending on where the code runs. This chapter explains some of the differences you will encounter between browser and server runtimes and also explains some important concepts that are common to all runtimes, such as the event loop, scope and events.

Runtime Features

Even an aged browser will give you access to the Document Object Model (DOM), mouse and keyboard events, forms, and navigation. A modern browser will add offline storage, an indexed database, HTTP requests, geolocation, and suite of application program interfaces (APIs) for device sensors such as light and proximity. JavaScript isn’t just the most common language in web browsers; it has been running on servers since the early nineties. JavaScript’s prominence as a server-side language has really gained traction with NodeJS, which is a server technology built on the V8 JavaScript engine. Running on the server gives you access to databases, the file system, cryptography, domain name resolution, streams, and countless other modules and utilities. Figure 4-1 illustrates how JavaScript as a language is made powerful by the APIs supplied by browsers or servers.

9781430267911_Fig04-01.jpg

Figure 4-1. JavaScript features in browser vs. server environments

Unless you explicitly use an API that allows thread creation, such as web workers or a child process, the statements in your program will be queued to execute on a single thread. Running a program on a single thread removes many of the headaches that would be caused by multiple threads trying to manipulate the same data, but it does mean you need to bear in mind that your code could be queued. A long running event handler can block other events from firing in a timely manner and the order in which the queue gets executed can vary in subtle ways. The queue is usually processed in first-in first-out order, but different runtime environments may revisit the queue at different times. For example, one environment may return to the queue only when a function has been completed, but another may revisit the queue if the first function calls out to a second function. In the latter case, another statement may be executed before the second function is called. Despite the alarming nature of these potential differences, it is rare to find that they cause any problems in practice.

As well as processing the queue containing all of the events, the runtime may have other tasks to perform that need to be processed on the same thread; for example a browser may need to redraw the screen. If you have a function that takes too long to run, you could affect the redraw speed of the browser. To allow a browser to draw 60 frames per second, you would need to keep the execution of any function to less than 17 ms. Keeping functions fast is very easy in practice, except where you deal with an API with blocking calls, such as localStorage, or if you execute a long running loop.

One of the most common side effects of the single-threaded approach at runtime is that intervals and timers may appear to take longer than the specified time to execute. This is because they have to wait in the queue to be executed. Listing 4-1 provides an example to test this behavior. Calling the test function sets up the 50-ms timer and measures how long it takes to fire. Running this test a few times will show you that you get a result in the range of 50 to 52 ms, which is what you’d expect.

Listing 4-1. Queued timer

function test() {
    var testStart = performance.now();

    window.setTimeout(function () {
        console.log(performance.now() - testStart);
    }, 50);
}

test();

To simulate a long running process, a loop that runs for 100 ms has been added to the test function in Listing 4-2. The loop starts after the timer is set up, but because nothing is de-queued until the original test function is completed, the timer executes much later than before. The times logged in this example are typically in the range of 118 to 130 ms.

Listing 4-2. Queued timer, delayed, waiting for the test method to finish

function test() {
    var testStart = performance.now();

    window.setTimeout(function () {
        console.log(performance.now() - testStart);
    }, 50);

    // Simulated long running process
    var start = +new Date();
    while (true) {
        if (+new Date() - start > 100) {
            break;
        }
    }
}

test();

Image Note  The performance.now high resolution timer is not yet supported everywhere, but this method of measuring execution times is more accurate than using the Date object. Dates are based on the system clock, which is synchronized as often as every 15 minutes. If synchronization occurs while you are timing an operation, it will affect your result. The performance.now value comes from a high resolution timer that can measure submillisecond intervals, starts at 0 when the page begins to load, and isn’t adjusted during synchronization.

Scope

The term scope refers to the set of available identifiers that can be resolved in a given context. In most C-like languages, identifiers are block scoped, meaning they are available within the same set of curly-braces that they are defined in. Variables declared within a set of curly braces are not available outside of those braces, but statements within the curly braces can access variables declared outside of the braces. Listing 4-3 shows this general C-like scoping in action.

This is not currently the case in JavaScript (and therefore TypeScript). If the code in Listing 4-3 was executed in a JavaScript runtime, the value logged in both statements would be Outer: 2, rather than Outer: 1. This is because scope is defined by functions, rather than curly braces.

Listing 4-3. C-like scope

var scope = 1;

{
    var scope = 2;

    // Inner: 2
    console.log('Inner: ' + scope);
}

// Outer: 1
console.log('Outer: ' + scope);

To provide the same restricted scope to the inner variable and logging statement in JavaScript, the curly braces would need to be replaced by a function definition, as shown in Listing 4-4.

Listing 4-4. Functional scope

var scope = 1;

(function () {
    var scope = 2;

    // Inner: 2
    console.log('Inner: ' + scope);
}());

// Outer: 1
console.log('Outer: ' + scope);

As ECMAScript 6 gains traction, block-level scope will be made available using the let keyword. The let keyword can be used anywhere you previously used the var keyword, but variables declared with let are block scoped rather than function scoped.

Listing 4-5 shows an example where a variable is wrapped in curly braces, but because variables have function scope it can be accessed from outside of the block.

Listing 4-5. Using the var keyword

{
    var name = 'Scope Example';
    console.log('A: ' + name);
}

// 'B: Scope Example'
console.log('B: ' + name);

Using the let keyword as shown in Listing 4-6 restricts the scope of the name variable to the block, and the attempt to access the value of the variable outside of the braces fails.

Listing 4-6. Using the let keyword

{
    let name = 'Scope Example';
    console.log('A: ' + name);
}

// 'B: undefined'
console.log('B: ' + name);

Block-level scoping applies wherever a set of curly braces is used, for example, when writing loops, if-statements, switch-statements, or even just a pair of curly braces without an expression as shown in these examples.

As with all new features in the ECMAScript 6 proposal, browser support is currently limited. TypeScript currently does not allow the use of the let keyword unless the compiler is targeting ES6 mode (which is currently planned, but not available). The compiler flags are described in detail in Appendix 2.

Variable Hoisting

When you declare a variable with the var keyword, the declaration is hoisted to the top of the function it is declared in. This variable hoisting is usually irrelevant to your program, but there are subtle scenarios that will result in strange behavior.

It is important to declare local variables with the var keyword; otherwise the variable contributes to the global scope, rather than the local scope. The TypeScript compiler will warn you if you accidentally omit the var keyword, as long as there isn’t already a global variable with the same name. If there is already a global variable with the same name, the compiler will assume you intended to re-use the global variable.

Listing 4-7 shows one such example. The global type variable is declared first. The use of the type variable at the top of the function looks like a reference to the global variable because the local variable has not yet been declared. Because of variable hoisting, the local variable declaration is moved to the top of the function at runtime, hiding the global variable of the same name. The assignment remains in the original location. This results in an undefined value being logged, rather than the global 'Ring Tailed Lemur' value or the local 'Ruffed Lemur' value. The local type variable is declared, but not assigned when console.log is called.

Listing 4-7. Variable hoisting

var type = 'Ring Tailed Lemur';

function Lemur() {
    console.log(type);
    var type = 'Ruffed Lemur';
}

Lemur();

Variable hoisting also applies to the let keyword, although the declaration in this case is hoisted to the top of the block scope rather than to the top of the function scope. This keeps the behavior consistent between block-scoped variables and function-scoped variables.

Image Note  In your program, the best way to avoid confusion is to avoid adding to the global scope wherever possible. The absence of global variables means the TypeScript compiler can warn you about usage of variables before they are declared and accidental omission of the var or let keywords.

Callbacks

Almost all modern JavaScript APIs, including the new browser APIs that supply access to readings from device sensors, avoid blocking the main thread by accepting a callback. A callback is simply a function that you pass as an argument, which is called when an operation has completed.

To illustrate the benefits of callbacks, Figure 4-2 shows the program flow while waiting for a blocking sensor to respond to a request. Because the request is blocking the thread for the duration of the request, no other statements can be executed. Blocking the event queue for more than a few milliseconds is undesirable and must be avoided for long operations. Operations involving calls to the file system, hardware devices, or calls across a network connection all have the potential to block your program for unacceptable lengths of time.

9781430267911_Fig04-02.jpg

Figure 4-2. Blocking call

Callbacks are very useful for avoiding these blocking requests. Figure 4-3 shows how this pattern is used to avoid blocking the main thread during a long-running process. When the request is made, a function is passed along with the request. The main thread is then able to process the event queue as normal. When the long-running process ends, the callback function is then called, being passed any relevant arguments. The callback is placed on the event queue and is executed in turn.

9781430267911_Fig04-03.jpg

Figure 4-3. Using a callback

Although callbacks are commonly used to avoid blocking the program during a long-running process, you can freely pass a function as an argument anywhere in your program. Listing 4-8 demonstrates this. The go function accepts a function argument. The callback parameter has a type annotation that restricts the functions that can be passed to only those that accept a string argument. The callbackFunction satisfies this type annotation.

Listing 4-8. Passing a function as an argument

function go(callback: (arg: string) => void) {
    callback.call(this, 'Example Argument'),
}

function callbackFunction(arg: string) {
    alert(arg);
}

go(callbackFunction);

In the body of the go function, the callback is executed using the call method, which is available on all functions in JavaScript.

There are three ways to execute the callback from within the go function. In Listing 4-8, the call method was used. When you use call, you must supply a variable that will be used to set the context of the this keyword within the callback. You can follow the context argument with any number of additional arguments; these will be passed into the callback. You can also use the apply method, which is almost identical to call, except you pass the arguments as an array, as shown in Listing 4-9.

Listing 4-9. Using apply

function go(callback: (arg: string) => void) {
    callback.apply(this, ['Example Argument']);
}

The third method of executing a callback is to simply append the parameter name with parentheses as demonstrated in Listing 4-10. This technique doesn’t allow the context to be set, but you can pass arguments in the usual way by placing them within the parentheses.

Listing 4-10. Simple function call

function go(callback: (arg: string) => void) {
    callback('Example Argument');
}

There is an additional use for the apply method outside of the context of callbacks. Because it accepts an array containing arguments, you can use apply to extract arguments from an array. This is demonstrated in Listing 4-11. To find the maximum number in the numbers array you would either write a loop to test each one or pass each value individually into the Math.max function using each index. Using the apply method means you can simply pass the numbers array and have the apply method convert the array into the argument list. Because you aren’t using apply to modify the scope, you can pass null as the first argument.

Listing 4-11. Using apply to convert array to arguments

var numbers = [3, 11, 5, 7, 2];

// A fragile way of finding the maximum number // var max = Math.max(numbers[0], numbers[1], numbers[2], numbers[3], numbers[4]);

// A solid way to find the maximum
var max = Math.max.apply(null, numbers);

// 11
console.log(max);

The pattern of using callbacks is one example of functions passed as arguments. The next section describes how powerful this language feature is and how it can be used in other ways.

Passing Functions as Arguments

Functions are first-class citizens in JavaScript, which means they can be passed as arguments, returned from another function as a return value, assigned to variables, and stored as properties on an object. Passing a function as an argument is the mechanism used to provide callbacks.

You can use the ability to pass functions as arguments to create a simple implementation of the observer pattern, storing a collection of subscribers and publishing an event to them all from a single class. This simple observer design is shown in Listing 4-12. Any number of subscribers can be added, and when the publisher receives a message, it distributes it to all of the subscribers.

Listing 4-12. Simple observer

interface Subscriber {
    () : any;
}

class Publisher {
    private subscribers: Subscriber[] = [];

    addSubscriber(subscriber: Subscriber) {
        this.subscribers.push(subscriber);
    }

    notify(message: string) {
        for (var i = 0; i < this.subscribers.length; i++) {
            this.subscribers[i].apply(message);
        }
    }
}

var publisher = new Publisher();

publisher.addSubscriber(function () {
    console.log('A: ' + this);
});

publisher.addSubscriber(function () {
    console.log('B: ' + this);
});

// A: Test message
// B: Test message
publisher.notify('Test message'),

Image Note  When you pass a function as an argument, you must omit the parentheses; for example, go(callbackFunction) rather than go(callbackFunction()), otherwise the function is executed and the return value is passed as the argument.

First-class functions are one of the most powerful features in any language. You can create higher-order functions that accept functions as arguments and return functions as results; this allows greater flexibility as well as granular code reusability in your program.

Events

Events are a fundamental concept in the JavaScript runtime, so they are of great interest to any TypeScript programmer. Event listeners are commonly attached to user-initiated events such as touch, click, keypress, and other interactions on a web page, but events can also be used as a mechanism for decoupling code that needs to trigger processing and the code that undertakes the work.

Events are handled across two distinct phases—capturing and bubbling. During capturing, the event is sent to the topmost elements in the document hierarchy first and then to more deeply nested elements. During bubbling it is sent to the target element first and then to its ancestors. The phase is supplied as a property of the event argument and can be accessed using e.eventPhase where your event argument is named e.

At the risk of overstating the point about running in an event loop on a single thread, it is worth remembering that multiple event listeners attached to the same event will execute sequentially, not in parallel, and a long-running listener may delay the execution of the subsequent listeners attached to the same event. When an event is triggered, each event listener is queued in the same order it is attached, if the first listener takes 2 s to run, the second listener is blocked for at least 2 s and will only execute once it reaches the top of the event queue.

Listing 4-13. Event listeners

class ClickLogger {
    constructor () {
        document.addEventListener('click', this.eventListener);
    }

    eventListener(e: Event) {
        // 3 (Bubbling Phase)
        var phase = e.eventPhase;

        var tag = (<HTMLElement>e.target).tagName;

        console.log('Click event in phase ' + phase +
            ' detected on element ' + tag + ' by ClickLogger.'),
    }
}

var clickLogger = new ClickLogger();

Listing 4-13 shows a class that attaches one of its methods, eventListener, to the click event on the document. When used in conjunction with the HTML page in Listing 4-14, this ClickLogger class will output messages based on the element clicked, for example

  • Click event detected on element DIV by ClickLogger.
  • Click event detected on element P by ClickLogger.
  • Click event detected on element BLOCKQUOTE by ClickLogger.
  • Click event detected on element FOOTER by ClickLogger.

Listing 4-14. Example document

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Event Demo</title>
</head>
<body>
    <div>
        Clicking on different parts of this document logs appropriate messages.
        <blockquote>
            <p>Any fool can write code that a computer can understand.
               Good programmers write code that humans can understand.</p>
            <footer>
                -Martin Fowler
            </footer>
        </blockquote>
    </div>
</body>
</html>

Image Note  The correct way to add an event listener is the addEventListener call. Versions of Internet Explorer prior to version 9 use an alternative attachEvent method. You can target both of these methods of attaching an event using the custom addEventCrossBrowser function shown in Listing 4-15. An improved version of this function appears in Chapter 5.

Listing 4-15. Cross-browser events

function addEventCrossBrowser(element, eventName, listener) {
    if (element.addEventListener) {
        element.addEventListener(eventName, listener, false);
    } else if (element.attachEvent) {
        element.attachEvent('on' + eventName, listener);
    }
}

class ClickLogger {
    constructor () {
        addEventCrossBrowser(document, 'click', this.eventListener);
    }

    eventListener(e: Event) {
        // 3 (Bubbling Phase)
        var phase = e.eventPhase;

        var tag = (<HTMLElement>e.target).tagName;

        console.log('Click event detected on element ' + tag + ' by ClickLogger.'),
    }
}

var clickLogger = new ClickLogger();

You are not limited to the finite list of supported events in a given runtime; you can listen for, and dispatch, your own custom events.

TypeScript’s Custom-Event Mechanism

Listing 4-16 shows the custom-event mechanism. In some environments, it is as simple as using addEventListener and dispatchEvent. You can pass custom data as part of the event to use in the listener.

Listing 4-16. Custom events

// Polyfill for CustomEvent:
// https://developer.mozilla.org/en/docs/Web/API/CustomEvent
(function () {
    function CustomEvent(event, params) {
        params = params || { bubbles: false, cancelable: false, detail: undefined };
        var evt = <any>document.createEvent('CustomEvent'),
        evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
        return evt;
    };

    CustomEvent.prototype = (<any>window).Event.prototype;

    (<any>window).CustomEvent = CustomEvent;
})();

// Fix for lib.d.ts
interface StandardEvent {
    new (name: string, obj: {}): CustomEvent;
}
var StandardEvent = <StandardEvent><any> CustomEvent;

// Code for custom events is below:

enum EventType {
    MyCustomEvent
}

class Trigger {
    static customEvent(name: string, detail: {}) {
        var event = new StandardEvent(name, detail);

        document.dispatchEvent(event);
    }
}

class ListeningClass {
    constructor() {
        document.addEventListener(
            EventType[EventType.MyCustomEvent],
            this.eventListener,
            false);
    }

    eventListener(e: Event) {
        console.log(EventType[EventType.MyCustomEvent] + ' detected by ClickLogger.'),
        console.log('Information passed: ' + (<any>e).detail.example);
    }
}

var customLogger = new ListeningClass();

Trigger.customEvent(
    EventType[EventType.MyCustomEvent],
    {
        "detail": {
            "example": "Example Value"
        }
    }
);

You can choose to use events, or code events, such as the simple observer from the earlier example in Listing 4-12, to distribute work throughout your program.

Event Phases

An event is dispatched to an event target along a propagation path that flows from the root of the document to the target element. Each progression along the path from the root to the target element is part of the capture phase of the event and the phase will be 1. Then the event reaches the event target, and the phase changes to the target phase, which is phase 2. Finally, the event flows in the reverse direction from the event target back to the root in the bubbling phase, which is phase 3.

These event phases are shown in Figure 4-4. The footer element in the figure is not part of the hierarchy between the root and the event target, so it is not included in the propagation path.

9781430267911_Fig04-04.jpg

Figure 4-4. Event phases

Events provide a powerful mechanism for decoupling the code in your program. If you trigger events rather than directly call code to perform an action, it is a simple task to divide the action into small event listeners with a single responsibility. It is also a trivial matter to add additional listeners later.

Extending Objects

Almost everything in JavaScript is an object that consists of a set of properties. Each property is a key-value pair with a string key and value of any type, including primitive types, objects, and functions. If the value is a function, it is commonly called a method. Whenever you create a class in TypeScript, it is represented using JavaScript objects, but there are also many built-in objects that you can use.

The native objects all remain open, which means you can extend them as easily as you can your own objects. You need to take care when extending a native object for the following reasons:

  • If everyone extended native objects, the chances are the extensions would overwrite each other or combine in incompatible ways.
  • It is possible that the native object definition may later clash with yours and your implementation will hide the native implementation.

So although it is possible to extend native objects, in general it is only recommended as a technique to be used as a polyfill, which is a way of adding current features to older runtimes. Although you may decide to live by a less restrictive rule, it is worth writing extensions to native objects in the style of a polyfill so you can at least detect when one of the following happens:

  • Native functionality is added with a name that clashes with your extension.
  • Another programmer adds another extension with the same name.
  • A third-party library or framework adds an extension with the same name.

The third item in particular suggests you shouldn’t write native object extensions if you are shipping your program to be used as a library by other programmers. If library authors routinely extended native objects, there would be a high chance of a clash and the winner would simply be the last extension to load, as it would overwrite all previous ones.

Image Note  The term polyfill (named after a wall smoothing and crack filling cement called Polyfilla) was coined by Remy Sharp (Remy Sharp’s Blog, http://remysharp.com/2010/10/08/what-is-a-polyfill/, 2010) as a term to describe a technique used to add missing native behavior in a way that defers to the native implementation when it becomes available. For example, you would attempt to detect the feature inside a browser and only add to it if it was missing.

Extending the Prototype

In Listing 4-17, the native NodeList, which contains a list of HTML elements, has been extended to add an each method that executes a callback function for each element in the list. The extension is added to the NodeList.prototype, which means it will be available on all NodeList instances. Calling document.querySelectorAll returns a NodeList of matching elements and now the each method can be used to display the contents of each element using the getParagraphText function. The use of the each method means the for loop can be defined in just a single place.

Listing 4-17. Extending objects in JavaScript

NodeList.prototype.each = function (callback) {
    for (var i = 0; i < this.length; i++) {
        callback.call(this[i]);
    }
};

var getParagraphText = function () {
    console.log(this.innerHTML);
};

var paragraphs = document.querySelectorAll('p'),
paragraphs.each(getParagraphText);

Rather than passing each element into the callback function as an argument, the call method is used to bind the element to the function’s context, which means the getParagraphText function can use the this keyword to refer to the element.

When you add this code to a TypeScript program, errors will be generated to warn you that the each method doesn’t exist on the NodeList interface. You can remove these errors and get intelligent autocompletion by adding to the interface in your program, as shown in Listing 4-18. The added benefit is that if the native object is updated in a way that clashes with your extension, the TypeScript compiler will warn you about the duplicate declaration.

Listing 4-18. Extending objects in TypeScript

interface NodeList {
        each(callback: () => any): void;
}

NodeList.prototype.each = function (callback) {
    for (var i = 0; i < this.length; i++) {
        callback.call(this[i]);
    }
};

var getParagraphText = function () {
    console.log(this.innerHTML);
};

var paragraphs = document.querySelectorAll('p'),
paragraphs.each(getParagraphText);

In this example, the this keyword within the each method has no type because it can’t be inferred. This can be improved as shown in Listing 4-19. By moving the elements from the contextual this keyword into a parameter, the autocompletion and type checking in your program is improved. This also means the function can be re-used more easily.

Listing 4-19. Improved TypeScript object extensions

interface NodeList {
        each(callback: (element: HTMLElement) => any): void;
}

NodeList.prototype.each = function (callback) {
    for (var i = 0; i < this.length; i++) {
        callback.call(null, this[i]);
    }
};

var getParagraphText = function (element: HTMLElement) {
    console.log(element.innerHTML);
};

var paragraphs = document.querySelectorAll('p'),
paragraphs.each(getParagraphText);

To make this solution more like a polyfill, the code should check for the existence of the each method before adding it. This is how you would add an interim feature that is planned but not yet available on your target runtime. You can see this in action in Listing 4-20.

Listing 4-20. Turning an extension into a polyfill

if (!NodeList.prototype.each) {
    NodeList.prototype.each = function (callback) {
        for (var i = 0; i < this.length; i++) {
            callback.call(null, this[i]);
        }
    };
}

Extending objects via the prototype is a technique that can be used on any object in TypeScript, even your own—although it is a convoluted way to add behavior to objects that are under your control. You may be tempted to use the technique to extend a library that you consume, as it would allow you to upgrade the library without losing your additions.

Sealing Objects

If you are concerned about your code being extended, you can prevent extensions being made to your instances by using Object.seal. Listing 4-21 shows a typical extension that someone else may make to your code, and Listing 4-22 shows how to prevent it. Object.seal prevents new properties from being added and marks all existing properties as nonconfigurable. It is still possible to modify the values of the existing properties.

Listing 4-21. Extended instance

class Lemur {
    constructor(public name: string) {

    }
}

var lemur = new Lemur('Sloth Lemur'),

// new property
lemur.isExtinct = true;

// true
console.log(lemur.isExtinct);

Listing 4-22. Sealing an instance

class Lemur {
    constructor(public name: string) {

    }
}

var lemur = new Lemur('Sloth Lemur'),

Object.seal(lemur);

// new property
lemur.isExtinct = true;

// undefined
console.log(lemur.isExtinct);

You can check whether an object is sealed using the Object.isSealed method, passing in the object you want to check. There are a series of similar operations that may be useful—each could be used in Listing 4-21 in place of the Object.seal call to get the results described in the following example.

  • Object.preventExtensions/Object.isExtensible is a more permissive version of Object.seal, allowing the properties to be deleted and to be added to the prototype.
  • Object.freeze/Object.isFrozen is a more restrictive alternative to Object.seal that prevents properties from being added or removed and also prevents values being changed.

There is an excellent overview of creating, extending, and sealing JavaScript objects in Expert JavaScript by Mark Daggett (Apress, 2013).

Alternatives to Extending

It would be somewhat irresponsible to advise against extending native objects without presenting an alternative solution to the problem. This section shows an example of the classList property that is available on HTML elements in modern web browsers. The polyfill is shown, and then an alternative solution is supplied that uses a façade to marshal the call between either the native classList or the substitute version.

Listing 4-23 shows a call to retrieve the list of classes from an element that will fail in old browsers. The classList API actually supplies options to add, remove, and toggle classes—but for this example just the retrieval of the array of class names is shown.

Listing 4-23. Using the native classList

var elem = document.getElementById('example'),

console.log(elem.classList);

One common solution to the potential absence of this feature is to use a polyfill. Listing 4-24 shows a simple polyfill that tests for the presence of the classList API and then adds it to the HTMLElement or Element prototype. The replacement function splits the string of class names to create an array, or returns an empty array if there are no class names.

Listing 4-24. Polyfill

if (typeof document !== "undefined" && !("classList" in document.documentElement)) {
    var elementPrototype = (HTMLElement || Element).prototype;
    if (elementPrototype) {
        Object.defineProperty(elementPrototype, 'classList',{
            get : function () {
                var list = this.className ? this.className.split(/s+/) : [];
                console.log('Polyfill: ' + list);
            }
        });
    }
}

var elem = document.getElementById('example'),

console.log(elem.classList);

Although using a polyfill is the right solution in this particular case (due to its close match to the native behavior and safety check that ensures it doesn’t overwrite the native implementation if it is present), it is worth looking at the alternative design too. In many cases, the solution in Listing 4-25 is a more stable option as it won’t ever clash with native or library code. The downside to this approach is that the calling code must be changed to reference the façade.

Listing 4-25. Façade

class Elements {
    static getClassList(elem) {
        if ('classList' in elem) {
            return elem.classList;
        }
        return elem.className ? elem.className.split(/s+/) : [];
    }
}

var elem = document.getElementById('example'),

console.log(Elements.getClassList(elem));

The façade option has one major benefit in addition to being better isolated than the polyfill. The intent of this code is clear. When it comes to maintaining your code, the less cluttered and more straightforward method in the Elements class trumps the polyfill every time. Clean and maintainable code is always preferable to a clever but complex solution.

Summary

The JavaScript runtime is well known for its quirks and surprises, but on the whole the TypeScript compiler will shield you from most of the common faux pas. Keeping the global scope clear can help the compiler to help you, so it is worth using the structural features of TypeScript such as modules and classes to enclose functions and variables.

Most of your code will execute on a single thread, and the callback pattern helps to avoid blocking this thread during long-running operations. Keeping functions short not only makes your program easier to maintain, it can also make your program more responsive as each time a function is called it is added to the back of the event queue and the runtime has an opportunity to process the oldest entry on the queue.

You can listen to native events and create custom events, or you can use the observer pattern to dispatch and listen to custom events in your program.

You can extend objects, including native objects, but it is often more appropriate to use mediating code to marshal the call to avoid clashing with other libraries or future extensions to native code. You can prevent extensions on your own objects by sealing, freezing, or preventing extensions.

Key Points

  • JavaScript is functionally scoped, with block-level variables added in ECMAScript 6.
  • Callbacks can help to avoid blocking the main thread.
  • Events can prevent tight coupling, whether using native events or your own publisher.
  • You can extend all JavaScript objects and almost everything in JavaScript is an object.
  • You can seal or freeze objects to prevent further changes.
  • You can polyfill missing behavior to make new features available on old platforms.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.109.34