Jim Cheung

Learning from jQuery

Chapter 1, Event Handling

Events in JavaScript

var foo = document.getElementById('foo');
foo.addEventListener('click', function (e) {
    this.style.color = 'red';
    e.preventDefault();
});

in jQuery, you can also return false to prevent the default action, but it also stops the event from propagating, which is generally undesired.

Events in IE8

IE8 code:

var foo = document.getElementById('foo');
foo.attachEvent('onclick', function (e) {
    // Either:
    foo.style.color = 'red';
    // Or:
    ((e.target) ? e.target : e.srcElement).style.color = 'red';
    e.returnValue = false;
});

Writting A Wrapper Function

function addEventListener(element, event, handler) {
    if (element.addEventListener) {
        element.addEventListener(event, function(e) {
            if (handler.call(this, e) === false) {
                e.preventDefault();
            }
        });
    } else if (element.attachEvent) {
        element.attachEvent('on' + event, function(e) {
            if (handler.call(element, e) === false) {
                e.returnValue = false;
            }
        });
    }
}
// usage
var foo = document.getElementById('foo');
addEventListener(foo, 'click', function(e) {
    this.style.color = 'red';
    return false;
});

Adding Event Handler to Multiple Elements

there are two ways to do this

  1. cycle through the elements and add the event handler to each one
  2. add the event handler to a common parent of the elements, and wait for it to bubble up

the 2nd is generally preferred because it uses fewer resources, but if there are only a few elements, it can be overkill. The 1st method is more commonly used.

jQuery does both methods automatically

// first method
$('.bar').click(callback);
// second method
$(document).on('click', '.bar', callback);

JavaScript does not do this automatically. call .addEventListener or .attachEvent on a list of elements will throw an error because it isn't defined; call previously defined addEventListener won't do anything as it won't be able to find.

in order to do it, have to loop them

var bars = document.getElementsByClassName('bar');
for (var i = 0; i < bars.length; i++) {
    addEventListener(bars[i], 'click', callback);
}

document.getElementsByClassName returns a NodeList, not an array. one main difference between the two is that NodeLists update live.

Event Propagation

when an event is fired on an element, it isn't just fired for the specific element, it is also fired for all parent elements of that element. this can be useful for setting an event listener on multiple elements at the same time without having to loop through them one by one:

document.addEventListener('click', function(e) {
    var element = e.srcElement;
    if (element.tagName === 'A') {
        var url = getAnchorURL(element);
        if (isEvil(url)) {
            e.preventDefault();
            // inform user they clicked an "evil" link
        }
    }
});

jQuery code

$(document).on('click', 'a', function() {
    var element = e.srcElement,
        url = getAnchorURL(element);
    if (isEvil(url)) {
        e.preventDefault()
        
        // inform user they clicked an "evil" link
    }
});

the action of events being fired on the parent elements is called event propagation. the order in which they are fired is called the event order.

there are 2 possible event orders: bubbling and capturing

when an event bubbles

self -> parent -> ... -> body -> html

when an event captures

html -> body -> ... -> parent -> self

addEventListener has a 3rd parameter that allows you to specify the order: true or unspecified for bubbling, false for capturing. attachEvent doesn't support capturing, so IE8 or below only support bubbling events.

so example above should use capturing event listener:

document.addEventListener('click', function(e) {
    var element = e.srcElement;
    if (element.tagName === 'A') {
        var url = getAnchorURL(element);
        if (isEvil(url)) {
            e.preventDefault();
            e.stopPropagation();
            // inform user they clicked an "evil" link
        }
    }
}, false);

so which are fired first, bubbling or captured event listener? WC3 specifics that events should capture down from the document, and then bubble back up again:

html -> body -> parent -> self (capturing) -> self (bubbling) -> parent -> body -> html

for IE8, can use .cancelBubble to cancel propagation, create a fake stopPropagation method:

e.stopPropagation = function () {
    e.cancelBubble = true;
}

Triggering Events

then trigger event in jQuery with .trigger, it actually cycles all events set by .on (or .click etc) and calls them. so it will only trigger event handlers set by jQuery, any handlers set by addEventListener will be ignored.

for javascript, create event with document.createEvent and dispatch with dispatchEvent:

var element = document.getElementById('foo');
var event = document.createEvent('UIEvents');
event.initUIEvent('click', true, true, window, 1);
var returned = element.dispatchEvent(event);

you can decide whether to prevent default action by the returned value.

if you need to store more information to event, just modify the event object:

var event = document.createEvent('UIEvents');
event.initUIEvent('click', true, true, window, 1);
event.x = 100;
event.y = 50;
var returned = element.dispatchEvent(event);

for IE8, use document.createEventObject and .fireEvent

var element = document.getElementById('foo');
var event = document.createEventObject();
event.button = 1;
element.fireEvent('onclick', event);

Removing Event Handlers

in jQuery, just use the .off method

function clickHandler(){};
$('#foo').click(clickHandler);
// either:
$('#foo').off('click', clickHandler);
// or:
$('#foo').off('click');

the 1st removes only the handler specified as the second argument. the 2nd removes all click handlers. calling .off with no arguments would remove all event handlers of every type.

and for the 2nd argument, the exact function must be passed

// does not work
$('#foo').click(function(){});
$('#foo').off('click', function(){});

to remove event in javascript, use .removeEventListener, it doesn't work in IE8

var element = document.getElementById('foo');
function clickHandler(){};

addEventListener(foo, 'click', clickHandler);
foo.removeEventListener('click', clickHandler);

the 2nd argument is neglected, an error will be thrown. if we want to remove all events, must do it by loop

in IE8, use .detachEvent

// same as above
// ... 
foo.detachEvent('onclick', clickHandler);

Updated Wrappers

var listeners = [];
function addEventListener(element, event, handler) {
    if (element.addEventListener) {
        element.addEventListener(event, handler);
    } else if (element.attachEvent) {
        var newHandler = function(e) {
            e.preventDefault = function() {
                e.returnValue = false;
            };
            e.stopPropagation = function(e) {
                e.cancelBubble = true;
            };
            handler.call(element, e);
        };
        element.attachEvent('on' + event, newHandler);
        listeners.push([handler, newHandler]);
    }
}
function removeEventListener(element, event, handler) {
    if (element.removeEventListener) {
        element.removeEventListener(event, handler);
    } else if (element.detachEvent) {
        event = 'on' + event;
        for (var i = 0; i < listeners.length; i++) {
            if (listeners[i][0] === handler) {
                element.detachEvent(event, listeners[i][1]);
                break;
            }
        }
    }
}

Adding a "Once Only" Event Listener

jQuery's .one method adds an event listener that will only be called once. to do this in javascript, store a copy of the function in an object and then call removeEventListener in the event handler itself.

Chapter 2, Constructors and Prototypes

Constructors

Constructors are functions that are called by the new keyword

function Greeting(item) {
    this.log = function() {
        console.log('Hello' + item);
    };
}
var hello = new Greeting('world');
hello.log(); // Hello world
// or:
new Greeting('alien').log(); // Hello alien

Begin the name of your constructors with capital letter, this will help you remember whether it is a function or a constructor;

Method Chaining

just use return this;

Constructor, Not Function

calling constructor directly may result in an error or unexpected behavior, you can put following code at the top of the function:

if (!(this instanceof Foo)) {
    return new Foo(bar);
}

Prototypes

Prototypes allow the developer to create a method or property of an object that is inherited by all instances of that object.

function User(id) {
    this.id = id;
    this.sendMsg = function (msg) {
        // action
    };
    this.getMsgs = function () {
        // action
    };
}

if we had hundreds, thousands of the User object, every object whould have a copy of those two functions, which would use quite a bit of memory.

rewrite using prototype

function User(id) {
    this.id = id;
}
User.prototype.sendMsg = function (msg) {
    // action
};
User.prototype.getMsgs = function () {
    // action
};

it would use a lot less memory, and it also have the advantage changes can apply to exiting instances

function User(id) {
    this.id = id;
}
var bob = new User(1);
console.log(bob.foo); // undefined
User.prototype.foo = 'bar';
console.log(bob.foo); // bar

when a method is called, it checks

self -> prototype of the object -> prototype of the prototype -> ...

in jQuery, we assign methods by either jQuery.fn.extend (cycles through the given object, adding methods to the prototype) or directly to jQuery.fn

jQuery.fn.log = function() {
    console.log(this);
    return this;
};
$('p').log();

jQuery.fn is actually jQuery.prototype, jquery developers think it looks nicer:

jQuery.fn = jQuery.prototype; 

Chapter 3, DOM Traversal and Manipulation

Selecting an Element

in jQuery:

$('#foo');

in javascript:

document.getElementById('foo'); // return element or null
document.getElementsByClassName('bar'); // return a NodeList
document.getElementsByTagName('p'); // return a NodeList

A NodeList is an object that acts like an array, but it isn't. you can access elements using elements[i], but you need to use Array prototype for methods like .splice and .forEach

var elements = document.getElementsByClassName('bar');
Array.prototype.slice.call(elements, 2, 5);j

.slice return an array of elements, not a NodeList.

Selecting Elements with a CSS Selector

it is possible to select elements using CSS selector via .querySelector and .querySelectorAll:

document.querySelector('#foo');
document.querySelectorAll('.bar');
document.querySelectorAll('.bar span, #foo');
document.querySelectorAll('a[href*="danger"]'); // CSS3 selector, does not supported in IE

to alias $ as document.querySelector and $$ as document.querySelectorAll:

if (!document.querySelector || !document.querySelectorAll) {
    // load library here, such as the Sizzle library
}
function $(selector) {
    return document.querySelector
        ? document.querySelector(selector)
        : customQuerySelector(selector);
}
function $$(selector) {
    return document.querySelectorAll
        ? document.querySelectorAll(selector)
        : customQuerySelectorAll(selector);
}

Selecting Children

in jQuery:

$('#foo').children('.bar');

in javascript:

// work:
document.getElementById('foo').getElementsByClassName('.bar');

// does not work:
document.getElementsByTagName('p').getElementsByClassName('test');
// this works:
var elements = document.getElementsByTagName('p'), 
    allElements = [];
Array.prototype.forEach.call(elements, function(element) {
        children = element.getElementsByClassName('test');
        allElements = allElements.concat(Array.prototype.slice.call(children));
});
// this also works:
document.querySelectorAll('p .test');

Selecting the Next Element

in jQuery:

$('#foo').next('.bar');

in javascript:

var element = document.getElementById('foo');
element = element.nextElementSibling;
while (element && element.className.indexOf('bar') === -1) {
    element = element.nextElementSibling;
}

to find the previous elemenet, use .previousElementSibling, to find parent, use .parentElement

to return all of an elment's siblings (like jQuery's .siblings):

var element = document.getElementById('foo');
var elements = element.parentNode.childNodes;
for (var siblings = [], i = 0; i < elements.length; i++) {
    if (elements[i].nodeType === 1 && elements[i] !== element) {
        siblings.push(elements[i]);
    }
}

Creating an Element

in jQuery:

$('<strong class="test">text</strong>").appendTo('body');

there are two ways to create elements in javascript:

  1. modify .innerHTML of the element, but this should usually be avoided as it converts the element in to HTML, addes new HTML, and parses it back again. this means any event listeners and formatting added on the fly will be lost.
  2. the correct way, use document.createElement and .appendChild

javascript:

// the wrong way, using innerHTML:
document.body.innerHTML += '<strong class="test">text</strong>';
// the correct way
var newElement = document.createElement('strong');
newElement.setAttribute('class', 'test');
newElement.innerHTML = 'text';
document.body.appendChild(newElement);
// alternative of using .innerHTML, use createTextNode
var text = document.createTextNode('text');
newElement.appendChild(text);

Modifying an Existing Element

in jQuery:

$('#foo').text($('#foo').text().replace(/swear/g, '*****'));

but that only works on elements with no children

in javascript:

var element = document.getElementById('foo');
element.innerHTML = element.innerHTML.replace('swear', '*****');

if the element has a lot of children:

var TEXT_NODE = 3, 
    ELEMENT_NODE = 1;
function replace(element, find, replacement) {
    var child, i, value,
        children = element.childNodes;
    for (i = 0; i < children.length; i++) {
        child = children[i];
        value = child.nodeValue;
        if (child.nodeType === TEXT_NODE) {
            child.nodeValue = value.replace(find, replacement);
        } else if (child.nodeType === ELEMENT_NODE) {
            replace(child, find, replacement);
        }
    }
}
var element = document.getElementById('foo');
replace(element, /swear/g, '*****');

Cycling Through Elements

in jQuery:

$('.bar').each(function() {
    $(this).css('color', 'red');
});

in javascript:

var elements = document.getElementsByClassName('bar');
for(var i = 0; i < elements.length; i++) {
    elements[i].style.color = 'red';
}

if you want to refer to the element as this, you can use an anonymous function with .call to set the scope:

var elements = document.getElementsByClassName('bar');
for(var i = 0; i < elements.length; i++) {
    (function() {
        this.style.color = 'red';
    }).call(elements[i]);
} 

it would be a lot cleaner when elements[i] is heavily used.

Moving and Coping Elements

in jQuery:

$('#foo').insertBefore($('#bar')); // move #foo to directly before #bar
$('#foo').clone().insertAfter($('#bar')); // copies #foo to after #bar

in javascript:

// move element
var foo = document.getElementById('foo');
var bar = document.getElementById('bar');
bar.parentNode.insertBefore(foo, bar);
// explain:
// element.parentNode.insertBefore(elementToInsert, insertBeforeThis);
// copy element
var foo = document.getElementById('foo').cloneNode(true);
var bar = document.getElementById('bar');
bar.parentNode.insertBefore(foo, bar);

the argument being passed to .cloneNode should almost always be true, it tells javascript that you want to clone the entire tree, as well as the element itself. setting to false or unspecified would just copy the children without cloning them, so making any changes to the children of new element would also change the chidren of the old element (and vice versa).

Chapter 4, AJAX

Sending an AJAX Request

in jQuery:

$.get('/ajax/?foo=bar', function (data) {
    console.log(data);
});

in javascript:

if (window.XMLHttpRequest()) {
    var req = new XMLHttpRequest();
} else {
    // IE
    var req = new ActiveXObject('Microsoft.XMLHTTP');
}
var url = '/ajax/?foo=bar';
req.open('GET', url, true);
req.onreadystatechange = function() {
    if (req.readyState === 4 && req.status === 200) {
        console.log(req.responseText);
    } else if (req.readyState === 4) {
        throw new Error('XHR Request failed: ' + req.status);
    }
};
req.send();

few things to know about:

Sending POST Request in JavaScript

almost same as GET example, but with a couple of minor differences:

// ...
var url = '/ajax/';
var data = 'foo=bar';
req.open('POST', url, true);
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
// ... 
req.send(data);

Writing a Wrapper Function

function request(method, url, data, callback) {
    if (window.XMLHttpRequest) {
        var req = new XMLHttpRequest();
    } else {
        var req = new ActiveXObject('Microsoft.XMLHTTP');
    }
    if (method === 'GET' && typeof data === 'string') {
        url += '?' + data;
    }
    req.open(method, url, true);
    if (method === 'POST' && typeof data === 'string') {
        req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    }
    req.onreadystatechange = function() {
        if (req.readyState === 4 && req.status === 200) {
            var contentType = req.getResponseHeader('Content-type');
            if (contentType === 'application/json') {
                callback(JSON.parse(req.responseText));
            } else {
                callback(req.responseText);
            }
        } else if (req.readyState === 4) {
            throw new Error("XHR Request failed: " + req.status);
        }
    };
    req.send((typeof data === 'string' && method === 'POST') ? data : null);
    return req;
}

you can call it using :

request('GET', '/ajax', 'foo=bar', function (data) {
    console.log(body);
});
        

you can also create alias functions, just like jQuery:

function get(url, data, callback) {
    return request('GET', url, data, callback);
}
function post(url, data, callback) {
    return request('POST', url, data, callback);
}

A Simple Application of AJAX

to check request is a ajax call:

if ($_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest')

(it's a very simple app, read the book)

Chapter 5, JavaScript Conventions

Optimizations

Caching Variables

// cache the length in for loop
for (var i = 0, l = ary.length; i < l; i++ {
    // do something
}
// when using jQuery, cache element
var foo = $('#foo');
foo.click(function () {
    $.myfunc(foo, function () {
        foo.show();
    });
    foo.hide();
});

parseInt

parseInt contains additional functionality that you may not need, use new Number() is faster

parseInt('35'); // 35
new Number('35'); // faster
parseInt('21.8'); // 21
new Number('21.8'); // 21.8
parseInt('3 dogs'); // 3
new Number('3 dogs'); // NaN
// new Number() returns object, to convert it to number
typeof new Number('3'); // object
typeof +new Number('3'); // number

Loops

cycle throught the loop backward is faster

for (var i = array.length; i--;) {
    // do something
}
var i = 100;
while(i--) {
    // do something
}

Functions

declarations versus expressions:

// function declaration
function test() {
    // ...
}
// anonymous function expression
var test = function () {
    // ...
};
// named function expression
var test = function test () {
    // ...
};

function declaration are hoisted (placed at the beginning of the scope) and can be called before they're actually defiend

// works:
test();
function test() {
    // ...
}

but function will be hoisted even if the block of code will never be run:

test(); // false
if (true) {
    function test() {
        return true;
    }
} else {
    console.log('test'); // will never be run
    function test () {
        return false;
    }
}

that doesn't happen with anonymous or named function expressions.

(something more, but not important)

(end)