Learning Advanced JavaScript


    Double-click the code to edit the tutorial and try your own code.
    This tutorial contains code and discussion from the upcoming book Secrets of the JavaScript Ninja by John Resig.

    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" );