31 KiB
created_at | title | url | author | points | story_text | comment_text | num_comments | story_id | story_title | story_url | parent_id | created_at_i | _tags | objectID | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2016-12-11T04:47:14.000Z | Learning Advanced JavaScript (2008) | http://ejohn.org/apps/learn/ | kparaju | 192 | 55 | 1481431634 |
|
13149635 |
Learning Advanced JavaScript
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 = Array.prototype.slice.call(arguments), object = args.shift(); return function(){ return fn.apply(object, args.concat(Array.prototype.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!" );
- Defining Functions
- What ways can we define functions?
- function isNimble(){ return true; } var canFly = function(){ return true; }; window.isDeadly = function(){ return true; }; log(isNimble, canFly, isDeadly);
- Does the order of function definition matter?
- 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; }
- Where can assignments be accessed?
- 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; };
- Can functions be defined below return statements?
- function stealthCheck(){
assert( stealth(), "We'll never get below the return, but that's OK!" );
return stealth(); function stealth(){ return true; }
}
stealthCheck();
- Named Functions
- We can refer to a function, within itself, by its name.
- function yell(n){ return n > 0 ? yell(n-1) + "a" : "hiy"; } assert( yell(4) == "hiyaaaa", "Calling the function by itself comes naturally." );
- What is the name of a function?
- 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 );
- We can even do it if we're an anonymous function that's an object property.
- 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." );
- But what happens when we remove the original object?
- 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?" ); }
- Let's give the anonymous function a name!
- 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." );
- What if we don't want to give the function a name?
- 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." );
- Functions as Objects
- How similar are functions and objects?
- var obj = {}; var fn = function(){}; assert( obj && fn, "Both the object and function exist." );
- How similar are functions and objects?
- 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." );
- Is it possible to cache the return results from a function?
- 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 ); log( "Cache found: ", getElements.cache.pre.length );
- QUIZ: Can you cache the results of this function?
- 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?" );
- One possible way to cache the results:
- 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." );
- Context
- What happens if a function is an object property?
- var katana = { isSharp: true, use: function(){ this.isSharp = !this.isSharp; } }; katana.use(); assert( !katana.isSharp, "Verify the value of isSharp has been changed." );
- 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){ 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 = 1; 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.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] assert(highest(1, 1, 2, 3)[0] == 3, "Get the highest value."); // Expecting: [5,4,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 Array().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 = 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.)" );
- 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("
- Loading...
");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);
- Function Prototypes
- Adding a prototyped method to a function.
- 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." );
- Properties added in the constructor (or later) override prototyped properties.
- 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." );
- Prototyped properties affect all objects of the same constructor, simultaneously, even if they already exist.
- 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." );
- QUIZ: Make a chainable Ninja method.
- 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." );
- The chainable method must return this.
- 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." );
- Instance Type
- Examining the basics of an object.
- 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." ); - We can still use the constructor to build other instances.
- function Ninja(){}
var ninja = new Ninja();
var ninjaB = new ninja.constructor();
assert( ninjaB instanceof Ninja, "Still a ninja object." );
- QUIZ: Make another instance of a Ninja.
- 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." );
- QUIZ: Use the .constructor property to dig in.
- 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." );
- Inheritance
- The basics of how prototypal inheritance works.
- 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" );
- QUIZ: Let's try our hand at inheritance.
- 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." );
- The result is rather straight-forward.
- 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." );
- 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" );
- Our final target (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)));
};
};
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" );
- Bonus: Function Length
- How does a function's length property work?
- function makeNinja(name){} function makeSamurai(name, rank){} assert( makeNinja.length == 1, "Only expecting a single argument" ); assert( makeSamurai.length == 2, "Multiple arguments expected" );
- We can use it to implement method overloading.
- 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 ); };
}
- How method overloading might work, using the function length property.
- 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" );