[旧文]打造Ajax通用库 - 1.Css选择器的实现

[size=xx-large]这是很久很久很久以前写的文章,其中[color=red]暗藏Bug[/color],各位小心阅读...[/size]


[size=large]
内容提要:
介绍Ajax通用库的系列教程,为编写可重用,高质量javascript的代码提供了指导,目前已经完成第一节"Css选择器的实现"的草稿.

目标读者:
有一点Ajax基础的前台Web开发人员

作者: 张沈鹏
Email:zsp007@gmail.com
时间:2007年6月21日13:05:51

没有人喜欢重复造轮子,在Ajax领域也是一样.通常我们都会去寻找一些Ajax的库来简化我们的应用编写,常用的库有Jquery,Prototype等.

也许你会提出这样一个问题:"既然已经有了众多的如此优秀Ajax库,有必要花费大量的精力来熟悉那些只有库的开发者才需要理解的细节,甚至来自己实现一个这样的库吗?"

我来还是引用来自<<银弹和我们的职业>>中的一段话作为回答吧.

"
新技术给高手带来方便。菜鸟们却不用指望被新技术拯救。

沿用以前的比喻,一流的摄影师不会因为相机的更新换代而丢掉饭碗,反而可能借助先进技术留下传世佳作。因为摄影的本质困难,还是摄影师的艺术感觉。热门技术也就等于相机。不停追新,学习这个框架,那个软件,好比成天钻研不同相机的说明书。而热门技术后的来龙去脉,才好比摄影技术。为什么推出这个框架?它解决了什么其它框架不能解决的问题?它在哪里适用?它在哪里不适用?它用了什么新的设计?它改进了哪些旧的设计?Why is forever.

-- http://blog.csdn.net/g9yuayon/archive/2006/12/10/1437195.aspx
"

本文通过介绍一个简易Css选择器的实现过程,来带领大家一窥Ajax库编写的奥妙.

Ajax库与其他程序的编写有一个显著的不同,那就是代码尽可能的短小精悍.这并不是说要求你少加注释,或者把代码密密麻麻的挤在一行,或是把变量名取成一个字母.诸如此类的问题完全可以交由工具来完成,实践证明它们已经相当完善.

另外一个问题就是javascript语句行尾的分号.以往,由于相关的代码工具压缩不完善,一般而言不建议省略这个分号.但是进来一个开源的Javascript压缩程序JSA(Javascript Analyser)能够完美的补全被省略的分号,所以个人认为这个问题现在已经无足轻重,你完全可以依据自己的喜好取舍.

我们还是先来写几个十分短小但很常用的函数作为准备工作.之所以要写这些辅助函数,主要是为了体现代码的可复用性.

首先是

function copy(dic,obj) {
for(var i in dic)
if(!obj[i])
obj[i]=dic[i];
return obj;
}

function extend(dic,obj){
copy(dic,obj.prototype);
}


上面两个函数很简单,稍微有一点js代码基础的人都应该能很容易的看出他们的用途.但还是啰唆的讲两句吧.

第一个函数copy是将一个字典的属性复制给一个对象.在Javascript字典就是一个object键值对,在这里只是做语意上的区分.
在本文这个示例中copy只是在接下来的函数extend中被重新封装一次,用途不是很明显.但它的确很有用,在<<打造Ajax通用库>>系列文章接下来的部分我会再次提及它.

第二个函数extend顾名思义,是把copy作用在对象的原型(prototype)上.这样对于该类型对象的每个实例都会有字典中所描述的属性.看看紧随的代码,你应该可以明白如何来运用该函数.

extend({
//查找对象第一次出现的位置
indexOf
:
function(elem_to_find,i){
for(var l=this.length,i=i||0;i<l;++i)
if(this[i]==elem_to_find)
return i;
return -1;
},
//求两个数组的交集,如果有重复元素,只保留一个
U:
function(array){
for(var i=0,l=array.length;i<l;++i)
if(this.indexOf(array[i])<0)
this.push(array[i]);
},
//对每个元素依次调用指定函数
each:
function(func){
for(var l=this.length,i=0;i<l;++i)
func(this[i])
}
},Array);


在此,对内建类型Array进行了extend.对于内建类型进行原型扩展有利也有弊,优点是:十分方便,从下面的用法演示可见一斑.缺点:是会影响对象的for...in遍历.比如数组,在不对其prototype进行修改的情况下可以这样来遍历,比如:
var a=["a","b","c"]
for(var i in a)alert("a["+i+"]="+a[i]);//显示a[0]=a ; a[1]=b ; a[2]=c
但是如果你对Array.prototype进行了扩展,便会多出被扩展的项来.
比如:
Array.prototype.name="array"
var a=["a","b","c"]
for(var i in a)alert("a["+i+"]="+a[i]);//显示a[0]=a ; a[1]=b ; a[2]=c ; a[name]=array

个人认为,for...in循环应该只用于对象,所以不应该对Object的prototype进行扩展,其他的内建类型则不必太在意这个限制.

下面先来看看extend字典中的3个函数的用途,同时剖析其中的一些代码.

1.
indexOf
和string的indexOf类似,有两种调用方式

array.indexOf(elem)
查找数组中该元素第一次出现的位置

array.indexOf(elem, start)
查找数组中该元素在array[start]及其之后第一次出现的位置

用法演示:
var array1=[0,2,4,2,8];
alert("array1.indexOf(2)="+array1.indexOf(2)); //显示 array1.indexOf(2)=1
alert("array1.indexOf(2,3)="+array1.indexOf(2,3)); //显示 array1.indexOf(2,3)=3
alert("array1.indexOf(1)="+array1.indexOf(1)); //显示 array1.indexOf(1)=-1

对于Firefox系列的浏览器,在其1.5版之后使用的是JavaScript版本号是1.6,内建了方法.但是很不幸,诸如IE等浏览器的数组没有相应的函数.于是我们用extend手工为它们的数组添加了一个indexOf.

注意如果有内建的indexOf,extend并不会去覆盖它.这样,我们既可以享受native函数的高效,又能得到不支持该方法浏览器的兼容.鱼与熊掌二者兼得,不亦乐乎.

代码剖析:
function(elem_to_find,i){
for(var i=i||0,l=this.length;i<l;++i)...;
}
这里中有2个值得留意的地方
a.
i=i||0
i是可选参数,其值为大于等于0的正整数.
又因为 (undefined || 0) 的返回值是0, (正整数 || 0)的返回值是正整数.
根据这个技巧 i=i||0 相当于把i的默认值设为0
b.
for(var l=this.length,i=i||0;i<l;++i)
this.length每次调用都要检查数组长度是否改变,这需要消耗一点点的时间.通常来说,这一点点时间是可以忽略不计 ,但是作为一个可能被非常频繁调用来说,进行适当的性能优化是有益无害的.
因为这里不会对数组填删元素,进而不会影响数组的长度.可以用一个变量来保存长度值,有测试表明这样比每次都访 问length属性要在循环速度上要快上30%左右.
当然,如果你不需要按照顺序正向遍历的话,可以使用另一个更为简洁的技巧,如下:
for(var i=this.length;i>0;--i)

2.
U(数组/类数组)
求两个数组的交集,如果有重复元素,只保留一个.之所以选择大写的U作为函数名,是因为它和数学上的交集符号很类似,方便记忆.
(注:类数组的含义见函数$A的简介)

用法演示:
var array2=[0,1,2,3],array3=[1,2,3,4];
array2.U(array3);
alert(array2); // 显示 [0,1,2,3,4]

3.
each( function(elem) )
对数组中的每个对象依次调用该函数.该函数有一个参数,其值为当前被处理的元素.许多语言中都有类似的功能,比如python的filter.

用法演示:
var array4=[0,1,2,3],array5=[];
array4.each(function(e){
array5.push(e*e);
});
alert(array5);//显示 [0 1 4 9]

OK,下面再写一个工具函数,为Css选择器的编写做最后的准备.

function $A(obj){
var i=obj.length,array=[];
while(i)array[--i]=obj[i]
return i==0?array:[o];
}
该函数的是用来将为类数组的参数转换为数组,如果参数没有length属性,则返回[obj].

类数组是指和数组类似有length属性,可以同过xxx[0],xxx[1]的方式获取其中的元素,但本身并不是array实例的对象.比如document.getElementsByTagName("body")返回的就是一个类数组对象.

通过这种转换,就可以对其应用属于数组的那些方法,其中包括上面的自定义方法.

终于,可以切入正题,编写我们的Css选择器 -- $ 了.

其$用法如下,熟悉css的朋友一定感到很亲切吧.

$("#div1").innerHTML
$("#div1 .class2 p")[0].innerHTML
$(".class2")[1].innerHTML

还是来看代码
$=function css_select(css,scope){
if(window==this)
return new css_select(css,scope);

css=css.split(' ');
scope=scope||document;

//#应该只出现1次,如果出现应该是第1个
if("#"==css[0].charAt(0))
scope=scope.getElementById(css.shift().substring(1));

if(css[0]){
var result,className,elems,elems_bak;
scope=[scope];

css.each(
function(i){
if("."==i.charAt(0))
className=i.substring(1),i='*';
result=[];
scope.each(function(e){
elems=e.getElementsByTagName(i)
if(className){
elems_bak=$A(elems);
elems=[];
elems_bak.each(
function(e){
if(e.className.split(/\s+/).indexOf(className)>-1)
elems.push(e);
});
className=0;
}
result.U(elems);
});
scope=result;
});
}

return scope;
}

显而易见,$不过是css_select的一个别名,这样可以方便的通过修改$为其他名称来防止名字冲突.

css_select有两个参数
1.css选择字串

它支持三种选择
1. id 方式 : #id名
2. className(样式名) 方式 : .样式名
3. TagName(标签名) 方式 : 标签名

其中,由于id是应该可以用来唯一确定元素的,所以id在选择字串至多出现1次,如果有也应该是第一个标识符.其他两种可以任意相互嵌套使用.

当css选择字串有且仅有id名时返回的是单个元素,其他情况下返回被选择元素的数组(即使被选中的元素只有一个).

2.搜索范围
比如document,或是某个HTMLElement

可以看到在这段代码反复重用前面定义的代码,下面来谈谈其实现原理.

它首先对传入字串按空格划分为数组
css=css.split(' ');

依据上面所说id应该出现1次,如果出现应该是第1个,所以可以首先判断第一个选择符是否是一个id标识符
if("#"==css[0].charAt(0))scope=scope.getElementById(css.shift().substring(1));
如果第一个元素是id标识符,则通过document.getElementById(这时scope==document)获取指定的元素
这里css.shift().substring(1)是一个通过.来链接 弹出 , 截取 操作的示例,用这种连.技巧可以使得代码显得简洁优雅.

接下来由于字符串数组不会再包含id标示了,我们只需要对className和tagName进行选择操作.

要实现className,tagName可以相互嵌套,可依次遍历css数组中标识符,并相应的把前一个的结果作为后一个的作用域.配合前面扩展是数组方法each和两个变量scope和result可以很轻松的实现该功能.

对于tagName进行选择操作很简单,可以通过Dom中getElementsByTagName来完成,而我们主要的工作是完成一个功能类似getElementsByClassName的代码片段.

这里的做法是通过getElementsByTagName('*')来取得.

最后,我们给出测试页面的完整代码,你可以复制这些代码到一个html,来查看最终效果,注意请用UTF-8编码保存:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
div {border:solid red 1px; margin:10px;float:left;padding:10px;}
.class2 {border-color:black;}
p {border:solid green 1px;}
</style>

<script>
window.οnlοad=function(){
alert($("#div1").innerHTML)
alert($("#div1 .class2 p")[0].innerHTML)
alert($(".class2")[1].innerHTML)
}
</script>

<script>
function copy(dic,obj) {
for(var i in dic)
if(!obj[i])
obj[i]=dic[i];
return obj;
}

function extend(dic,obj){
copy(dic,obj.prototype);
}

extend({
//查找对象第一次出现的位置
indexOf
:
function(elem_to_find,i){
for(var l=this.length,i=i||0;i<l;++i)
if(this[i]==elem_to_find)
return i;
return -1;
},
//求两个数组的交集,如果有重复元素,只保留一个
U:
function(array){
for(var i=0,l=array.length;i<l;++i)
if(this.indexOf(array[i])<0)
this.push(array[i]);
},
//对每个元素依次调用指定函数
each:
function(func){
for(var l=this.length,i=0;i<l;++i)
func(this[i])
}
},Array);

function $A(obj){
var i=obj.length,array=[];
while(i)array[--i]=obj[i];
return i==0?array:[o];
}

$=function css_select(css,scope){
if(window==this)
return new css_select(css,scope);

css=css.split(' ');
scope=scope||document;

//#应该只出现1次,如果出现应该是第1个
if("#"==css[0].charAt(0))
scope=scope.getElementById(css.shift().substring(1));

if(css[0]){
var result,className,elems,elems_bak;
scope=[scope];

css.each(
function(i){
if("."==i.charAt(0))
className=i.substring(1),i='*';
result=[];
scope.each(function(e){
elems=e.getElementsByTagName(i)
if(className){
elems_bak=$A(elems);
elems=[];
elems_bak.each(
function(e){
if(e.className.split(/\s+/).indexOf(className)>-1)
elems.push(e);
});
className=0;
}
result.U(elems);
});
scope=result;
});
}

return scope;
}

</script>


</head><body>
<div id="div1">
Css Selector :
<div class="class2">
<p>Hello World !</p>
</div>
<div class="class2">Author Email:zsp007@gmail.com</div>
</div>


</body></html>[/size]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值