The Power of Prototype.js

转载 2006年06月13日 10:05:00

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!).

37、Power Query-不使用IF嵌套进行匹配

本节要点:Power Query-不使用IF嵌套进行匹配(突破IF语句嵌套写法) 标题可能很笼统,话不多说,直接看案例。 左表是学生成绩表,右边是成绩范围对应的等级,目的是匹配到学生成绩的等级...
  • zhongguomao
  • zhongguomao
  • 2017年01月16日 10:31
  • 1938

29、Power Query-分支语句的进阶

本节重点:Power Query-分支语句的进阶 例子:同样是之前的数据源,现在要根据多列进行判断,要求求出每门功课都及格的人数。 比如上图中的“休闲海”小朋友,他的三门功课都是">=60...
  • zhongguomao
  • zhongguomao
  • 2017年01月12日 14:55
  • 3055

监听 HOME键 BACK键 POWER键 + 总结

Back键的监听   对于Back键的监听比较容易,可以在多个系统回调处拦截,比如在activity的下列方法中都可以收到Back键按下的事件: @Override     public void ...
  • qweewqpkn
  • qweewqpkn
  • 2015年05月08日 15:33
  • 1403

31、Power Query-行、列、表的构造

本节重点:Power Query-行、列、表的构造 之前我们讲过了已有数据的构造,这次我们使用空查询来简单讲解一下行(record)、列(list)、表(table)的创建。 一、行(record) ...
  • zhongguomao
  • zhongguomao
  • 2017年01月13日 10:11
  • 1905

48、Power Query-大量复杂数据的整理汇总

工作中,生活中不免会遇到需要整理大量复杂数据的情况,单纯的靠手动去搜索统计固然可以,但是效率极其低下。 如下图,列举了1990年~2015年NBA美职篮全明星的参赛名单,现在需要统计出每个人的参赛次数...
  • zhongguomao
  • zhongguomao
  • 2017年02月13日 16:53
  • 2464

1、大道至简的数据处理工具-(Microsoft Power Query入门)

大道至简的数据处理工具-Microsoft Power Query 告别复杂的excel函数,excel VBA编程,让一切回归简单与职能。 什么样的人群适合这样的一个工具: 1、出纳、会计、统计、仓...
  • zhongguomao
  • zhongguomao
  • 2016年12月23日 15:23
  • 11648

32、Power Query-利用自定义函数获取指定页数数据

本节要点:Power Query-利用自定义函数获取指定页数数据 我们前面不是有一节已经讲过如何获取网页的数据了么? http://blog.csdn.net/zhongguomao/article/...
  • zhongguomao
  • zhongguomao
  • 2017年01月13日 10:37
  • 2112

9、Power Map—应用拾取坐标系统确定经纬度

本节要点:拾取坐标确定经度以及纬度 例如之前的楼盘位置,比如有些地址比较模糊,根据名称的话在地图上找不到,这个时候我们就需要用经度以及纬度来确定。 我们可以利用网络工具百度地图...
  • zhongguomao
  • zhongguomao
  • 2017年02月18日 14:39
  • 1603

2、Power Query-动态汇总单元格区域数据

数据来源之单元格区域数据(动态汇总单元格区域数据) 这次主要讲解power query的基本操作界面。 任务:求出各个行业第一、二季度的总数据。 例子非常简单,聪明的朋友可能会认为用透视图更加简...
  • zhongguomao
  • zhongguomao
  • 2016年12月23日 21:42
  • 4970

6、Power View—条形图的应用

经过之前的学习铺垫,今天我们正式进入到图表的应用一块。 如下图素材,数据量较大,我们需要用图表的方式来展现每个品类的销售总额。 进入到Power View界面,我们可以在“设计”选项卡下面...
  • zhongguomao
  • zhongguomao
  • 2017年02月22日 16:27
  • 970
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:The Power of Prototype.js
举报原因:
原因补充:

(最多只允许输入30个字)