The Power of Prototype.js

标签: functioncharacterarraysjavascriptmethodsobject
1498人阅读 评论(0) 收藏 举报
分类:

Computer languages evolve across an interesting number of vectors, and not always in ways that the original designers had planned. For every high level, top-down decision to implement new features and capabilities, there are interesting bits of best practices, useful libraries, and design patterns that can, subtly and sometimes not so subtly, change the course of direction of a language in critical ways.

AJAX is a good case in point - I’m in the process of writing on AJAX for a book, and occasionally I have to step out of my own preconceived notions of where the language (principally refering to JavaScript here and not the XML side) has been and look at where the language is going in terms of its own long and winding path. Certainly Ruby has been influencing things by bundling in interesting JavaScript components on the server side, but I think a more interesting case in point is the use of a set of libraries - collected together as prototype.js - that are rapidly reshaping how we use the language, especially in the context of web browsers.

The prototype.js libraries, developed by Sam Stephenson at http://prototype.conio.net/, seemed to have evolved out of the Ruby on Rails project to take on a life of its own. It includes a number of extraordinarily useful library functions and introduces the “$” as notation within JavaScript. This library now underlies many of the AJAX frameworks in use on the web, and it’s not unlikely that it will creep into the “core” implementation over time.

One of the central things that prototype.js does is define a set of additional useful objects, including a new Hash object, a new Enumerable class, ranges, an easy to use AJAX class, as well as extensions to such core classes as number, string and array. It also provides the most same and rational shortcuts to entirely too verbose methods such as getElementById.

To illustrate that last point, prototype.js defines a function called “$” … yep, that’s right - the dollar sign. Turns out that the dollar sign is in fact a valid character for names in JavaScript (a fact obscured by years of dominance by JScript, which didn’t recognize this salient fact). The prototype.js library defines $() as a function to replace the ubiquitous (and painful) document.getElementById() method, with an added twist that if an element (or other object) is passed into it, the object gets passed out the other side. This means that if you are wanting to refer to an element with id “foo”, you’d use the expression:

var foo = $("foo");
var foo2 = $(foo);

rather than

var foo = document.getElementById("foo");
if (typeof foo == "string"){
var foo2 = document.getElementById("foo");
}
else {
var foo2 = foo;
}

Given a typical browser function, this can turn something as painful as:

var updateFunctionList=function(){
var functionMenu = document.getElementById("functionMenu");
var functionDisplay = document.getElementById("functionDisplay");
var functionTabs = document.getElementById("functionTabs");
var functionTabPanels = document.getElementById("functionTabPanels");
tabCt=0;
for (key in this.functionType){
tabCt++;
var tab = "<tab xmlns='"+namespaces.xul+"' label='"+this.functionType[key]+"'/>";
var tabNode = (new DOMParser()).parseFromString(tab,"text/xml").documentElement.cloneNode(true);
if (tabCt ==1){
tabNode.setAttribute("selected","true");
}
functionTabs.appendChild(tabNode);
var tabPanel = "<tabpanel xmlns='"+namespaces.xul+"' id='function_"+key+"' orient='vertical' class='functionPanel'/>";
var tabPanelNode = (new DOMParser()).parseFromString(tabPanel,"text/xml").documentElement.cloneNode(true);
functionTabPanels.appendChild(tabPanelNode);
}
for (key in this.functionSet){
var menuitem = "<menuitem label='"+this.functionSet[key].name+"' xmlns='"+namespaces.xul+"' oncommand='window.calculator.exec(/""+key+"/")'/>";
menuitemNode = (new DOMParser()).parseFromString(menuitem,"text/xml").documentElement.cloneNode(true);
functionMenu.appendChild(menuitemNode);
var button = "<button label='"+this.functionSet[key].name+"' xmlns='"+namespaces.xul+"' oncommand='window.calculator.exec(/""+key+"/")'/>";
buttonNode = (new DOMParser()).parseFromString(button,"text/xml").documentElement.cloneNode(true);
var panel = document.getElementById("function_"+ this.functionSet[key].functionType);
panel.appendChild(buttonNode);
}
}

(from a sample XUL app I’m doing for my Firefox book), into something at least a little cleaner:

//$E is my own function, in the same mold, for creating elements from strings
var $E=function(eltStr){
if (typeof(eltStr)=="string"){
return (new DOMParser()).parseFromString(eltStr,"text/xml").documentElement.cloneNode(true);
}
else {
return eltStr;
}
}

var updateFunctionList = function(){
var tabCt=0;
for (key in this.functionType){
tabCt++;
var tab = "<tab xmlns='"+namespaces.xul+"' label='"+this.functionType[key]+"'/>";
var tabNode = $E(tab);
if (tabCt ==1){
tabNode.setAttribute("selected","true");
}
$('functionTabs').appendChild(tabNode);
var tabPanel = "<tabpanel xmlns='"+namespaces.xul+"' id='function_"+key+"' orient='vertical' class='functionPanel'/>";
$('functionTabPanels').appendChild($E(tabPanel));
}
for (key in this.functionSet){
var menuitem = "<menuitem label='"+this.functionSet[key].name+"' xmlns='"+namespaces.xul+"' oncommand='window.calculator.exec(/""+key+"/")'/>";
$('functionMenu').appendChild($E(menuitem););
var button = "<button label='"+this.functionSet[key].name+"' xmlns='"+namespaces.xul+"' oncommand='window.calculator.exec(/""+key+"/")'/>";
// the following references an individual panel content
$("function_"+ this.functionSet[key].functionType).appendChild($E(button));
}
}

The upside of this should be obvious - less code needed, the code isn’t a dense tangle of getElementById statements, and legibility is significantly approved.

The hash and array capabilities are similarly defined (and are cross-platform). One of the more intriguing problems that I’ve encountered with JavaScript arrays is that they are not terribly enumeration friendly. While you can use tthe built-in object enumeration that is part of JavaScript on Arrays, such enumerations not only return the numbered items in the array, but also all of the method and property handlers for that array, meaning that you have to specifically filter to stop once the array has exceeded the length:

var arr=["red","green","yellow","blue"];
var ct=0;
for (var index=0;index !=arr.length;indexx++){
var item = arr[index];
print (arr[index]);
}

The global $A() function turns arrays into fully enumeratable arrays, and in the process adds a few additional (and very useful) methods:

var arr=$A(["red","green","yellow","blue"]);
arr.each(function(color){print(color.toUpperCase());});

The each() method on the arrays incorporates a for loop for iterating through each of the items in the array. So far, this isn’t that different from the use of the for each keywords. However, prototype.js then goes on to use this method to invoke more sophisticated methods. For instance, suppose that you had a character generator for a game. You can use the prototype.js methods to significantly simplify many of the key array operations:

var NPCharacters = function(numChars){
var NPCharacter = function(charName){
var rollDie= function(numDie,pips,bias){
var sum = 0;
numDie.times(function(index){
sum +=Math.ceil(Math.random()*pips + bias);
if (sum >20){
sum =20;
}
});
return sum;
};
this.pcProps=$A(["strength","intelligence","wisdom","dexterity", "constitution","charisma"]);
this.generateCharacter=function(charName){
ch = new Object();
ch.gender = (Math.random()>0.5)?"female":"male";
ch.name = charName;
this.pcProps.each(function(pcProp){
ch[pcProp] = rollDie(3,6,0.2);
});
this._character = ch;
}

this.toString=function(){
var buf ="{";
var recStack = [];
for (key in this._character){
recStack.push(key+":'"+this._character[key]+"'");
}
buf += recStack.join(", ")+"}";
return buf;
}
this.generateCharacter(charName);
}

this.generateCharacters=function(numChars){
var characterSet = [];
numChars.times(function(index){
var npcharacter = new NPCharacter("Character "+index);
characterSet.push(npcharacter);
});
this.characterSet = characterSet;
};
this.query = function(fn){
return $A(this.characterSet).findAll(fn);
}
this.toString = function(){
var buf = "[";
var npArr = [];
var charSet = this.characterSet;
charSet.length.times(function(index){
npArr.push(charSet[index].toString());
});
buf += npArr.join(",n");
return buf+"]";
}
this.generateCharacters(numChars);
}

There are a number of interesting functions covered here, illustrating how to build a character set generator. When passed an integer argument into the NPCharacters() constructor, the class creates that number of characters automagically.

var npcs = new NPCharacters(5);
print(npcs);
=>
[{gender:'male', name:'Character 0', strength:'9', intelligence:'11', wisdom:'12', dexterity:'15', constitution:'16', charisma:'13'},
{gender:'male', name:'Character 1', strength:'14', intelligence:'12', wisdom:'8', dexterity:'14', constitution:'13', charisma:'9'},
{gender:'female', name:'Character 2', strength:'15', intelligence:'12', wisdom:'12', dexterity:'10', constitution:'10', charisma:'14'},
{gender:'female', name:'Character 3', strength:'11', intelligence:'12', wisdom:'11', dexterity:'15', constitution:'9', charisma:'13'},
{gender:'female', name:'Character 4', strength:'9', intelligence:'15', wisdom:'12', dexterity:'10', constitution:'11', charisma:'10'}]

However, I think one of the cooler features is the findAll() method, which is used in the NPCharacter.query() method. It takes a callback function with the item and an index as a signature, returning true if a criterion is met and false otherwise.

   this.query = function(fn){
return $A(this.characterSet).findAll(fn);
}

Thus, if you wanted to retrieve an array of all characters that are both intelligent (intelligence >14) and female (gender = “female”), you’d write it as:

npcs.query(function(record,index){with(record._character){return intelligence >14 && gender == "female";}});

(There are simpler ways of representing it, but this gets the idea across.)

The $A() function not only appends certain methods to the Array object, but also lets it inherit from the Enumeration class, which make group operations easier to do, including find, findAll, reject, pluck, partition and so forth. A full listing of these and other enumerable methods can be found at http://www.sergiopereira.com/articles/prototype.js.html.

Iterative loops can be created with the times() method, which takes an integer and uses that as the upper-bound for an incremental loop on a function:

var rollDie= function(numDie,pips,bias){
var sum = 0;
numDie.times(function(index){
sum +=Math.ceil(Math.random()*pips + bias);
if (sum >20){
sum =20;
}
});

Similarly you can make ranged arrays with the $R(min,max,includeBounds) function,which returns an incremental array of numbers from the min to either the max or just below the max, depending upon the includeBounds implementation.

The Hash object (implemented via the $H() function) provides additional objects on hashes (associative arrays such as the JavaScript base object) including keys(), values(), merge(),toQueryString(), and inspect(). While these can generally be obtained without the need for the special rider functions (i.e., with for loops), these can make for somewhat cleaner and more followable code.

Other extensions to the Array class include the following methods: clear() [Clears the array], compact() [ removes null and undefined entries], first() [gets the first item of the array], flatten() [turns multidimensional arrays into linear ones], indexOf(value) [ returns the index of the first selected item), inspect() [returns a pretty printed output], last() [returns the last item in and array], reverse(), shift() [Remove one item from the beginning], and without() [excludes the given items passed from the array.

Given the move to push arrays as first class data stack objects, these methods offer a dramatic improvement to the capabilities of most applications, and what’s more, they are rapidly becoming standardized as prototype.js becomes adopted.

This is just touching the surface of what prototype has to offer. In a future column, I’ll be touching on the AJAX and DOM functionality that prototype exposes. More information about the classes exposed with prototype can be found at http://www.sergiopereira.com/articles/prototype.js.html., and the prototype.js core can be found at http://prototype.conio.net/.

Kurt Cagle is an author and CTO for Metaphorical Web, Inc., and has been playing around with Javascript since its inception. He lives in Victoria, BC with his wife and daughters, not to mention nearby bears, cougars and leather jacket clad black squirrels (you gotta watch out for them, they’re vicious!).

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1571696次
    • 积分:20052
    • 等级:
    • 排名:第403名
    • 原创:299篇
    • 转载:341篇
    • 译文:17篇
    • 评论:759条
    最新评论