原文地址:
在一个群上看到好几次问到call和apply的作用,function这两个方法的效果大家都很容易理解,但一般很难让人深刻地理解使用它们的时机。
call和apply都有一个功能:改变函数的上下文,也就是在调用函数的同时,改变函数内部this的指向的对象。apply还可以向函数传递参数。如果一个函数的调用必须给定相应的参数,则只能够用apply方法。下面通过编写一个JS组件来说明这两个方法在什么时机下使用,主要用在事件处理上。在制作表单时,常常需要让用户输入一定范围内的数据,超出这个范围的数据视为非法。如人的年龄,世界上没有一个人的年龄为-1岁。如果采用下列列表让用户输入,列表可能太长而影响用户使用体验。我们可以使用一个文本框,让用户输入数据,然后验证。由于这种情况很常见,那么为用JS来编写一个组件,把一个文本框封装起来,实现验证逻辑,提高代码的可重用性。完整的代码如下://为Array类添加两个新方法Array.prototype.Add = function(item){ for(var i=0;i凡是私有的方法和成员我都以下划线(_)作为变量名的开头,使用类时,不应使用这些接口,否则会出现不正确的结果。现在让我们一点点分析代码:首先是两个数组扩展函数max) throw new Error('min不能大于max'); this._dom = null; //组件的DOM对象 this._invalidTypeHandler = []; //数据类型不正确时调用的函数数组 this._overflowHandler = []; //数据超出范围时调用的函数数组 this._min = min; //最小值 this._max = max; //最大值 this._init(); //初始化}NumTextBox.prototype = { getValue : function(){ //获取当前值 return this._dom.value*1; }, setValue : function(value){ //设置当前值 if (isNaN(value)) throw new Error('参数value必须为数字'); value = value*1; if (value this._max) throw new Error('数据不合法');//数据不合法 this._dom.value = value; }, getMin : function(){ //获取最小值 return this._min; }, setMin : function(value){//设置最小值 if (isNaN(value) || value*1 > this._max) throw new Error('参数value不是数字或大于max'); this._min = value*1; }, getMax : function(){//获取最大值 return this._max; }, setMax : function(value){//设置最大值 if (isNaN(value) || value*1 < this._min) throw new Error('参数value不是数字或大于max'); this._max = value; }, AddInvalidTypeEventHandler : function(handler){//添加非法数据类型处理函数 if (typeof(handler)!='function') throw new Error('参数handler必须为函数'); this._invalidTypeHandler.Add(handler); }, RemoveInvalidTypeEventHandler : function(handler){//移除非法数据类型处理函数 if (typeof(handler)!='function') throw new Error('参数handler必须为函数'); this._invalidTypeHandler.Remove(handler); }, AddOverflowEventHandler : function(handler){//添加溢出处理函数 if (typeof(handler)!='function') throw new Error('参数handler必须为函数'); this._overflowHandler.Add(handler); }, RemoveOverflowEventHandler : function(handler){//移除溢出处理函数 if (typeof(handler)!='function') throw new Error('参数handler必须为函数'); this._overflowHandler.Remove(handler); }, _raiseInvalidTypeEvent : function(ext){//触发非法数据类型事件 var cancel = false; for(var i=0;i =oNumTextBox._min && value <=oNumTextBox._max)) oNumTextBox._raiseOverflowEvent.apply(oNumTextBox,[ext]); }, _init : function(controlId,min,max){ //创建DOM this._dom = document.createElement('input'); this._dom.type = 'text'; this._dom.id = controlId; this._dom.name = controlId; this._dom.oNumTextBox = this; this._dom.onblur = this._checkValue;//事件绑定 document.body.appendChild(this._dom);//放入网页中 }}function invalid1(ext){ alert("valid1\n,当前值为"+this.getValue());}function invalid2(ext){ alert("valid2,测试多处理函数和保留焦点"); return true;}function overflow(ext){ alert("输入的值必须在"+this.getMin()+"和"+this.getMax()+"之间"); return true;}var test = new NumTextBox("test",18,60);test.AddInvalidTypeEventHandler(invalid1);//添加非法数据类型事件处理函数test.AddInvalidTypeEventHandler(invalid2);test.AddOverflowEventHandler(overflow);//添加溢出事件处理函数
Array.prototype.Add = function(item){ for(var i=0;i我们为Array类添加两个方法,这样做是为了方便后面操作数组.Add方法检查数组是含有item,如果有,什么都不操作,若没有,则添加item到数组中。Remove方法检查数组是否含有item,若有则从数组删除item,没有则什么都不做。这两个方法实际上是实现集合的添加元素和删除元素的操作,要保证集合中元素的唯一性。对JS内置类的扩展在许多JS库中都有,如ASP.NET Ajax,对JS内置类进行丰富的扩展,开发起来极其方便和高效率。接着看看我们的主角:NumTextBox类,它的构造器如下:
function NumTextBox(controlId,min,max){ if (!controlId || typeof(controlId)!='string') throw new Error('参数controlId为空或不是字符串类型'); if (isNaN(min)) throw new Error('参数min必须为数字'); if (isNaN(max)) throw new Error('参数max必须为数字'); min = min*1; //如果 min = '123',转化为数字类型 max = max*1; if (min>max) throw new Error('min不能大于max'); this._dom = null; //组件的DOM对象 this._invalidTypeHandler = []; //数据类型不正确时调用的函数数组 this._overflowHandler = []; //数据超出范围时调用的函数数组 this._min = min; //最小值 this._max = max; //最大值 this._init(); //初始化}
controlId为组件的标识ID,待会会看到它的作用。min和max分别赋于最大值和最小值。构造器会对参数的数据合法性进行判断,如果不合法会抛出错误。
接着看看这个类的原型(prototype)的内容。getValue : function(){ //获取当前值 return this._dom.value*1; }, setValue : function(value){ //设置当前值 if (isNaN(value)) throw new Error('参数value必须为数字'); value = value*1; if (value
this._max) throw new Error('数据不合法');//数据不合法 this._dom.value = value; }, getMin : function(){ //获取最小值 return this._min; }, setMin : function(value){//设置最小值 if (isNaN(value) || value*1 > this._max) throw new Error('参数value不是数字或大于max'); this._min = value*1; }, getMax : function(){//获取最大值 return this._max; }, setMax : function(value){//设置最大值 if (isNaN(value) || value*1 < this._min) throw new Error('参数value不是数字或大于max'); this._max = value; },
复制代码
对于getValue, setValue, getMin, setMin, getMax, setMax这样的方法实际上是提供属性。因为JS不支持属性,所以我采用这样的命名方式。在ASP.NET Ajax中,也采用类似的方式。我们也可以直接调用类的 _min,_max等字段进行赋值,但这样做就不能保证数据的合法性,通过方法来赋值可以先检验数据,这也就是属性的本质作用。接着看看AddInvalidTypeEventHandler : function(handler){//添加非法数据类型处理函数 if (typeof(handler)!='function') throw new Error('参数handler必须为函数'); this._invalidTypeHandler.Add(handler); }, RemoveInvalidTypeEventHandler : function(handler){//移除非法数据类型处理函数 if (typeof(handler)!='function') throw new Error('参数handler必须为函数'); this._invalidTypeHandler.Remove(handler); }, AddOverflowEventHandler : function(handler){//添加溢出处理函数 if (typeof(handler)!='function') throw new Error('参数handler必须为函数'); this._overflowHandler.Add(handler); }, RemoveOverflowEventHandler : function(handler){//移除溢出处理函数 if (typeof(handler)!='function') throw new Error('参数handler必须为函数'); this._overflowHandler.Remove(handler); },
复制代码
_init : function(controlId,min,max){ //创建DOM this._dom = document.createElement('input'); this._dom.type = 'text'; this._dom.id = controlId; this._dom.name = controlId; this._dom.oNumTextBox = this; this._dom.onblur = this._checkValue;//事件绑定 document.body.appendChild(this._dom);//放入网页中 }我想除了倒数第二、三句: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怎么写的。
_checkValue : function(ext){//检查数据 ext = ext ? ext : window.event; var oNumTextBox = this.oNumTextBox; if (this.value == "") oNumTextBox._raiseInvalidTypeEvent.apply(oNumTextBox,[ext]); return; } var value = this.value*1; if (!(value>=oNumTextBox._min && value <=oNumTextBox._max)) oNumTextBox._raiseOverflowEvent.apply(oNumTextBox,[ext]); }, return; if (isNaN(this.value)){ oNumTextBox._raiseInvalidTypeEvent.apply(oNumTextBox,[ext]); return; } var value = this.value*1; if (!(value>=oNumTextBox._min && value <=oNumTextBox._max)) oNumTextBox._raiseOverflowEvent.apply(oNumTextBox,[ext]); },
第一句大家都很明白,旨在消灭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_raiseInvalidTypeEvent : function(ext){//触发非法数据类型事件 var cancel = false; for(var i=0;i
其实这两个类功能一样。它们都是遍历整个函数指针数组,然后,逐个调用。这里才是apply真正的用法。
我们为什么要把类对象作为函数上下文呢?因为使用这个类的人,在外部可以通过this来访问类的方法来获取组件的接口(属性)。他们往往不在乎这个组件的其他信息(如构成这个组件的HTML代码),他们只想要知道用户输入的值。更何况这个值已转化成数字,而不是原来的字符串,那么就更方便他们的使用。
我们还要留意那个cancel变量,它是处理函数的返回值。要注意它是最后一个处理函数的返回值,因为后一个处理函数的返回值会覆盖前一个处理函数的访回值。这个cancel是根据处理函数返回的值来决定是否让文本框获得焦点。如果为true,则获得焦点,实际上是不让用户转移,直到他输入一个合法数据为止。如果为false,则用户可能留下一个非法的数据。
那看看如何使用这个类:
首先是声明三个处理函数:
function invalid1(ext){ alert("valid2,测试多处理函数和保留焦点"); return true; //保留焦点}function overflow(ext){ alert("输入的值必须在"+this.getMin()+"和"+this.getMax()+"之间"); return true;} alert("valid1\n当前值为:"+this.getValue()); //调用了类对象的getValue方法}function invalid2(ext){ alert("valid2,测试多处理函数和保留焦点"); return true; //保留焦点}function overflow(ext){ alert("输入的值必须在"+this.getMin()+"和"+this.getMax()+"之间"); return true;}
然后是实例化NumTextBox类
var test = new NumTextBox("test",18,60);接着是把处理函数绑定到类的事件中:
test.AddInvalidTypeEventHandler(invalid1);//添加非法数据类型事件处理函数test.AddInvalidTypeEventHandler(invalid2);test.AddOverflowEventHandler(overflow);//添加溢出事件处理函数
这样就可以了。实际的使用就是如此简单。可重用性大大增强