lib-present-learn-js-index.htm / htm
<html> <head> <title>Learning Advanced JavaScript</title> <style> body { background: #222; color: #FFF; font-family: Helvetica, Arial; font-size: 16px; } textarea, #pre { font-size: 20px; width: 100%; height: 425px; font-family: Monaco, Courier New; background: #000; color: #FFF; overflow: auto; margin: 0; border: 0; padding: 0 0 0 5px; } #container, #pre { position: absolute; top: 30px; width: 100%; } textarea { z-index: 1; } #pre { z-index: 2; } #area { position: absolute; top: 30px; left: 1%; width: 98%; } h3 { font-size: 24px; } h3.large { position: absolute; top: 45%; left: 0; width: 100%; font-size: 48px; text-align: center; } div.buttons { position: absolute; top: 0px; right: 5px; } input.run { font-weight: bold; } a { color: #EEE; text-decoration: none; border-bottom: 1px solid #EEE; } .FAIL b, .ERROR b { color: red; /* #990066 */ } .PASS b { color: #73C836; } #results { line-height: 1.3em; font-size: 18px; } </style> <link rel="stylesheet" href="lib-present-learn-js-recipes.css"/> <script src="lib-present-learn-js-jquery.js"></script> <script src="lib-present-learn-js-jquery.chili.js"></script> <script src="lib-present-learn-js-recipes.js"></script> <script> jQuery(document).ready(function(){ jQuery("dl").hide(); jQuery("form").submit(function(){ jQuery("#results").empty(); try { (new Function( jQuery("#code").val() ))(); } catch(e){ error(e.message); } for ( var item in window ) { if ( !stasis[item] ) { window[item] = undefined; delete window[item]; } } return false; }); jQuery("#code").keydown(function(e){ if ( this.setSelectionRange ) { var start = this.selectionStart, val = this.value; if ( e.keyCode == 13 ) { var match = val.substring(0, start).match(/(^|\n)([ \t]*)([^\n]*)}; for ( var item in window ) { stasis[item] = true; } var pos; if ( location.hash ) { pos = parseInt(location.hash.substr(1)) - 1; loadSample(); } else { showTOC(); } function showTOC(){ jQuery("#pre").empty(); jQuery("h3").removeClass("large").html("Learning Advanced JavaScript"); jQuery("#pre, #code").height(425).show(); jQuery("dd:empty").prev("dt").each(function(i){ var dt = jQuery("dt").index(this); jQuery("<a href='#" + (dt+1) + "'>" + (i+1) + ") " + this.innerHTML + "\n</a>").click(function(){ pos = dt; loadSample(); return false; }).appendTo("#pre"); }); jQuery("div.buttons").hide(); } function loadSample(){ jQuery("div.buttons").show(); var code = jQuery("#code"); var source = (jQuery("dd").eq(pos).find("pre").html() || "") .replace(/(^|\n) /g, "$1").replace(/ (|\n)/g, "$1"); if ( !source ) { jQuery("h3").addClass("large"); jQuery("#pre, #code, #run").hide(); } else { jQuery("h3").removeClass("large"); jQuery("#pre, #code").show(); if ( source.match(/assert\(|log\(|error\(/) ) jQuery("#run").show(); else jQuery("#run").hide(); } jQuery("h3").html( (source ? "#" + (pos + 1) + ": " : "") + jQuery("dt").eq(pos).html() ); code.val( source.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") ); jQuery("#pre").html( source ).chili(); jQuery("#results").empty(); code.add("#pre").height(275)[0]; if ( code[0].scrollHeight > 275 ) code.add("#pre").height( code[0].scrollHeight + 5 ); var last = jQuery("dt").length - 1; if ( pos == 0 ) jQuery("#prev").css("visibility", "hidden"); if ( pos > 0 ) jQuery("#prev").css("visibility", "visible"); if ( pos == last ) jQuery("#next").css("visibility", "hidden"); if ( pos < last ) jQuery("#next").css("visibility", "visible"); window.location.hash = pos + 1; } setInterval(function(){ if ( location.hash != ("#" + (pos + 1)) ) { var num = parseInt(location.hash.substr(1)) - 1; if ( isNaN(num) ) { if ( jQuery("div.buttons").is(":visible") ) { showTOC(); } } else { pos = num; loadSample(); } } }, 100); }); </script> <script> function assert(pass, msg){ var type = pass ? "PASS" : "FAIL"; jQuery("#results").append("<li class='" + type + "'><b>" + type + "</b> " + msg + "</li>"); } function error(msg){ jQuery("#results").append("<li class='ERROR'><b>ERROR</b> " + msg + "</li>"); } function log(){ var msg = ""; for ( var i = 0; i < arguments.length; i++ ) { msg += " " + arguments[i]; } jQuery("#results").append("<li class='LOG'><b>LOG</b> " + msg + "</li>"); } </script> </head> <body> <h3>Learning Advanced JavaScript</h3> <div id="area"> <pre id="pre" class="javascript"></pre> <form id="form"> <div class="buttons"> <input type="submit" value="Run" class="run" id="run"/> <input type="button" id="prev" value="« Prev"/> <input type="button" id="next" value="Next »"/> </div> <div id="container"> <textarea id="code" wrap="off"></textarea> <ol id="results"></ol> </div> </form> </div> <dl> <dt>Our Goal</dt> <dd></dd> <dt>Goal: To be able to understand this function:</dt> <dd><pre>// The .bind method from Prototype.js Function.prototype.bind = function(){ var fn = this, args = Array.prototype.slice.call(arguments), object = args.shift(); return function(){ return fn.apply(object, args.concat(Array.prototype.slice.call(arguments))); }; }; </pre></dd> <dt>Some helper methods that we have:</dt> <dd><pre>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!" );</pre></dd> <dt>Defining Functions</dt> <dd></dd> <dt>What ways can we define functions?</dt> <dd><pre> function isNimble(){ return true; } var canFly = function(){ return true; }; window.isDeadly = function(){ return true; }; log(isNimble, canFly, isDeadly); </pre></dd> <dt>Does the order of function definition matter?</dt> <dd><pre> var canFly = function(){ return true; }; window.isDeadly = function(){ return true; }; assert( isNimble() && canFly() && isDeadly(), "Still works, even though isNimble is moved." ); function isNimble(){ return true; } </pre></dd> <dt>Where can assignments be accessed?</dt> <dd><pre> assert( typeof canFly == "undefined", "canFly doesn't get that benefit." ); assert( typeof isDeadly == "undefined", "Nor does isDeadly." ); var canFly = function(){ return true; }; window.isDeadly = function(){ return true; }; </pre></dd> <dt>Can functions be defined below return statements?</dt> <dd><pre> function stealthCheck(){ assert( stealth(), "We'll never get below the return, but that's OK!" ); return stealth(); function stealth(){ return true; } } stealthCheck(); </pre></dd> <dt>Named Functions</dt> <dd></dd> <dt>We can refer to a function, within itself, by its name.</dt> <dd><pre> function yell(n){ return n > 0 ? yell(n-1) + "a" : "hiy"; } assert( yell(4) == "hiyaaaa", "Calling the function by itself comes naturally." ); </pre></dd> <dt>What is the name of a function?</dt> <dd><pre> var ninja = function myNinja(){ assert( ninja == myNinja, "This function is named two things - at once!" ); }; ninja(); assert( typeof myNinja == "undefined", "But myNinja isn't defined outside of the function." ); log( ninja );</pre></dd> <dt>We can even do it if we're an anonymous function that's an object property. <dd><pre> var ninja = { yell: function(n){ return n > 0 ? ninja.yell(n-1) + "a" : "hiy"; } }; assert( ninja.yell(4) == "hiyaaaa", "A single object isn't too bad, either." ); </pre></dd> <dt>But what happens when we remove the original object?</dt> <dd><pre> var ninja = { yell: function(n){ return n > 0 ? ninja.yell(n-1) + "a" : "hiy"; } }; assert( ninja.yell(4) == "hiyaaaa", "A single object isn't too bad, either." ); var samurai = { yell: ninja.yell }; var ninja = null; try { samurai.yell(4); } catch(e){ assert( false, "Uh, this isn't good! Where'd ninja.yell go?" ); } </pre></dd> <dt>Let's give the anonymous function a name!</dt> <dd><pre> var ninja = { yell: function yell(n){ return n > 0 ? yell(n-1) + "a" : "hiy"; } }; assert( ninja.yell(4) == "hiyaaaa", "Works as we would expect it to!" ); var samurai = { yell: ninja.yell }; var ninja = {}; assert( samurai.yell(4) == "hiyaaaa", "The method correctly calls itself." ); </pre></dd> <dt>What if we don't want to give the function a name?</dt> <dd><pre> var ninja = { yell: function(n){ return n > 0 ? arguments.callee(n-1) + "a" : "hiy"; } }; assert( ninja.yell(4) == "hiyaaaa", "arguments.callee is the function itself." ); </pre></dd> <dt>Functions as Objects</dt> <dd></dd> <dt>How similar are functions and objects?</dt> <dd><pre> var obj = {}; var fn = function(){}; assert( obj && fn, "Both the object and function exist." ); </pre></dd> <dt>How similar are functions and objects?</dt> <dd><pre> var obj = {}; var fn = function(){}; obj.prop = "some value"; fn.prop = "some value"; assert( obj.prop == fn.prop, "Both are objects, both have the property." ); </pre></dd> <dt>Is it possible to cache the return results from a function?</dt> <dd><pre> function getElements( name ) { var results; if ( getElements.cache[name] ) { results = getElements.cache[name]; } else { results = document.getElementsByTagName(name); getElements.cache[name] = results; } return results; } getElements.cache = {}; log( "Elements found: ", getElements("pre").length ); </pre></dd> <dt>QUIZ: Can you cache the results of this function?</dt> <dd><pre> function isPrime( num ) { var prime = num != 1; // Everything but 1 can be prime for ( var i = 2; i < num; i++ ) { if ( num % i == 0 ) { prime = false; break; } } return prime; } assert( isPrime(5), "Make sure the function works, 5 is prime." ); assert( isPrime.cache[5], "Is the answer cached?" );</pre></dd> <dt>One possible way to cache the results:</dt> <dd><pre> function isPrime( num ) { if ( isPrime.cache[ num ] != null ) return isPrime.cache[ num ]; var prime = num != 1; // Everything but 1 can be prime for ( var i = 2; i < num; i++ ) { if ( num % i == 0 ) { prime = false; break; } } isPrime.cache[ num ] = prime return prime; } isPrime.cache = {}; assert( isPrime(5), "Make sure the function works, 5 is prime." ); assert( isPrime.cache[5], "Make sure the answer is cached." ); </pre></dd> <dt>Context</dt> <dd></dd> <dt>What happens if a function is an object property? <dd><pre> var katana = { isSharp: true, use: function(){ this.isSharp = !this.isSharp; } }; katana.use() assert( !katana.isSharp, "Verify the value of isSharp has been changed." );</pre></dd> <dt>What exactly does context represent?</dt> <dd><pre> 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." ); </pre></dd> <dt>How can we change the context of a function?</dt> <dd><pre> 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." ); </pre></dd> <dt>Different ways of changing the context:</dt> <dd><pre> 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" ); </pre></dd> <dt>QUIZ: How can we implement looping with a callback?</dt> <dd><pre> 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."); }); </pre></dd> <dt>A possible solution for function looping:</dt> <dd><pre> 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."); }); </pre></dd> <dt>Instantiation</dt> <dd></dd> <dt>What does the new operator do?</dt> <dd><pre> 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." ); </pre></dd> <dt>We have a 'this' context that is a Ninja object.</dt> <dd><pre> 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." );</pre></dd> <dt>QUIZ: Add a method that gives a name to the ninja.</dt> <dd><pre> 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." );</pre></dd> <dt>Add a new property and method to the object.</dt> <dd><pre> 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." );</pre></dd> <dt>What happens when we forget to use the new operator?</dt> <dd><pre>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." ); </pre></dd> <dt>What happens when we forget to use the new operator? (cont.)</dt> <dd><pre> 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." ); </pre></dd> <dt>We need to make sure that the new operator is always used.</dt> <dd><pre> 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." ); </pre></dd> <dt>QUIZ: Is there another, more generic, way of doing this?</dt> <dd><pre> 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." ); </pre></dd> <dt>A solution using arguments.callee.</dt> <dd><pre> 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." ); </pre></dd> <dt>Flexible Arguments</dt> <dd></dd> <dt>Using a variable number of arguments to our advantage.</dt> <dd><pre> 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." ); </pre></dd> <dt>How can we find the Min/Max number in an array?</dt> <dd><pre> 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."); </pre></dd> <dt>Another possible solution:</dt> <dd><pre> 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."); </pre></dd> <dt>Uh oh, what's going wrong here?</dt> <dd><pre> function highest(base){ return arguments.sort(function(a,b){ return a - b; }).slice(0, base); } assert(highest(1, 1, 2, 3).length == 1, "Get only the highest value."); assert(highest(3, 1, 2, 3, 4, 5)[2] == 3, "Verify the results."); </pre></dd> <dt>QUIZ: We must convert array-like objects into actual arrays. Can any built-in methods help?</dt> <dd><pre> // Hint: Arrays have .slice and .splice methods which return new arrays. function highest(base){ return makeArray(arguments).sort(function(a,b){ return a - b; }).slice(0, base); } function makeArray(array){ // Implement me! } // Expecting: [3] assert(highest(1, 1, 2, 3).length == 1, "Get only the highest value."); // Expecting: [5,4,3] assert(highest(3, 1, 2, 3, 4, 5)[2] == 3, "Verify the results."); </pre></dd> <dt>We can use built-in methods to our advantage.</dt> <dd><pre> function highest(base){ return makeArray(arguments).sort(function(a,b){ return a - b; }).slice(0, base); } function makeArray(array){ return Array().slice.call( array ); } assert(highest(1, 1, 2, 3).length == 1, "Get only the highest value."); assert(highest(3, 1, 2, 3, 4, 5)[2] == 3, "Verify the results."); </pre></dd> <dt>QUIZ: Implement a multiplication function (first argument by largest number).</dt> <dd><pre> 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.)" ); </pre></dd> <dt>We can use call and apply to build a solution.</dt> <dd><pre> function multiMax(multi){ // Make an array of all but the first argument var allButFirst = Array().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.)" ); </pre></dd> <dt>Closures</dt> <dd></dd> <dt>A basic closure.</dt> <dd><pre> var num = 10; function addNum(myNum){ return num + myNum; } assert( addNum(5) == 15, "Add two numbers together, one from a closure." );</pre></dd> <dt>But why doesn't this work?</dt> <dd><pre> var num = 10; function addNum(myNum){ return num + myNum; } num = 15; assert( addNum(5) == 15, "Add two numbers together, one from a closure." );</pre></dd> <dt>Closures are frequently used for callbacks.</dt> <dd><pre> 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." ); }); </pre></dd> <dt>They're also useful for timers.</dt> <dd><pre> 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); </pre></dd> <dt>and they're also frequently used when attaching event listeners.</dt> <dd><pre> 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." );</pre></dd> <dt>Private properties, using closures.</dt> <dd><pre> 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." ); </pre></dd> <dt>QUIZ: What are the values of the variables?</dt> <dd><pre>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); }</pre></dd> <dt>The last one is quite tricky, we'll revisit it.</dt> <dd><pre>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); }</pre></dd> <dt>Temporary Scope</dt> <dd></dd> <dt>Self-executing, temporary, function</dt> <dd><pre> (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" );</pre></dd> <dt>Now we can handle closures and looping.</dt> <dd><pre>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);</pre></dd> <dt>The anonymous wrapper functions are also useful for wrapping libraries.</dt> <dd><pre> (function(){ var myLib = window.myLib = function(){ // Initialize }; // ... })(); </pre></dd> <dt>Another way to wrap a library:</dt> <dd><pre> var myLib = (function(){ function myLib(){ // Initialize } // ... return myLib; })(); </pre></dd> <dt>QUIZ: Fix the broken closures in this loop!</dt> <dd><pre> var count = 0; for ( var i = 0; i < 4; i++ ) { setTimeout(function(){ assert( i == count++, "Check the value of i." ); }, i * 200); }</pre></dd> <dt>A quick wrapper function will do the trick.</dt> <dd><pre> 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);</pre></dd> <dt>Function Prototypes</dt> <dd></dd> <dt>Adding a prototyped method to a function.</dt> <dd><pre> function Ninja(){} Ninja.prototype.swingSword = function(){ return true; }; var ninjaA = Ninja(); assert( !ninjaA, "Is undefined, not an instance of Ninja." ); var ninjaB = new Ninja(); assert( ninjaB.swingSword(), "Method exists and is callable." ); </pre></dd> <dt>Properties added in the constructor (or later) overwrite prototyped properties.</dt> <dd><pre> function Ninja(){ this.swingSword = function(){ return true; }; } // Should return false, but will be overridden Ninja.prototype.swingSword = function(){ return false; }; var ninja = new Ninja(); assert( ninja.swingSword(), "Calling the instance method, not the prototype method." );</pre></dd> <dt>Prototyped properties affect all objects of the same constructor, simultaneously, even if they already exist.</dt> <dd><pre> function Ninja(){ this.swung = true; } var ninjaA = new Ninja(); var ninjaB = new Ninja(); Ninja.prototype.swingSword = function(){ return this.swung; }; assert( ninjaA.swingSword(), "Method exists, even out of order." ); assert( ninjaB.swingSword(), "and on all instantiated objects." ); </pre></dd> <dt>QUIZ: Make a chainable Ninja method.</dt> <dd><pre> function Ninja(){ this.swung = true; } var ninjaA = new Ninja(); var ninjaB = new Ninja(); // Add a method to the Ninja prototype which // returns itself and modifies swung assert( !ninjaA.swing().swung, "Verify that the swing method exists and returns an instance." ); assert( !ninjaB.swing().swung, "and that it works on all Ninja instances." ); </pre></dd> <dt>The chainable method must return this.</dt> <dd><pre> function Ninja(){ this.swung = true; } var ninjaA = new Ninja(); var ninjaB = new Ninja(); Ninja.prototype.swing = function(){ this.swung = false; return this; }; assert( !ninjaA.swing().swung, "Verify that the swing method exists and returns an instance." ); assert( !ninjaB.swing().swung, "and that it works on all Ninja instances." ); </pre></dd> <dt>Instance Type</dt> <dd></dd> <dt>Examining the basics of an object.</dt> <dd><pre> function Ninja(){} var ninja = new Ninja(); assert( typeof ninja == "object", "However the type of the instance is still an object." ); assert( ninja instanceof Ninja, "The object was instantiated properly." ); assert( ninja.constructor == Ninja, "The ninja object was created by the Ninja function." ); </pre></dd> <dt>We can still use the constructor to build other instances.</dt> <dd><pre> function Ninja(){} var ninja = new Ninja(); var ninjaB = new ninja.constructor(); assert( ninjaB instanceof Ninja, "Still a ninja object." ); </pre></dd> <dt>QUIZ: Make another instance of a Ninja.</dt> <dd><pre>var ninja = (function(){ function Ninja(){} return new Ninja(); })(); // Make another instance of Ninja var ninjaB = ___; assert( ninja.constructor == ninjaB.constructor, "The ninjas come from the same source." );</pre></dd> <dt>QUIZ: Use the .constructor property to dig in.</dt> <dd><pre>var ninja = (function(){ function Ninja(){} return new Ninja(); })(); // Make another instance of Ninja var ninjaB = new ninja.constructor(); assert( ninja.constructor == ninjaB.constructor, "The ninjas come from the same source." );</pre></dd> <dt>Inheritance</dt> <dd></dd> <dt>The basics of how prototypal inheritance works.</dt> <dd><pre> function Person(){} Person.prototype.dance = function(){}; function Ninja(){} // Achieve similar, but non-inheritable, results Ninja.prototype = Person.prototype; Ninja.prototype = { dance: Person.prototype.dance }; assert( (new Ninja()) instanceof Person, "Will fail with bad prototype chain." ); // Only this maintains the prototype chain Ninja.prototype = new Person(); var ninja = new Ninja(); assert( ninja instanceof Ninja, "ninja receives functionality from the Ninja prototype" ); assert( ninja instanceof Person, "... and the Person prototype" ); assert( ninja instanceof Object, "... and the Object prototype" ); </pre></dd> <dt>QUIZ: Let's try our hand at inheritance.</dt> <dd><pre> function Person(){} Person.prototype.getName = function(){ return this.name; }; // Implement a function that inherits from Person // and sets a name in the constructor var me = new Me(); assert( me.getName(), "A name was set." );</pre></dd> <dt>The result is rather straight-forward.</dt> <dd><pre> function Person(){} Person.prototype.getName = function(){ return this.name; }; function Me(){ this.name = "John Resig"; } Me.prototype = new Person(); var me = new Me(); assert( me.getName(), "A name was set." );</pre></dd> <dt>Built-in Prototypes</dt> <dd></dd> <dt>We can also modify built-in object prototypes. <dd><pre> 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) ); }); </pre></dd> <dt>Beware: Extending prototypes can be dangerous.</dt> <dd><pre> 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;</pre></dd> <dt>Enforcing Function Context</dt> <dd></dd> <dt>What happens when we try to bind an object's method to a click handler?</dt> <dd><pre> 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" ); </pre></dd> <dt>We need to keep its context as the original object.</dt> <dd><pre> 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" ); </pre></dd> <dt>Add a method to all functions to allow context enforcement.</dt> <dd><pre> 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" ); </pre></dd> <dt>Our final target (the .bind method from Prototype.js).</dt> <dd><pre> Function.prototype.bind = function(){ var fn = this, args = Array.prototype.slice.call(arguments), object = args.shift(); return function(){ return fn.apply(object, args.concat(Array.prototype.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" ); </pre></dd> <dt>Bonus: Function Length</dt> <dd></dd> <dt>How does a function's length property work?</dt> <dd><pre> function makeNinja(name){} function makeSamurai(name, rank){} assert( makeNinja.length == 1, "Only expecting a single argument" ); assert( makeSamurai.length == 2, "Multiple arguments expected" ); </pre></dd> <dt>We can use it to implement method overloading.</dt> <dd><pre> function addMethod(object, name, fn){ // Save a reference to the old method var old = object[ name ]; // Overwrite the method with our new one object[ name ] = function(){ // Check the number of incoming arguments, // compared to our overloaded function if ( fn.length == arguments.length ) // If there was a match, run the function return fn.apply( this, arguments ); // Otherwise, fallback to the old method else if ( typeof old === "function" ) return old.apply( this, arguments ); }; } </pre></dd> <dt>How method overloading might work, using the function length property.</dt> <dd><pre> function addMethod(object, name, fn){ // Save a reference to the old method var old = object[ name ]; // Overwrite the method with our new one object[ name ] = function(){ // Check the number of incoming arguments, // compared to our overloaded function if ( fn.length == arguments.length ) // If there was a match, run the function return fn.apply( this, arguments ); // Otherwise, fallback to the old method else if ( typeof old === "function" ) return old.apply( this, arguments ); }; } function Ninjas(){ var ninjas = [ "Dean Edwards", "Sam Stephenson", "Alex Russell" ]; addMethod(this, "find", function(){ return ninjas; }); addMethod(this, "find", function(name){ var ret = []; for ( var i = 0; i < ninjas.length; i++ ) if ( ninjas[i].indexOf(name) == 0 ) ret.push( ninjas[i] ); return ret; }); addMethod(this, "find", function(first, last){ var ret = []; for ( var i = 0; i < ninjas.length; i++ ) if ( ninjas[i] == (first + " " + last) ) ret.push( ninjas[i] ); return ret; }); } var ninjas = new Ninjas(); assert( ninjas.find().length == 3, "Finds all ninjas" ); assert( ninjas.find("Sam").length == 1, "Finds ninjas by first name" ); assert( ninjas.find("Dean", "Edwards").length == 1, "Finds ninjas by first and last name" ); assert( ninjas.find("Alex", "X", "Russell") == null, "Does nothing" ); </pre></dd> </dl> </body> </html>
(C) Æliens 20/08/2009
You may not copy or print any of this material without explicit permission of the author or the publisher. In case of other copyright issues, contact the author.