js是一门函数式语言,因为js的强大威力依赖于是否将其作为函数式语言进行使用。在js中,我们通常要大量使用函数式编程风格。函数式编程专注于:少而精、通常无副作用、将函数作为程序代码的基础构件块。
在函数式编程中,有一种函数称为匿名函数,也就是没有名称的函数,是js中的一个非常重要的概念。通常匿名函数的使用情况是,创建一个供以后使用的函数。比如将匿名函数保存在一个变量里面,或将其作为一个对象方法,更有甚者将其作为一个回调等等之类的。
//保存在变量中,通过fn去引用 var fn=function(){return true;}; //对象方法 var obj={ fn:function(){return ture;} }; //作为返回值 function method(){ return function(){return true;}; } function method2(callback){ return callback(); } //作为回调 method2(function(){return true;});
尽管匿名函数没有名称,但是其在不同的时机都是可以调用的。对于匿名函数,有一个小细节值得注意的是,将其赋值给一个变量,例如var fn=function(){},不要认为这个变量fn就是这个匿名函数的名称,fn只是一个引用该函数的变量而已,请不要误解。上一篇文章讲过,一个函数声明包含四个部分:
1、function 关键字;
2、函数名称,为可选。没有名称的函数称为匿名函数;
3、圆括号包含的一个以逗号分隔的参数列表,该参数列表可以为空,但是圆括号必须存在;
4、大括号包含的函数体。函数体可以为空,但大括号必须存在。
匿名函数没有名称,如果我们在将它赋值给变量的时候,给它声明一个名称呢,可以吗?当然可以。不过这时,这个匿名函数就变成了内联函数,这个内联函数名称只能在其自身函数中使用。有一个递归的例子就说明了这种情况:
//求和函数。sum是该内联函数名称,该内联函数名称只能在其自身函数内部使用。 var fn=function sum(n){ return n>1?sum(n-1)+n:1; } //alert(sum(10)); 这种方式使用是错误的--sum未定义 alert(fn(4));//10
当函数调用自身,或调用另外一个函数,但这个函数在调用其他函数的某个地方又调用了自己时,递归就发生了。
我们还可以使用匿名函数来进行递归调用。如:
//使用匿名函数进行递归调用 var myMath={ sum:function(n){ return n>1?this.sum(n-1)+n:1; } }; //将匿名函数加上一个函数名称,变为内联函数,这种方式定义的递归比较安全 var myMath2={ sum:function add(n){ return n>1?add(n-1)+n:1; } };
将匿名函数加上一个函数名称,变为内联函数,并使用该函数名称定义递归,这种方式定义的递归函数比直接用对象方法定义的要安全得多。因为一个进行递归调用的对象属性引用,与函数的实际名称不同,这种引用可能是暂时的,这种依赖方式会导致混乱。如:
//使用匿名函数进行递归调用 var myMath={ sum:function(n){ return n>1?this.sum(n-1)+n:1; } }; var myMath2={ sum2:myMath.sum //直接引用myMath的sum方法 }; //将myMath对象清空 myMath={}; try{ alert(myMath2.sum2(4)); }catch(e){ alert(e.message); } //结果:this.sum is not a function
为什么会出现this.sum不是一个函数的错误呢? 因为我们通过myMath2对象去调用的sum2方法,而sum2方法引用的是myMath的sum方法,在sum方法内部的this现在指向的是myMath2对象,而myMath2对象没有sum方法。记住,当一个函数作为方法被调用时,函数上下文,也就是this参数,指的是调用该方法的对象。如果对this参数还有不明白的地方,请去看上一篇文章javascript进阶笔记(1) 。在上面的代码中,我们使用内联函数的名称进行递归是很安全的,虽然myMath对象已经被清空了,但是myMath的sum方法依然可以被访问,这是闭包的作用。
尽管可以给内联函数进行命名,但这些名称只能在函数自身内部才是可见的。也就是说,内联函数的名称,它们的作用域仅限于声明它们的函数。
js中的函数与其他语言中的函数不同,js赋予了函数很多特性,其中最重要的特性之一就是将函数作为第一型对象。函数可以有属性,也可以有方法,可以分配给变量和属性,也可以享有所有普通对象所拥有的特性,而且还有一个超级特性,它们可以被调用!!!这句话非常重要!我们可以给js中的函数设置某些属性,让函数拥有状态和缓存记忆。如下代码:
//给函数设置id属性 var store={ nextId:1, cache:{}, add:function(fn){ if(!fn.id) fn.id=this.nextId++; return !!(this.cache[fn.id]=fn); } };
//素数判断 //如果该函数的属性缓存中有该素数,则从缓存在取,否则就需要进行计算后将结果存于缓存中,并将结果返回。 function isPrime(value){ if(!isPrime.result) isPrime.result={}; if(isPrime.result[value]!=null) return isPrime.result[value]; var prime=value!=1; for(var i=0;i<value;i++){ if(value%i==0){ prime=false; break; } } return isPrime.result[value]=prime; }
缓存记忆有两个优点:
1、享有性能优势。直接从以前计算的结果集合中取出结果,如果没有,就计算后将结果保存在缓存中;
2、发生在幕后,无需用户或开发人员做任何特殊操作或为此做任何额外的初始化工作。
缺点:
1、牺牲内存。
接下来我们实现一个伪数组对象,不错,就是伪造一个数组。代码如下:
var elems={ length:0,//实现一个数组必须的属性,集合中元素的个数 add:function(elem){ Array.prototype.push.call(this,elem); }, gather:function(elem){ this.add(elem); }, delete:function(){ Array.prototype.pop.call(this); } };
本来Array这个构造器的原型对象prototype就有关于操作数组的方法,我们可以直接拿来用,不要觉得不要意思,都是自家东西,哈哈!Array.prototype.push方法是通过其函数上下文操作自身数组的。每次调用push方法,程序将会增加length的属性值,然后给对象添加一个数字属性,并将其引用到传入的元素上。
在js中,在函数的实际调用中,我们可以给函数传递一个可变的实际参数列表,也就是说,js灵活且强大的特性之一就是函数可以接受任意数量的参数。我们通过Math对象的max和min方法来举例。如下代码:
var myMath={ //求数组中的最大值 array数组的元素个数是可变的 max:function(array){ return Math.max.apply(Math,array); }, //求数组中的最小值 array数组的元素个数是可变的 min:function(array){ return Math.min.apply(Math,array); } };
var maxValue=myMath.max([3,2,5,6,1]);//6
maxValue=myMath.max([5,8]);//8
因为js中没有针对数组求最大值和最小值的方法,因此我们可以自己定义。举此例的目的,是为了接下来引入函数重载的概念。
js不像其他语言一样,可以进行函数的重载。在js中,没有函数重载的方式。那么在js中还能进行函数的重载吗?当然可以!不过需要绕一个弯。
所有的函数都有一个length属性,这个属性等于该函数在声明时需要传入形参的个数。请不要将函数的length属性与arguments的length属性混淆,arguments的length属性是实际传入参数的个数。
因此,对于一个函数,在参数方面,我们可以确定两件事情:
1、通过函数的length属性,可以知道该函数声明了多少个形参;
2、通过arguments.length,可以知道函数在调用时传入了多少个实参。
我们可以利用以上参数个数的差异化来实现函数的重载。
首先,来看通过传入参数的个数去执行不同的操作。如果想要冗长且完整的函数,可以像如下定义重载:
var persons={ find:function(){ switch(arguments.length){ case 0: /*do something*/ break; case 1: /*do something*/ break; case 2: /*do something*/ break; default: /*do something*/ break; } } };
在这种方式中,通过arguments参数获取实际传入的参数个数进行判断,每一种情况都会执行不同的操作。但是这种判断方式不是很整洁,也比较冗长。当然,有的时候也需要做这样的操作。
在此,我们可以换一种思路,通过一个addMethod函数来给persons对象重载find方法。代码如下:
function addMethod(object,methodName,fn){ //将旧方法保存下来,在old.apply那一步会依次循环 匹配实参与形参的个数是否相等。循环最重要的事情是 每调用一次addMethod方法,就会将fn和old变量保存在当前对应的闭包中 var old=object[methodName]; //重载该对象的方法 object[methodName]=function(){ //如果该匿名函数的形参个数和实参个数匹配,就调用该函数 if(fn.length==arguments.length) return fn.apply(this,arguments); //如果传入的参数不匹配,则调用原有参数的方法 else if(typeof old =='function') return old.apply(this,arguments); }; } var persons={}; //给persons对象创建一个find方法 addMethod(persons,"find",function(){ //为了方便调试,就直接返回形参的个数, return 0; }); //给persons对象重载一个find方法 addMethod(persons,"find",function(name){ return 1; }); //给persons对象重载一个find方法 addMethod(persons,"find",function(first,last){ return 2; }); //接下来调用persons的find方法 alert(persons.find());//0 alert(persons.find('sj'));//1 alert(persons.find(1,4));//2
看一下浏览器对persons的find方法的解析:
这是一个绝佳的技巧,因为这些绑定函数实际上并没有存储于任何典型的数据结构中,而是在闭包里作为引用进行存储。
在使用这种特定技巧的时候,需要注意以下几点:
1、这种重载方式只适用于不同数量的参数,但并不区分类型、参数名称或其他东西;
2、这种重载方式会有一些函数调用的开销,我们要考虑在高性能时的情况。