- Our Goal
- Goal: To be able to understand this function:
// The .bind method from Prototype.js
Function.prototype.bind = function(){
var fn = this, args = [].slice.call(arguments), object = args.shift();
return function(){
return fn.apply(object,
args.concat([].slice.call(arguments)));
};
};
- Some helper methods that we have:
assert( true, "I'll pass." );
assert( "truey", "So will I." );
assert( false, "I'll fail." );
assert( null, "So will I." );
log( "Just a simple log", "of", "values.", true );
error( "I'm an error!" );
- Context
- What exactly does context represent?
function katana(){
this.isSharp = true;
}
katana();
assert( isSharp === true,
"A global object now exists with that name and value." );
var shuriken = {
toss: function(){
this.isSharp = true;
}
};
shuriken.toss();
assert( shuriken.isSharp === true,
"When it's an object property, the value is set within the object." );
- How can we change the context of a function?
var object = {};
function fn(){
return this;
}
assert( fn() == this, "The context is the global object." );
assert( fn.call(object) == object,
"The context is changed to a specific object." );
- Different ways of changing the context:
function add(a, b){
return a + b;
}
assert( add.call(this, 1, 2) == 3,
".call() takes individual arguments" );
assert( add.apply(this, [1, 2]) == 3,
".apply() takes an array of arguments" );
- QUIZ: How can we implement looping with a callback?
function loop(array, fn){
for ( var i = 0; i < array.length; i++ ) {
// Implement me!
}
}
var num = 0;
loop([0, 1, 2], function(value, i){
assert(value == num++,
"Make sure the contents are as we expect it.");
assert(this instanceof Array,
"The context should be the full array.");
});
- A possible solution for function looping:
function loop(array, fn){
for ( var i = 0; i < array.length; i++ )
fn.call( array, array[i], i );
}
var num = 0;
loop([0, 1, 2], function(value, i){
assert(value == num++,
"Make sure the contents are as we expect it.");
assert(this instanceof Array,
"The context should be the full array.");
});
- Instantiation
- What does the new operator do?
function Ninja(){
this.name = "Ninja";
}
var ninjaA = Ninja();
assert( !ninjaA, "Is undefined, not an instance of Ninja." );
var ninjaB = new Ninja();
assert( ninjaB.name == "Ninja",
"Property exists on the ninja instance." );
- We have a 'this' context that is a Ninja object.
function Ninja(){
this.swung = false;
// Should return true
this.swingSword = function(){
this.swung = !this.swung;
return this.swung;
};
}
var ninja = new Ninja();
assert( ninja.swingSword(), "Calling the instance method." );
assert( ninja.swung, "The ninja has swung the sword." );
var ninjaB = new Ninja();
assert( !ninjaB.swung,
"Make sure that the ninja has not swung his sword." );
- QUIZ: Add a method that gives a name to the ninja.
function Ninja(name){
// Implement!
}
var ninja = new Ninja("John");
assert( ninja.name == "John",
"The name has been set on initialization" );
ninja.changeName("Bob");
assert( ninja.name == "Bob",
"The name was successfully changed." );
- Add a new property and method to the object.
function Ninja(name){
this.changeName = function(name){
this.name = name;
};
this.changeName( name );
}
var ninja = new Ninja("John");
assert( ninja.name == "John",
"The name has been set on initialization" );
ninja.changeName("Bob");
assert( ninja.name == "Bob",
"The name was successfully changed." );
- What happens when we forget to use the new operator?
function User(first, last){
this.name = first + " " + last;
}
var user = User("John", "Resig");
assert( typeof user == "undefined",
"Since new wasn't used, the instance is undefined." );
- What happens when we forget to use the new operator? (cont.)
function User(first, last){
this.name = first + " " + last;
}
window.name = "Resig";
var user = User("John", name);
assert( name == "John Resig",
"The name variable is accidentally overridden." );
- We need to make sure that the new operator is always used.
function User(first, last){
if ( !(this instanceof User) )
return new User(first, last);
this.name = first + " " + last;
}
var name = "Resig";
var user = User("John", name);
assert( user, "This was defined correctly, even if it was by mistake." );
assert( name == "Resig", "The right name was maintained." );
- QUIZ: Is there another, more generic, way of doing this?
function User(first, last){
if ( !(this instanceof ___) )
return new User(first, last);
this.name = first + " " + last;
}
var name = "Resig";
var user = User("John", name);
assert( user, "This was defined correctly, even if it was by mistake." );
assert( name == "Resig", "The right name was maintained." );
- A solution using arguments.callee.
function User(first, last){
if ( !(this instanceof arguments.callee) )
return new User(first, last);
this.name = first + " " + last;
}
var name = "Resig";
var user = User("John", name);
assert( user, "This was defined correctly, even if it was by mistake." );
assert( name == "Resig", "The right name was maintained." );
- Flexible Arguments
- Using a variable number of arguments to our advantage.
function merge(root){
for ( var i = 0; i < arguments.length; i++ )
for ( var key in arguments[i] )
root[key] = arguments[i][key];
return root;
}
var merged = merge({name: "John"}, {city: "Boston"});
assert( merged.name == "John", "The original name is intact." );
assert( merged.city == "Boston", "And the city has been copied over." );
- How can we find the Min/Max number in an array?
function smallest(array){
return Math.min.apply( Math, array );
}
function largest(array){
return Math.max.apply( Math, array );
}
assert(smallest([0, 1, 2, 3]) == 0, "Locate the smallest value.");
assert(largest([0, 1, 2, 3]) == 3, "Locate the largest value.");
- Another possible solution:
function smallest(){
return Math.min.apply( Math, arguments );
}
function largest(){
return Math.max.apply( Math, arguments );
}
assert(smallest(0, 1, 2, 3) == 0, "Locate the smallest value.");
assert(largest(0, 1, 2, 3) == 3, "Locate the largest value.");
- Uh oh, what's going wrong here?
function highest(){
return arguments.slice(1).sort(function(a,b){
return b - a;
});
}
assert(highest(1, 1, 2, 3)[0] == 3, "Get the highest value.");
assert(highest(3, 1, 2, 3, 4, 5)[1] == 4, "Verify the results.");
- QUIZ: We must convert array-like objects into actual arrays. Can any built-in methods help?
// Hint: Arrays have .slice and .splice methods which return new arrays.
function highest(){
return makeArray(arguments).slice(1).sort(function(a,b){
return b - a;
});
}
function makeArray(array){
// Implement me!
}
// Expecting: [3,2,1,1]
assert(highest(1, 1, 2, 3)[0] == 3, "Get the highest value.");
// Expecting: [5,4,3,3,2,1]
assert(highest(3, 1, 2, 3, 4, 5)[1] == 4, "Verify the results.");
- We can use built-in methods to our advantage.
function highest(){
return makeArray(arguments).sort(function(a,b){
return b - a;
});
}
function makeArray(array){
return [].slice.call( array );
}
assert(highest(1, 1, 2, 3)[0] == 3, "Get the highest value.");
assert(highest(3, 1, 2, 3, 4, 5)[1] == 4, "Verify the results.");
- QUIZ: Implement a multiplication function (first argument by largest number).
function multiMax(multi){
// Make an array of all but the first argument
var allButFirst = ___;
// Find the largest number in that array of arguments
var largestAllButFirst = ___;
// Return the multiplied result
return multi * largestAllButFirst;
}
assert( multiMax(3, 1, 2, 3) == 9, "3*3=9 (First arg, by largest.)" );
- We can use call and apply to build a solution.
function multiMax(multi){
// Make an array of all but the first argument
var allButFirst = [].slice.call( arguments, 1 );
// Find the largest number in that array of arguments
var largestAllButFirst = Math.max.apply( Math, allButFirst );
// Return the multiplied result
return multi * largestAllButFirst;
}
assert( multiMax(3, 1, 2, 3) == 9, "3*3=9 (First arg, by largest.)" );
- Closures
- A basic closure.
var num = 10;
function addNum(myNum){
return num + myNum;
}
assert( addNum(5) == 15,
"Add two numbers together, one from a closure." );
- But why doesn't this work?
var num = 10;
function addNum(myNum){
return num + myNum;
}
num = 15;
assert( addNum(5) == 15,
"Add two numbers together, one from a closure." );
- Closures are frequently used for callbacks.
var results = jQuery("#results").html("<li>Loading...</li>");
jQuery.get("test.html", function(html){
results.html( html );
assert( results, "The element to append to, via a closure." );
});
- They're also useful for timers.
var count = 0;
var timer = setInterval(function(){
if ( count < 5 ) {
log( "Timer call: ", count );
count++;
} else {
assert( count == 5, "Count came via a closure, accessed each step." );
assert( timer, "The timer reference is also via a closure." );
clearInterval( timer );
}
}, 100);
- and they're also frequently used when attaching event listeners.
var count = 1;
var elem = document.createElement("li");
elem.innerHTML = "Click me!";
elem.onclick = function(){
log( "Click #", count++ );
};
document.getElementById("results").appendChild( elem );
assert( elem.parentNode, "Clickable element appended." );
- Private properties, using closures.
function Ninja(){
var slices = 0;
this.getSlices = function(){
return slices;
};
this.slice = function(){
slices++;
};
}
var ninja = new Ninja();
ninja.slice();
assert( ninja.getSlices() == 1,
"We're able to access the internal slice data." );
assert( ninja.slices === undefined,
"And the private data is inaccessible to us." );
- QUIZ: What are the values of the variables?
var a = 5;
function runMe(a){
assert( a == ___, "Check the value of a." );
function innerRun(){
assert( b == ___, "Check the value of b." );
assert( c == ___, "Check the value of c." );
}
var b = 7;
innerRun();
var c = 8;
}
runMe(6);
for ( var d = 0; d < 3; d++ ) {
setTimeout(function(){
assert( d == ___, "Check the value of d." );
}, 100);
}
- The last one is quite tricky, we'll revisit it.
var a = 5;
function runMe(a){
assert( a == 6, "Check the value of a." );
function innerRun(){
assert( b == 7, "Check the value of b." );
assert( c == undefined, "Check the value of c." );
}
var b = 7;
innerRun();
var c = 8;
}
runMe(6);
for ( var d = 0; d < 3; d++ ) {
setTimeout(function(){
assert( d == 3, "Check the value of d." );
}, 100);
}
- Temporary Scope
- Self-executing, temporary, function
(function(){
var count = 0;
var timer = setInterval(function(){
if ( count < 5 ) {
log( "Timer call: ", count );
count++;
} else {
assert( count == 5, "Count came via a closure, accessed each step." );
assert( timer, "The timer reference is also via a closure." );
clearInterval( timer );
}
}, 100);
})();
assert( typeof count == "undefined",
"count doesn't exist outside the wrapper" );
assert( typeof timer == "undefined", "neither does timer" );
- Now we can handle closures and looping.
for ( var d = 0; d < 3; d++ ) (function(d){
setTimeout(function(){
log( "Value of d: ", d );
assert( d == d, "Check the value of d." );
}, d * 200);
})(d);
- The anonymous wrapper functions are also useful for wrapping libraries.
(function(){
var myLib = window.myLib = function(){
// Initialize
};
// ...
})();
- Another way to wrap a library:
var myLib = (function(){
function myLib(){
// Initialize
}
// ...
return myLib;
})();
- QUIZ: Fix the broken closures in this loop!
var count = 0;
for ( var i = 0; i < 4; i++ ) {
setTimeout(function(){
assert( i == count++, "Check the value of i." );
}, i * 200);
}
- A quick wrapper function will do the trick.
var count = 0;
for ( var i = 0; i < 4; i++ ) (function(i){
setTimeout(function(){
assert( i == count++, "Check the value of i." );
}, i * 200);
})(i);
- Built-in Prototypes
- We can also modify built-in object prototypes.
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(fn){
for ( var i = 0; i < this.length; i++ ) {
fn( this[i], i, this );
}
};
}
["a", "b", "c"].forEach(function(value, index, array){
assert( value,
"Is in position " + index + " out of " + (array.length - 1) );
});
- Beware: Extending prototypes can be dangerous.
Object.prototype.keys = function(){
var keys = [];
for ( var i in this )
keys.push( i );
return keys;
};
var obj = { a: 1, b: 2, c: 3 };
assert( obj.keys().length == 3, "We should only have 3 properties." );
delete Object.prototype.keys;
- Enforcing Function Context
- What happens when we try to bind an object's method to a click handler?
var Button = {
click: function(){
this.clicked = true;
}
};
var elem = document.createElement("li");
elem.innerHTML = "Click me!";
elem.onclick = Button.click;
document.getElementById("results").appendChild(elem);
elem.onclick();
assert( elem.clicked,
"The clicked property was accidentally set on the element" );
- We need to keep its context as the original object.
function bind(context, name){
return function(){
return context[name].apply(context, arguments);
};
}
var Button = {
click: function(){
this.clicked = true;
}
};
var elem = document.createElement("li");
elem.innerHTML = "Click me!";
elem.onclick = bind(Button, "click");
document.getElementById("results").appendChild(elem);
elem.onclick();
assert( Button.clicked,
"The clicked property was correctly set on the object" );
- Add a method to all functions to allow context enforcement.
Function.prototype.bind = function(object){
var fn = this;
return function(){
return fn.apply(object, arguments);
};
};
var Button = {
click: function(){
this.clicked = true;
}
};
var elem = document.createElement("li");
elem.innerHTML = "Click me!";
elem.onclick = Button.click.bind(Button);
document.getElementById("results").appendChild(elem);
elem.onclick();
assert( Button.clicked,
"The clicked property was correctly set on the object" );
- The .bind method from Prototype.js.
Function.prototype.bind = function(){
var fn = this, args = [].slice.call(arguments), object = args.shift();
return function(){
return fn.apply(object,
args.concat([].slice.call(arguments)));
};
};
var Button = {
click: function(value){
this.clicked = value;
}
};
var elem = document.createElement("li");
elem.innerHTML = "Click me!";
elem.onclick = Button.click.bind(Button, false);
document.getElementById("results").appendChild(elem);
elem.onclick();
assert( Button.clicked === false,
"The clicked property was correctly set on the object" );