编写一个JS组件来说说call和apply的用法

原文地址:

    http://bbs.blueidea.com/thread-2779716-1-1.html

在一个群上看到好几次问到call和apply的作用,function这两个方法的效果大家都很容易理解,但一般很难让人深刻地理解使用它们的时机。
call和apply都有一个功能:改变函数的上下文,也就是在调用函数的同时,改变函数内部this的指向的对象。apply还可以向函数传递参数。如果一个函数的调用必须给定相应的参数,则只能够用apply方法。

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

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

完整的代码如下:

//为Array类添加两个新方法
Array.prototype.Add = function(item){
	for(var i=0;i<this.length;i++)
		if (this[i]==item)
			return;
	this.push(item);
}

Array.prototype.Remove = function(item){
	for(var i=0;i<this.length;i++)
		if (this[i]==item){
			this.splice(i,1);
			break;
		}
}

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(); //初始化
}
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._min || 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<this._invalidTypeHandler.length;i++)
			cancel = this._invalidTypeHandler[i].apply(this,[ext]);
		if(cancel)
			this._dom.focus(); //获得焦点
	},
	_raiseOverflowEvent : function(ext){//触发溢出事件
		var cancel = false;
		for(var i=0;i<this._overflowHandler.length;i++)
			cancel = this._overflowHandler[i].apply(this,[ext]);
		if (cancel)
			this._dom.focus();
	},
	_checkValue : function(ext){//检查数据
		ext = ext ? ext : window.event;
		var oNumTextBox = this.oNumTextBox;
		if (this.value == "")
			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]);
	},
	_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<this.length;i++)
                if (this[i]==item)
                        return;
        this.push(item);
}
Array.prototype.Remove = function(item){
        for(var i=0;i<this.length;i++)
                if (this[i]==item){
                        this.splice(i,1);
                        break;
                }
}


我们为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)的内容。

  1.         getValue : function(){ //获取当前值
                    return this._dom.value*1;
            },
            setValue : function(value){ //设置当前值
                    if (isNaN(value))
                            throw new Error('参数value必须为数字');
                    value = value*1;
                    if (value<this._min || 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);
        },
  • 复制代码


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

然后我们看看_init的内部方法:

        _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<this._invalidTypeHandler.length;i++)
                        cancel = this._invalidTypeHandler[i].apply(this,[ext]);
                if(cancel)
                        this._dom.focus(); //获得焦点
        },
        _raiseOverflowEvent : function(ext){//触发溢出事件
                var cancel = false;
                for(var i=0;i<this._overflowHandler.length;i++)
                        cancel = this._overflowHandler[i].apply(this,[ext]);
                if (cancel)
                        this._dom.focus();
        },

其实这两个类功能一样。它们都是遍历整个函数指针数组,然后,逐个调用。这里才是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);//添加溢出事件处理函数

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


你可能感兴趣的:(编写一个JS组件来说说call和apply的用法)