Learning from jQuery
- Chapter 1, Event Handling
- Chapter 2, Constructors and Prototypes
- Chapter 3, DOM Traversal and Manipulation
- Chapter 4, AJAX
- Chapter 5, JavaScript Conventions
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
- IE does not support
addEventListener
(until IE9) - only supports bubbling events, you can't refer to the element using
this
; you have to use eithere.target
ore.srcElement
- it doesn't support
e.preventDefault
, have to sete.returnValue
tofalse
instead.
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
- cycle through the elements and add the event handler to each one
- 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:
- 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. - 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:
- the 3rd argument of
req.open
should always betrue
, it tells you want to make the request asynchronous. if set tofalse
, it a synchronous call, it causes a memory leak in Firefox, and Mozilla disabled it; attempting to send will throw an error. It was also bad practice to use it when it did work. - it is safe to use
.onreadystatechange
instead of adding an event listener req.readyState
will always be set to 4, eventually. There is no need to have asetTimeout
to throw an error on timeout.- when sending GET requests, you should just append any data you want to send to the end of the URL.
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)