请选择 进入手机版 | 继续访问电脑版
收藏本站腾讯微博新浪微博
点点网模板设计大赛 phpchina

经典论坛

 找回密码
 注册

QQ登录

只需一步,快速开始

蓝色理想 最新研发动态 用悬赏 三天解决问题 解决访问速度慢 论坛支持农历生日 - 给官方提建议

论坛活动及任务 归纳网站最新活动 地图任务 邮件更新任务:保护帐号安全

积分换实物,来参加蓝色理想积分兑换吧! 联系招聘客服 蓝色理想帮你找工作! 万元奖励等你拿——点点网模板设计大赛

查看: 2672|回复: 0

编写一个JS组件来说说call和apply的用法 [复制链接]

ComPhilip 楼主
帖子
213
体力
715
威望
4
发表于 2007-8-13 02:04:46 |显示全部楼层
在一个群上看到好几次问到call和apply的作用,function这两个方法的效果大家都很容易理解,但一般很难让人深刻地理解使用它们的时机。
call和apply都有一个功能:改变函数的上下文,也就是在调用函数的同时,改变函数内部this的指向的对象。apply还可以向函数传递参数。如果一个函数的调用必须给定相应的参数,则只能够用apply方法。

下面通过编写一个JS组件来说明这两个方法在什么时机下使用,主要用在事件处理上。

在制作表单时,常常需要让用户输入一定范围内的数据,超出这个范围的数据视为非法。如人的年龄,世界上没有一个人的年龄为-1岁。如果采用下列列表让用户输入,列表可能太长而影响用户使用体验。我们可以使用一个文本框,让用户输入数据,然后验证。由于这种情况很常见,那么为用JS来编写一个组件,把一个文本框封装起来,实现验证逻辑,提高代码的可重用性。

完整的代码如下:

 提示:您可以先修改部分代码再运行



凡是私有的方法和成员我都以下划线(_)作为变量名的开头,使用类时,不应使用这些接口,否则会出现不正确的结果。

现在让我们一点点分析代码:首先是两个数组扩展函数
  1. Array.prototype.Add = function(item){
  2.         for(var i=0;i<this.length;i++)
  3.                 if (this[i]==item)
  4.                         return;
  5.         this.push(item);
  6. }
  7. Array.prototype.Remove = function(item){
  8.         for(var i=0;i<this.length;i++)
  9.                 if (this[i]==item){
  10.                         this.splice(i,1);
  11.                         break;
  12.                 }
  13. }
复制代码

我们为Array类添加两个方法,这样做是为了方便后面操作数组.Add方法检查数组是含有item,如果有,什么都不操作,若没有,则添加item到数组中。Remove方法检查数组是否含有item,若有则从数组删除item,没有则什么都不做。这两个方法实际上是实现集合的添加元素和删除元素的操作,要保证集合中元素的唯一性。
对JS内置类的扩展在许多JS库中都有,如ASP.NET Ajax,对JS内置类进行丰富的扩展,开发起来极其方便和高效率。

接着看看我们的主角:NumTextBox类,它的构造器如下:
  1. function NumTextBox(controlId,min,max){
  2.         if (!controlId || typeof(controlId)!='string')
  3.                 throw new Error('参数controlId为空或不是字符串类型');
  4.         if (isNaN(min))
  5.                 throw new Error('参数min必须为数字');
  6.         if (isNaN(max))
  7.                 throw new Error('参数max必须为数字');
  8.         min = min*1; //如果 min = '123',转化为数字类型
  9.         max = max*1;
  10.         if (min>max)
  11.                 throw new Error('min不能大于max');
  12.         this._dom = null; //组件的DOM对象
  13.         this._invalidTypeHandler = []; //数据类型不正确时调用的函数数组
  14.         this._overflowHandler = []; //数据超出范围时调用的函数数组
  15.         this._min = min; //最小值
  16.         this._max = max; //最大值
  17.         this._init(); //初始化
  18. }
复制代码

controlId为组件的标识ID,待会会看到它的作用。min和max分别赋于最大值和最小值。构造器会对参数的数据合法性进行判断,如果不合法会抛出错误。
接着看看这个类的原型(prototype)的内容。

  1.         getValue : function(){ //获取当前值
  2.                 return this._dom.value*1;
  3.         },
  4.         setValue : function(value){ //设置当前值
  5.                 if (isNaN(value))
  6.                         throw new Error('参数value必须为数字');
  7.                 value = value*1;
  8.                 if (value<this._min || value>this._max)
  9.                         throw new Error('数据不合法');//数据不合法
  10.                 this._dom.value = value;
  11.         },
  12.         getMin : function(){ //获取最小值
  13.                 return this._min;
  14.         },
  15.         setMin : function(value){//设置最小值
  16.                 if (isNaN(value) || value*1 > this._max)
  17.                         throw new Error('参数value不是数字或大于max');
  18.                 this._min = value*1;
  19.         },
  20.         getMax : function(){//获取最大值
  21.                 return this._max;
  22.         },
  23.         setMax : function(value){//设置最大值
  24.                 if (isNaN(value) || value*1 < this._min)
  25.                         throw new Error('参数value不是数字或大于max');
  26.                 this._max = value;
  27.         },
复制代码

对于getValue, setValue, getMin, setMin, getMax, setMax这样的方法实际上是提供属性。因为JS不支持属性,所以我采用这样的命名方式。在ASP.NET Ajax中,也采用类似的方式。我们也可以直接调用类的 _min,_max等字段进行赋值,但这样做就不能保证数据的合法性,通过方法来赋值可以先检验数据,这也就是属性的本质作用。

接着看看
  1.         AddInvalidTypeEventHandler : function(handler){//添加非法数据类型处理函数
  2.                 if (typeof(handler)!='function')
  3.                         throw new Error('参数handler必须为函数');
  4.                 this._invalidTypeHandler.Add(handler);
  5.         },
  6.         RemoveInvalidTypeEventHandler : function(handler){//移除非法数据类型处理函数
  7.                 if (typeof(handler)!='function')
  8.                         throw new Error('参数handler必须为函数');
  9.                 this._invalidTypeHandler.Remove(handler);
  10.         },
  11.         AddOverflowEventHandler : function(handler){//添加溢出处理函数
  12.                 if (typeof(handler)!='function')
  13.                         throw new Error('参数handler必须为函数');
  14.                 this._overflowHandler.Add(handler);
  15.         },
  16.         RemoveOverflowEventHandler : function(handler){//移除溢出处理函数
  17.                 if (typeof(handler)!='function')
  18.                         throw new Error('参数handler必须为函数');
  19.                 this._overflowHandler.Remove(handler);
  20.         },
复制代码

这4个函数其实是对外提供两个事件:InvalidType和Overflow。现在对Array类进行扩展的两个方法:Add和Remove在这里用上了。由于IE不完美支持DOM事件模型,我们只好自己实现。
我们可以把多个函数绑定到一个事件中,而不用将代码全部挤在一个函数体内。通过Add***,我们可以往事件添加处理函数,当事件发生时,会逐个调用这些函数。通过Remove***,我们可以移除这些处理函数。在这里,造器中的两个数组——_invalidTypeHandler 和 _overflowHandler——是用来存放函数指针(function是一个对象,它是一个引用类型)的。

然后我们看看_init的内部方法:
  1.         _init : function(controlId,min,max){ //创建DOM
  2.                 this._dom = document.createElement('input');
  3.                 this._dom.type = 'text';
  4.                 this._dom.id = controlId;
  5.                 this._dom.name = controlId;
  6.                 this._dom.oNumTextBox = this;
  7.                 this._dom.onblur = this._checkValue;//事件绑定
  8.                 document.body.appendChild(this._dom);//放入网页中
  9.         }
复制代码

我想除了倒数第二、三句:this._dom.oNumTextBox = this;this._dom.onblur = this._checkValue,大家都明白其他语句的作用。这个方法是创建一个文本框,设定id和name属性,构造器的controlId参数赋给id属性,其他JS代码可通过document.getElementById来获得这个文本框。
this._dom.oNumTextBox = this; 这条代码是将NumTextBox类的一个实例对象附加到新创建的文本框的oNumTextBox属性中,之所以添加这个属性,待会说明。this._dom.onblur是绑定文本框失去焦点的事件,处理函数为NumTextBox的_checkValue方法。也就是说,我们封装验证代码的入口就在这里。通过把_checkValue绑定到文本框的事件上,我们可以搞许多花样。看看_checkValue怎么写的。

  1.         _checkValue : function(ext){//检查数据
  2.                 ext = ext ? ext : window.event;
  3.                 var oNumTextBox = this.oNumTextBox;
  4.                 if (this.value == "")
  5.                         return;
  6.                 if (isNaN(this.value)){
  7.                         oNumTextBox._raiseInvalidTypeEvent.apply(oNumTextBox,[ext]);
  8.                         return;
  9.                 }
  10.                 var value = this.value*1;
  11.                 if (!(value>=oNumTextBox._min && value <=oNumTextBox._max))
  12.                         oNumTextBox._raiseOverflowEvent.apply(oNumTextBox,[ext]);
  13.         },
复制代码

第一句大家都很明白,旨在消灭IE和Gecko核心在事件模型的差异。大家要特别注意这个函数中this的指向。在类模型的其他方法中,this指向类实例,但这里的this却是指向类创建的文本框对象(id为controlId的文本框)?为什么呢?因为这个_checkValue函数在绑定文本框的onblur事件,当文本框失去焦点时,浏览器会调用这个_checkValue,并把它的函数上下文(this)改为触发事件的html对象(也就是文本框)。所以this指向文本框。我们把许多的逻辑放到那个类对象上,那么如何找到那个对象呢?在_init中我们把类对象附加在文本框的oNumTextBox属性中,那么我们就可以通过this.oNumTextBox来获取。如_checkVallue的第二行代码所示。接着开始验证数据。
如果没有数据,则忽略(一个return直接退出).如果数据不是合法的数值,如"asdf",则调用触发InvalidType事件。所谓的触发,触发的动作就是调用_raiseInvalidTypeEvent的方法。朋友们,先暂时一下,在讲apply之前,说说事件的顺序。调用触发invalidType事件的函数后,我们直接return。这说明了两件事,invalidType事件和Overflow事件只能同时发生其中一个,而且InvaidType先发生。看看上面的代码是如何实现的。

现在我们说说这里为什么要用apply。
其实这里大可以不用apply,因为我们可以通过修改_raiseInvalidTypeEvent和_raiseOverflowEvent方法的声明,使它有两个参数,分别传递类对象和ext对象。有这两个对象,它们完全可以完成任务(什么任务,等一下说)。但我为什么要在这里apply改变上下文呢。其实是为了缩小this指向非类对象的范围。通过apply,我们立刻把函数的上下文改为类对象,使指向非类对象的this仅仅存在于_checkValue。这样,对于自己以后的修改,检查错误,提供了方便。那么为什么要用apply传递ext呢?使用call抛弃它不行吗?——这个当然行啦,我之所以用apply是为了保留额外的事件信息,使用类的人可能用到也说不定。

现在看看_raiseInvalidTypeEvent和_raiseOverfowEvent
  1.         _raiseInvalidTypeEvent : function(ext){//触发非法数据类型事件
  2.                 var cancel = false;
  3.                 for(var i=0;i<this._invalidTypeHandler.length;i++)
  4.                         cancel = this._invalidTypeHandler[i].apply(this,[ext]);
  5.                 if(cancel)
  6.                         this._dom.focus(); //获得焦点
  7.         },
  8.         _raiseOverflowEvent : function(ext){//触发溢出事件
  9.                 var cancel = false;
  10.                 for(var i=0;i<this._overflowHandler.length;i++)
  11.                         cancel = this._overflowHandler[i].apply(this,[ext]);
  12.                 if (cancel)
  13.                         this._dom.focus();
  14.         },
复制代码

其实这两个类功能一样。它们都是遍历整个函数指针数组,然后,逐个调用。这里才是apply真正的用法。
我们为什么要把类对象作为函数上下文呢?因为使用这个类的人,在外部可以通过this来访问类的方法来获取组件的接口(属性)。他们往往不在乎这个组件的其他信息(如构成这个组件的HTML代码),他们只想要知道用户输入的值。更何况这个值已转化成数字,而不是原来的字符串,那么就更方便他们的使用。
我们还要留意那个cancel变量,它是处理函数的返回值。要注意它是最后一个处理函数的返回值,因为后一个处理函数的返回值会覆盖前一个处理函数的访回值。这个cancel是根据处理函数返回的值来决定是否让文本框获得焦点。如果为true,则获得焦点,实际上是不让用户转移,直到他输入一个合法数据为止。如果为false,则用户可能留下一个非法的数据。

那看看如何使用这个类:
首先是声明三个处理函数:
  1. function invalid1(ext){
  2.         alert("valid1\n当前值为:"+this.getValue()); //调用了类对象的getValue方法
  3. }
  4. function invalid2(ext){
  5.         alert("valid2,测试多处理函数和保留焦点");
  6.         return true; //保留焦点
  7. }
  8. function overflow(ext){
  9.         alert("输入的值必须在"+this.getMin()+"和"+this.getMax()+"之间");
  10.         return true;
  11. }
复制代码

然后是实例化NumTextBox类
  1. var test = new NumTextBox("test",18,60);
复制代码

接着是把处理函数绑定到类的事件中:
  1. test.AddInvalidTypeEventHandler(invalid1);//添加非法数据类型事件处理函数
  2. test.AddInvalidTypeEventHandler(invalid2);
  3. test.AddOverflowEventHandler(overflow);//添加溢出事件处理函数
复制代码

这样就可以了。实际的使用就是如此简单。可重用性大大增强

[ 本帖最后由 ComPhilip 于 2007-8-13 02:22 编辑 ]
已有 1 人评分威望 收起 理由
Sheneyan + 4 精品文章

总评分: 威望 + 4   查看全部评分

西部数码顶级域名注册商39元抢注!
您需要登录后才可以回帖 登录 | 注册

Archiver|手机版|安久科技提供CDN|blueidea.com ( 京ICP备05002321号 )  

GMT+8, 2012-2-13 09:18 , Processed in 0.072096 second(s), 11 queries , Gzip On, Memcache On.

Powered by Discuz! X2

© 2001-2011 Comsenz Inc.

回顶部