所谓造轮子的好处就是复习知识点,加深对原版jquery的理解。
本文系笔者学习jquery的笔记,记述一个名为"dQuery"的初级版和缩水版jquery库的实现。主要涉及知识点包括面向对象,jquery,绑定,脚本化css等。
一. jquery的美元符意味什么?
先思考alert(typeof $)的结果中的$,它是一个对象,也是一个函数。
所以美元符字面上是jQuery
,其实就是一个jq对象,里面可以接受函数,字符串(#xxx,.xxx,xxx...),还有一种是对象(比如this)。
dQuery是基于面向对象实现的,所以需要先写构造函数。为了方便遍历和其它方法操作,所有内容返回到一个数组中。这个数组依附于dQuery对象存在,之后将为这个dquery对象属性添加方法。
function dQuery(vArg){//参数是变体变量
this.elements=[];//选择器选择的元素扔到这个数组中
}
因为新的对象可以接受三种类型的参数(字符串,对象,函数)。作为一个变体变量,根据匈牙利命名法可以用vXxx
命名,同时需要分类讨论变量的情况:
1.当参数为函数时,执行函数——window.onload.——不好。
jquery写作时,经常允许多个$(function()),但是写window.onload会导致最终只执行最后一个。这时需要给函数绑定window.onload事件。
绑定事件的函数如下:
function myAddEvent(obj,sEv,fn){
if(obj.attachEvent){
obj.attachEvent('on'+sEv,fn);
}else{
obj.addEventListener(sEv,fn,false);
}
}
所以,当为id选择器时,查找:document.getElementById(vArg.substring(1));并把它加到this.elements中去。
2.当参数为字符串时,执行选择器操作.
这里需要对id选择器,类选择器,元素选择器进行判断。
注意:把选择器结果丢到一个数组(dQuery.elements)中去。方便遍历事件。
用charAt进行判断参数首位:(#,.或是其它)
(1)首位为#
时:
执行document.getElementById,同时把这个字符串稍作处理,用substring(1)方法把首个字符给去掉。
(2)首位为.
时
选取class类。可以设计一个getByClass函数
function getByClass(oParent,sClass){
var aEle=oParent.getElementsByTagName('*');//选择父元素的所有元素
var aResult=[];
var re=new RegExp('\b'+sClass+'\b','i');//正则边界
var i=0;
for(i=0;i<aEle.length;i++){
if(re.test(aEle[i].className)){
aResult.push(aEle[i]);
}
}
return aResult;
}
在本实例中,oParent父级对象实际上就是document,另一方面,因为getByClass返回的对象实际是一个数组,所以:
this.elements=getByClass(document,vArg.substring(1));
(3)参数为对象时,返回一个对象本身
把这个对象push到
(4)绑定基本的事件——比如点击事件
在接下来就是绑定事件。比如click(function(){}),它是一个回调函数。需要用到原型(prototype)方法。
//对选择器函数绑定click事件
dQuery.prototype.click=function(fn){
var i=0;
//对于返回器数组的内容
for(i=0;i<this.elements.length;i++){
myAddEvent(this.elements[i],'click',fn);
}
}
(5)简写设置
到目前为止,在调用这个dQuery时,每次都需要这样写:
new dQuery(function(){
new dQuery('input').click(function(){
alert('a');
})
});
每次都要使用new
,不用就会出问题。
如果我想使用类似jquery的简写方式,使用$d
作为简写,可以通过一个函数来定义:
function $d(vArg){
return new dQuery(vArg);
}
所以当前的最终代码为:
//可重复调用的加载函数
function myAddEvent(obj,sEv,fn){
if(obj.attachEvent){
obj.attachEvent('on'+sEv,fn);
}else{
obj.addEventListener(sEv,fn,false);
}
}
//class选择器调用函数
function getByClass(oParent,sClass){
var aEle=oParent.getElementsByTagName('*');//选择父元素的所有元素
var aResult=[];
var re=new RegExp('\b'+sClass+'\b','i');//正则边界
var i=0;
for(i=0;i<aEle.length;i++){
if(re.test(aEle[i].className)){
aResult.push(aEle[i]);
}
}
return aResult;
}
//定义dQuery对象
function dQuery(vArg){//参数是变体变量
this.elements=[];//选择器选择的元素扔到这个数组中
switch(typeof vArg){
//如果参数是函数
case 'function':
myAddEvent(window,'load',vArg);
break;
//如果参数是字符串
case 'string':
switch(vArg.charAt(0)){
case '#'://id选择器参数应该为#号之后的字符段
var obj=document.getElementById(vArg.substring(1));
this.elements.push(obj);
break;
case '.'://class
this.elements=getByClass(document,vArg.substring(1));
break;
default://标签
this.elements=document.getElementsByTagName(vArg);
}
break;
//如果参数是对象。
case 'object':
this.elements.push(vArg);
}
}
//对选择器函数绑定click事件
dQuery.prototype.click=function(fn){
var i=0;
//对于返回器数组的内容
for(i=0;i<this.elements.length;i++){
myAddEvent(this.elements[i],'click',fn);
}
}
function $d(vArg){
return new dQuery(vArg);
}
(6)小结
- 通过element属性储存被选中的元素
- 判断参数类型,以此做出不同的判断
- 对于参数是函数的情况,需要进行绑定window.onload。
- 对象则直接插入
二. 添加事件
1. 之前已经用原型方法的加上了click事件,现在研究下show/hide的实现:
从最简单的display——none/block结果开始写起:
//对选择器函数绑定show/hide事件
dQuery.prototype.show=function(){
var i=0;
//对于返回器数组的内容
for(i=0;i<this.elements.length;i++){
this.elements[i].style.display='block';
}
}
dQuery.prototype.hide=function(){
var i=0;
//对于返回器数组的内容
for(i=0;i<this.elements.length;i++){
this.elements[i].style.display='none';
}
};
实现功能已经没有问题了。
2.hover的实现:
(1)hover有两个参数,第一个是鼠标移入函数,第二个是鼠标移出函数。实际上跟click事件一样:
dQuery.prototype.hover=function(fnover,fnout){
var i=0;
for(i=0;i<this.elements.length;i++){
//给这个对象一次性绑定两个事件
myAddEvent(this.elements[i],'mouseover',fnover);
myAddEvent(this.elements[i],'mouseout',fnout);
}
};
就调用一般的alert方法等已经没有问题了,接下来要用类似jquery中css的写法定义它的样式
(2)css有两种调用:设置样式,只有一个参数时获取样式。总之参数不定(1或2个,attr、value)。只有两个参数时很简单。只需要做个循环,把value值赋给相应的样式属性就可以了:
this.elements[i].style[attr]=value;
当只有一个参数attr时,获取的样式可能不是唯一的。这时可以参考jquery的css方法定义:获取的是第一个元素的样式——如果你用xxx.style.样式的方法——获取的仅仅是行间样式,那这就麻烦了。需要再引入一个兼容性良好的样式获取函数getStyle。
function getStyle(obj,attr){
//元素,样式
if(obj.currentStyle){//兼容ie9及以下浏览器
return obj.currentStyle[attr];
}else{
return getComputedStyle(obj,false)[attr];
}
}
//css方法
dQuery.prototype.css=function(attr,value){
if(arguments.length==2){//当参数个数为2时,使用设置css的方法
var i=0;
for(i=0;i<this.elements.length;i++){
this.elements[i].style[attr]=value;
}
}else{//只有一个参数时获取样式
return getStyle(this.elements[0],attr);
}
};
实验证明,传入background无效,但是对具体的background-color无效。
(3)改进:this
虽然说当dQuery对象的参数为一个对象时,返回的就是对象本身。但是有时候在使用this的时候(ie),this指向了其它的对象(window对象)。
不能用this的情形——行间,定时器,绑定。js库中,实际上是绑定处理的。在此需要了解call方法。因此需要对myAddEvent函数进行修改。
call方法
应该知道,jquery中充满了各种简写。比如show,全称为show.call。比如下面这个例子:
function show(){
alert(this);
}
show();
show.call();
这个show函数的弹出结果为:window对象。show.call()弹出结果也是window对象,但call可以加参数,加了参数之后,this返回的值为参数——show.call('abc');弹出结果为abc。
进一步,给a,b传入两个参数:
function show(a,b){
alert('this是:'+this+',a是:'+a+',b是:'+b);
}
show(12,5);
show.call(12,5);
调用show函数的第一个弹出结果是:
调用show.call(12,5)弹出:
这说明call方法中的第一个参数默认指向this。比如要调用可以写成show('abc',12,5)
与call类似,apply也类似——
apply('abc',[12,5])
的结果和show('abc',12,5)
完全一样。
知道了call方法,回到myAddEvent函数的修改上来,应该让fn返回的主题回到obj——
function myAddEvent(obj,sEv,fn){
if(obj.attachEvent){
obj.attachEvent('on'+sEv,function(){
fn.call(obj);//兼容ie
});
}else{
obj.addEventListener(sEv,fn,false);
}
}
这样一来,就可以使用$d(this)
了。
3.toggle方法
toggle方法是旧jquery中很有用的点击处理方法。可以传多个事件处理函数。每一次触发,就按顺序执行。调用形式如下:
$d(function(){
$d('#btn1').click(function(){
$d('#div1').toggle(function fn1(){
},function fn2(){
},function fn3(){
}...);
});
});
怎么模拟这个函数呢?——
(1)需要一个计数器,根据计数的结果选择执行哪个函数。
toggle可以传任意个参数。所以参数干脆不加了。
在计数器的实现过程中会出现问题。
window.onload=function (){
var aBtn=getElementsByTagName('button');
var i=0;
var count=0;//计数器
for(i=0;i<aBtn.length;i++){
aBtn[i].onclick=function(){
alert(count++);
}
}
}
如果我有多个按钮需要触发toggle内的事件,但是按照dQuery先前的方法,必然导致所有按钮公用一套计数器。这样一来效果就全乱了。怎么解决呢?
最好的办法是,把计数过程存为一个函数,
window.onload=function (){
var aBtn=getElementsByTagName('button');
var i=0;
function addClick(obj){
var count=0;//关键步骤:计数器塞进函数里了。
alert(count++);
}
for(i=0;i<aBtn.length;i++){
aBtn[i].onclick=function(){
addClick(aBtn[i]);
}
}
}
或者(对阅读不友好):
window.onload=function (){
var aBtn=getElementsByTagName('button');
var i=0;
for(i=0;i<aBtn.length;i++){
(function(obj){
var count=0;//关键步骤:计数器塞进函数里了。
alert(count++);
})(aBtn[i]);
}
}
}
分别计数就这么实现了。调用函数几次,就会产生多少个局部变量。也可以使用xxx.index这样的方法。
现在要在toggle里面调用私有的计数器,思路也是把计数器放到一个声明的函数里边。
dQuery.prototype.toggle=function(){
//私有计数器,计数器会被一组对象所享用。
function addToggle(obj){
var count=0;
myAddEvent(obj,'click',function(){
alert(count++);
})
}
var i=0;
for(i=0;i<this.elements.length;i++){
addToggle(this.elements[i]);
}
}
接下来是使用arguments
来做,用以替换掉addToggle里边的事件处理:让计数器递增的同时,选取它对参数长度取模的索引的参数,。并把本体call为参数obj(有点拗口)
myAddEvent(obj,'click',function(){
arguments[count++%arguments.length].call(obj);
}
结果出错。主要是arguments乱了,实际执行情况是,arguments是函数myAddEvent第三个参数(函数)的参数。因此,需要提前把toggle方法的arguments传入一个数组。
完整toggle的的代码是
//toggle方法:
dQuery.prototype.toggle=function(){
var _arguments=arguments;//把toggle的arguments存起来,以便在其它函数中可以调用。
//私有计数器,计数器会被一组对象所享用。
function addToggle(obj){
var count=0;
myAddEvent(obj,'click',function(){
_arguments[count++%_arguments.length].call(obj);
})
}
var i=0;
for(i=0;i<this.elements.length;i++){
addToggle(this.elements[i]);
}
}
接下来可以做一个简单的示例验证一下效果:
html
<input type="button" id="btn1" value="显示隐藏">
<div id="div1" style="100px;height:100px;background:red;"></div>
javascript:
$d(function(){
$d('input').toggle(function(){
$d('#div1').hide();
},function(){
$d('#div1').show();
})
});
效果如下
三. attr方法
attr管样式,css管属性。所以方法设置方面没有太大区别。
//attr方法和css方法类似。
dQuery.prototype.attr=function(attr value){
if(arguments.length==2){//设置属性
var i=0
for(i=0;i<this.elements.length;i++){
this.elements[i][attr]=value;
}
}else{//获取属性
return this.elements[0][attr];
}
}
没有问题。
四.获取与查找(eq,find,index)
1.eq方法跟数组索引差不多
在jquery中,$('.class1').eq(1)
表示选取选择器的第2个对象。现在模拟jquery的取法。
在dQuery中,很自然想到用return this.elements[n];
这样的代码选取。问题在于:返回的是一个原生的js对象,因此无法调用dQuery方法。因此需要把this.elements[n]
这个原生对象转化为dQuery对象。所以——
//eq选择器
dQuery.prototype.eq=function(n){
return new dQuery(this.elements[n]);
}
这样保证了返回出来的对象是可链的。
2.find方法
find方法在jquery中是用于筛选。比如$('ul').find('li')
表示在所有ul标签下选择li,而ol标记下的li不会被选择。此方法有一个父级,find里面,可能有class类,可能有div类,不可能有id类(为什么不直接用id选择器呢?所以不予考虑了)。
因为返回的是一个较为复杂的对象——它是依赖于dQuery对象的一个数组(aResult)。因此具体做法和定义dQuery的object类似。需要分类讨论:
//find选择器
dQuery.prototype.find=function(str){
var i=0;
var aResult=[];//存放临时数据
for(i=0;i<this.elements.length;i++){
switch(str.charAt(0)){
case '.'://class类
var aEle=getByClass(this.elements[i],str.substring(1));
aResult.concat(aEle);//桥接到aResult内。
break;
default://其它标签名(TagName)
var aEle=this.elements[i].getElementsByTagName(str);
aResult.concat(aEle);
}
}
var newdQuery=new dQuery();
newdQuery.elements=aResult;
return newdQuery;//保持可链。
}
这段代码有很大问题,aResult本是一个数组,但是this.elements[i].getElementsByTagName(str)
是一个html collection,是一个长得很像数组的集合。因此concat方法不适用。——concat不能用,但是push可以用。为了达到桥接的效果,可以人工定义一个小函数来实现。所以全部带代码是
//find选择器
//定义一个小函数,两个数组(元素集合),把两个类数组(html元素集合)合并在一块。
function appendArr(arr1, arr2){
var i=0;
for(i=0;i<arr2.length;i++){
arr1.push(arr2[i]);
}
}
dQuery.prototype.find=function(str){
var i=0;
var aResult=[];//存放临时数据
for(i=0;i<this.elements.length;i++){
switch(str.charAt(0)){
case '.'://class类
var aEle=getByClass(this.elements[i],str.substring(1));
aResult.concat(aEle);//桥接到aResult内。但是
break;
default://其它标签名(TagName)
var aEle=this.elements[i].getElementsByTagName(str);
appendArr(aResult,aEle);
}
}
var newdQuery=new dQuery();
newdQuery.elements=aResult;
return newdQuery;//保持可链。
}
3.index方法
jquery的原意是——获取一组同辈元素内,一组元素的索引值。
比如
<span></span>
<div id="div1"></div>
<div></div>
<div></div>
$(funciton(){
$('div').click(function(){
alert($(this).index());
})
})
在上面的过程中,点击#div1,弹出的结果为1,而不是0(把span算进索引0了)。简单点说就是:如何求某个元素在同级中的位置。
下面实现这段代码。
首先,使用index方法的必然是一个单独的对象,而不能是一个集合或数组。所以,必须指定this.elements[0]
。
一个人,比如说王五必然是他爹的儿子,要找到他的兄弟,只能通过他爹来找。接下来可以有头到尾遍历判断,假设老王的第五个儿子名字是王五,那么就可以判断,王五的索引值为4.同理,js用parentNode和child进行判断。
//获取索引值函数
function getIndex(obj){
var aBrother=obj.parentNode.children;
var i=0;
for(i=0;i<aBrother.length;i++){
if(aBrother[i]==obj){
return i;
}
}
}
接下来要做的就是把这个封装好的函数加到原型函数内:
dQuery.prototype.index=function(){
return getIndex(this.elements[0]);
}
那么,这个方法就算完成了。为了说明这个库是实用的,接下来以一个案例说明。
【应用案例】选项卡的实现
html骨架
<div id="tab">
<ul class="list_group">
<li><a class="active" href="javascript:;">1</a></li>
<li><a href="javascript:;">2</a></li>
<li><a href="javascript:;">3</a></li>
<li><a href="javascript:;">4</a></li>
</ul>
<div class="box">
<div class="content">
<img alt="1" src="images/1.jpg">
</div>
<div class="content">
<img alt="2" src="images/2.jpg">
</div>
<div class="content">
<img alt="3" src="images/3.jpg">
</div>
<div class="content">
<img alt="4" src="images/4.jpg">
</div>
</div>
</div>
css
*{
margin:0;
padding: 0;
}
ul{
list-style: none;
}
a{text-decoration: none;}
#tab{
400px;
margin:100px auto;
}
.list_group li{
float: left;
}
.list_group li a{
display: block;
40px;
line-height: 30px;
text-align: center;
}
.box{
clear: both;
}
.content{
display: none;
}
.content img{
border: 1px solid black;
400px;height: 300px;
}
在引用了dQuery的情况下,输入下列js代码:
$d(function(){
$d('li').click(function(){
var index=$d(this).index();
$d('.content').hide();
$d('.content').eq(index).show();
$d('li').css('background-color','white');
$d(this).css('background-color','#ccc');
})
})
就成功实现了一个丑陋的选项卡。因为不是完全可链的。还有addClass等功能还有待进一步完善。
附录
dQuery代码
//可重复调用的加载函数
function myAddEvent(obj,sEv,fn){
if(obj.attachEvent){
obj.attachEvent('on'+sEv,function(){
fn.call(obj);//兼容ie
});
}else{
obj.addEventListener(sEv,fn,false);
}
}
//class选择器调用函数
function getByClass(oParent,sClass){
var aEle=oParent.getElementsByTagName('*');//选择父元素的所有元素
var aResult=[];
var re=new RegExp('\b'+sClass+'\b','i');//正则边界
var i=0;
for(i=0;i<aEle.length;i++){
if(re.test(aEle[i].className)){
aResult.push(aEle[i]);
}
}
return aResult;
}
//获取计算后的样式
function getStyle(obj,attr){
//元素,样式
if(obj.currentStyle){//兼容ie9及以下
return obj.currentStyle[attr];
}else{
return getComputedStyle(obj,false)[attr];
}
}
//定义dQuery对象
function dQuery(vArg){//参数是变体变量
this.elements=[];//选择器选择的元素扔到这个数组中
switch(typeof vArg){
//如果参数是函数
case 'function':
myAddEvent(window,'load',vArg);
break;
//如果参数是字符串
case 'string':
switch(vArg.charAt(0)){
case '#'://id选择器参数应该为#号之后的字符段
var obj=document.getElementById(vArg.substring(1));
this.elements.push(obj);
break;
case '.'://class
this.elements=getByClass(document,vArg.substring(1));
break;
default://标签
this.elements=document.getElementsByTagName(vArg);
}
break;
//如果参数是对象。
case 'object':
this.elements.push(vArg);
}
}
//定义简写
function $d(vArg){
return new dQuery(vArg);
}
//对选择器函数绑定click事件
dQuery.prototype.click=function(fn){
var i=0;
//对于返回器数组的内容
for(i=0;i<this.elements.length;i++){
myAddEvent(this.elements[i],'click',fn);
}
}
//对选择器函数绑定show/hide事件
dQuery.prototype.show=function(){
var i=0;
//对于返回器数组的内容
for(i=0;i<this.elements.length;i++){
this.elements[i].style.display='block';
}
}
dQuery.prototype.hide=function(){
var i=0;
//对于返回器数组的内容
for(i=0;i<this.elements.length;i++){
this.elements[i].style.display='none';
}
};
//hover方法
dQuery.prototype.hover=function(fnover,fnout){
var i=0;
//对于返回器数组的内容
for(i=0;i<this.elements.length;i++){
//给这个对象一次性绑定两个事件
myAddEvent(this.elements[i],'mouseover',fnover);
myAddEvent(this.elements[i],'mouseout',fnout);
}
};
//css方法
dQuery.prototype.css=function(attr,value){
if(arguments.length==2){//当参数个数为2时,使用设置css的方法
var i=0;
for(i=0;i<this.elements.length;i++){
this.elements[i].style[attr]=value;
}
}else{//只有一个参数时获取样式
return getStyle(this.elements[0],attr);
}
};
//toggle方法:
dQuery.prototype.toggle=function(){
var _arguments=arguments;//把toggle的arguments存起来,以便在其它函数中可以调用。
//私有计数器,计数器会被一组对象所享用。
function addToggle(obj){
var count=0;
myAddEvent(obj,'click',function(){
_arguments[count++%_arguments.length].call(obj);
})
}
var i=0;
for(i=0;i<this.elements.length;i++){
addToggle(this.elements[i]);
}
}
//attr方法和css方法类似。
dQuery.prototype.attr=function(attr,value){
if(arguments.length==2){//设置属性
var i=0;
for(i=0;i<this.elements.length;i++){
this.elements[i][attr]=value;
}
}else{//获取属性
return this.elements[0][attr];
}
}
//eq选择器
dQuery.prototype.eq=function(n){
return new dQuery(this.elements[n]);
}
//find选择器
//定义一个小函数,两个数组(元素集合),把两个类数组(html元素集合)合并在一块。
function appendArr(arr1, arr2){
var i=0;
for(i=0;i<arr2.length;i++){
arr1.push(arr2[i]);
}
}
dQuery.prototype.find=function(str){
var i=0;
var aResult=[];//存放临时数据
for(i=0;i<this.elements.length;i++){
switch(str.charAt(0)){
case '.'://class类
var aEle=getByClass(this.elements[i],str.substring(1));
aResult.concat(aEle);//桥接到aResult内。但是
break;
default://其它标签名(TagName)
var aEle=this.elements[i].getElementsByTagName(str);
appendArr(aResult,aEle);
}
}
var newdQuery=new dQuery();
newdQuery.elements=aResult;
return newdQuery;//保持可链。
}
//获取索引值函数
function getIndex(obj){
var aBrother=obj.parentNode.children;
var i=0;
for(i=0;i<aBrother.length;i++){
if(aBrother[i]==obj){
return i;
}
}
}
dQuery.prototype.index=function(){
return getIndex(this.elements[0]);
}