从名字便可看书,此篇博客总结与《JavaScript忍者秘籍》。对于JavaScript来讲,函数为第一类型对象。因此这里,咱们主要是介绍JavaScript中函数的运用。javascript
对于什么是匿名函数,这里就不作过多介绍了。咱们须要知道的是,对于JavaScript而言,匿名函数是一个很重要且具备逻辑性的特性。一般,匿名函数的使用状况是:建立一个供之后使用的函数。前端
简单的举个例子以下:java
window.onload = function() { alert('hello'); } var templateObj = { shout:function() { alert('做为方法的匿名函数') } } templateObj.shout(); setTimeout(function() { alert('这也是一个匿名函数'); },1000)
上面的一个代码片断我就不作过多无用解释了,比较常规。node
递归,说白了,就是本身调用本身,或者调用另一个函数,可是这个函数的调用树的某一个地方又调用了本身。因此递归,就产生了。git
拿普通命名函数的递归最好的举例就是用最简单的递归需求:检测回文。github
回文的定义以下:一个短语,无论从哪个方向读,都是同样的。检测的工做固然方法多样,咱们能够建立一个函数,用待检测的回文字符逆序生成出一个字符,而后检测两者是否相同,若是相同,则为回文字符。算法
可是这种方法并非颇有逼格,确切的说,代价比较大,由于咱们须要分配并建立新的字符。数组
因此,咱们能够整理出以下简洁的办法:缓存
单个和零个字符都是回文
若是字符串的第一个字符和最后一个字符相同,而且除了两个字符之外,别的字符也知足该要求,那么咱们就能够检测出来了这个是回文了
function isPalindrome(txt) { if(txt.length<=1){ return true; } if(txt.charAt(0)!= txt.charAt(txt.length-1)) return false; return isPalindrome(txt.substr(1,txt.length-2)); }
上面的代码咱们并无作txt的一些类型检测,undefined、null等。
所谓的方法,天然离不开对象,直接看例子:
var ninja = { chirp:function(n) { return n>1?ninja.chirp(n-1)+'-chirp':'chirp'; } } console.log(ninja.chirp(3))//chirp-chirp-chirp
在上述代码中,咱们经过对象ninja.chirp方法的递归调用了本身。可是,由于咱们在函数上s会用了非直接引用,也就是ninja对象的chirp属性,因此才可以实现递归,这也就引出来一个问题:引用丢失
上面的示例代码,依赖于一个进行递归调用的对象属性引用。与函数的实际名称不一样,由于这种引用多是暂时的。
var ninja = { chirp:function(n) { return n>1?ninja.chirp(n-1)+'-chirp':'chirp'; } } var samurai = {chirp:ninja.chirp}; ninja = {}; try{ console.log(samurai.chirp(3) === 'chirp-chirp-chirp') }catch (err){ if(err) alert(false); }
如上,咱们把ninja属性上的方法赋值给了samurai,而后置空ninja,而后你懂得~这就是引用丢失的问题。
截图自《JavaScript忍者秘籍》
经过完善以前对匿名函数的粗略定义,咱们能够修复解决这个问题。在匿名函数中,咱们不在使用显示的ninja引用。这里咱们使用this(关于this的使用详解,请关注个人我的微信公众号:前端的全栈之路)。
var ninja = { chirp:function(n) { return n>1?this.chirp(n-1)+'-chirp':'chirp'; } }
当函数做为方法被调用的时候,函数的上下文指的是该方法的对象。
使用this调用,可让咱们的匿名函数更加的强大且灵活。可是。。。
上面咱们解决了做为函数方法做为递归时候的一个完美操做。但实际上,无论是否进行方法递归,巧妙使用this都是咱们应该所掌握的(关注微信公众号,迟早都给你说到)。
话说回来,其实这样写也仍是有问题的,问题在于给对象定义方法的时候,方法名称是写死的,若是属性名称不同,岂不是同样会丢失引用?
因此,这里咱们采用另外一种解决方案,给匿名函数起个名字吧!对的,确定又人会说,我擦!那仍是匿名函数么?嗯。。。好吧,那就不叫匿名函数了吧,叫内联函数~
var ninja = { chirp:function signal(n) { return n>1?signal(n-1)+'-chirp':'chirp'; } } var samurai = {chirps:ninja.chirp}; ninja = {}; try{ console.log(samurai.chirps(3) === 'chirp-chirp-chirp') }catch (err){ if(err) alert(false); }
因此如上的解决办法,就完美解决了咱们以前说到全部问题。内联函数还有一个很重要的一点,就是尽管能够给内联函数进行命名,可是这些名称只能在自身函数内部才可见。
JavaScript中的函数和其余语言中的函数有所不一样,JavaScript赋予了函数不少的特性,其中最重要的特性之一就是函数做为第一类型对象。是的,对象!
因此,咱们能够给函数添加属性,甚至能够添加方法。
有时候,咱们可能须要存储一组相关但又独立的函数,事件回调管理是最为明显的例子。向这个集合添加函数时候,咱们得知道哪些函数在集合中存在,不然不添加。
var store = { nextId:1, cache:{}, add:function(fn) { if(!fn.id){ fn.id = store.nextId++; return !!(store.cache[fn.id] = fn); } } } function ninja() {} console.log(store.add(ninja)); console.log(store.add(ninja));
上述代码比较简单常规,也就不作过多解释。
缓存记忆是构造函数的过程,这种函数可以记住先前计算的结果。经过避免重复的计算,极大地提升性能。
做为一个简单的例子,这里我来判断一个数字是否为素数。
function isPrime(value) { if(!isPrime.answers) isPrime.answers = {}; if(isPrime.answers[value]!=null){ return isPrime.answers[value] } var prime = value != 1;//1 不是素数 for(var i = 2;i<value;i++){ if(value%i===0){ prime = false; break; } } return isPrime.answers[value] = prime } console.log(isPrime(5)); console.log(isPrime.answers[5]);
如上代码也都是常规操做,不作过多解释。咱们能够经过下面的console.log判断出缓存是否成功。
缓存记忆有两个主要的优势:
在函数调用获取以前计算结果的时候,最终用户享有性能优点
发生在幕后,彻底无缝,最终用户和开发者都无需任何特殊的操做或者为此作任何初始化工做。
固然,总归会有缺点的
为了提升性能,任何类型的缓存确定会牺牲内存
纯粹主义者可能认为缓存这个问题不该该与业务逻辑放到一块儿。一个函数或者方法只应该作一件事。
很难测试和测量一个算法的性能。(好比咱们这个“简单”的例子)
经过元素标签名来获取DOM元素是一个很是常见的操做。可是性能可能不是特别好。因此从上面的缓存记忆咱们能够进行以下的骚操做:
function getElements(name) { if(!getElements.cache) getElements.cache = {}; return getElements.cache[name] = getElements.cache[name]||document.getElementsByTagName(name); }
上面的代码很简单,可是有么有眼前一亮的感受呢??我有!并且咱们还发现,这个简单的缓存的代码产生了5倍以上的性能提高。
咱们能够将状态和缓存信息存储在一个封装的独立位置上,不只在代码组织上有好处,并且外部存储或缓存对象无需污染做用域,就能够获取性能的提高。
别激动,下面还有更多的奇淫技巧~
有时候咱们想建立一个包含一组数据的对象。若是只是集合,则只须要建立一个数组便可。可是在某些状况下,除了集合自己,可能会有更多的状体须要保存。
一种选择是,每次建立对象新版本的时候都建立一个新数组,而后将元数据做为属性或者方法添加到这个新数组上。可是这个操做太常规了。
欣赏以下骚操做:
<html> <head></head> <body> <input id="first"> <input id="second"> <script> var elems = { length:0, add:function(elem) { Array.prototype.push.call(this,elem); }, gather:function(id) { this.add(document.getElementById(id)); } } elems.gather('first'); console.log(elems.length,elems[0].nodeType); elems.gather('second'); console.log(elems.length,elems[1].nodeType); </script> </body> </html>
一般,Array.prototype.push()是经过其函数上下文操做其自身数组的。这里咱们经过call方法来说咱们本身的对象扮演了一次他的上下文。push的方法会增长length的值(会认为他就是数组的length属性),而后给对象添加一个数字属性,并将其引用到传入的元素上。
关于函数的执行上下文,以及prototype的一些说明,将在后续文章写到。
JavaScript灵活且强大的特性之一是函数能够接受任意数量的参数。虽然JavaScript没有函数的重载,可是参数列表的灵活性是获取其余语言相似重载功能的关键所在
需求:查找数组中的最大值、最小值
一开始,我认为Math中提供的min(),max()能够知足,可是貌似他并不可以找到数组中的最大值最小值,难道要我这样:Math.min(arr[0],arr[1],arr[3]...)??
来吧,咱们继续咱们的奇淫技巧。
function smallest(arr) { return Math.min.apply(Math,arr); } function largest(arr) { return Math.max.apply(Math,arr); } console.log(smallest([0,1,2,3,4])); console.log(largest([0,1,2,3,4]));
不作过多解释,操做常规,是否是又是一个眼前一亮呢?
以前咱们有介绍过函数的隐士传递,arguments,也正是由于这个arguments的存在,才让函数有能力处理不一样数量的参数。即便咱们只定义固定数量的形参,经过arguments参数咱们仍是能够访问到实际传给函数的全部的参数。
方法的重载一般是经过在同名的方法里声明不一样的实例来达到目的。可是在javascript中并不是如此,在javaScript中,咱们重载函数的时候只有一个实现。只不过这个实现内部是经过函数实际传入的参数的特性和个数来达到相应目的的。
function merge(root){ for(var i = 1;i<arguments.length;i++){ for(var key in arguments[i]){ root[key] = arguments[i][key] } } return root; } var merged = merge( {name:'Neal'}, {age:24}, {city:'Beijing'} ); console.log(merged);
经过如上代码,咱们将传递给函数的对象都合并到一个对象中。在javascript中,没有强制函数声明多少个参数就得穿入多少个参数。函数是否能够成功处理这些参数,彻底取决于函数自己的定义。
注意,咱们要作的事情是想让第二个或者第n个参数上的属性合并到第一个对象中,因此这个遍历是从1开始的。
基于函数的参数,有不少种办法进行函数的重载。一种通用的方法是,根据传入参数的类型执行不一样的操做。另外一种办法是,能够经过某些特定参数是否存在来进行判断。还有一种是经过传入参数个数来进行判断。
假如对象上有一个方法,根据传入参数的个数来执行不一样的操做,冗长且呆呆的函数应该张这样:
var ninja = { whatever:function(){ switch(arguments.length){ case:0: //do something break; case:1: //do something break; case:2: //do something break; case:3: //do something break; } } }
这种方式,看起来很是的呆呆的。因此咱们换一种方式来讲下。
若是按照以下思路,添加剧载的方法会怎样呢。
var ninja = {}; addMethod(ninja,'whatever',function(){/*do something*/}); addMethod(ninja,'whatever',function(a){/*do something*/}); addMethod(ninja,'whatever',function(a,b){/*do something*/});
这里咱们使用一样的名称(whatever)将方法添加到该对象上,只不过每一个重载的函数是单独的。注意每个重载的函数参数是不一样的。经过这种方式,咱们真正为每个重载都建立了一个独立的匿名函数。漂亮且简洁。
下面就让我操刀来实现这个addMethod函数吧
function addMethod(object,name,fn){ var old = object[name]; object[name] = function(){ if(fn.length === arguments.length){ return fn.apply(this,arguments); }else if(typeof old == 'function'){ return old.apply(this,arguments); } } }
这个操做咱们这里解释一下,第一步,咱们保存原有的函数,由于调用的时候可能不匹配传入的参数个数。第二部建立一个新的匿名函数,若是该匿名函数的形参个数和实际个数匹配,就调用这个函数,不然调用原来的函数。
这里的fn.length是返回函数定义时候定义的形参个数。
下面解释下这个函数的执行吧。adMethod第一次调用将建立一个新的匿名函数传入零个参数进行调用的时候将会调用这个fn函数。因为此时这个ninja是一个新的对象,因此没必要担忧以前建立过的方法。
第二次调用addMethod的时候,首先将以前的同名函数保存到一个变量old中,而后将新建立的匿名函数做为方法。新方法首先检查传入的个数是否为1,若是是则调用新传入的fn,若是不是,则调用旧的。从新调用该函数的时候将在此检查参数个数是否为0
这种调用方式相似于剥洋葱,每一层都检查参数个数是否匹配。这里的一个技巧是关于内部匿名函数是否合访问到old和fn的。这个关于函数闭包的知识就在下一篇博客讲解(关注微信公众号吧)
function addMethod(object,name,fn){ var old = object[name]; object[name] = function(){ if(fn.length === arguments.length){ return fn.apply(this,arguments); }else if(typeof old == 'function'){ return old.apply(this,arguments); } } } var ninjas = { values:['Neal','yang','Nealyang','Neal yang'] } addMethod(ninjas,'find',function(){ return this.values; }); addMethod(ninjas,'find',function(name){ var ret = []; for(var i = 0;i<this.values.length;i++){ if(this.values[i].indexOf(name)===0){ ret.push(this.values[i]); } } return ret; }); addMethod(ninjas,'find',function(first,last){ var ret = []; for(var i = 0;i<this.values.length;i++){ if(this.values[i]==(first+' '+last)) ret.push(this.values[i]); } return ret; }); console.log(ninjas.find().length); console.log(ninjas.find('Neal')); console.log(ninjas.find('Neal','yang'));
关于上面使用的闭包想关注的知识,将在下一篇博客中,为你们总结。
而后使用如上的技巧的时候须要注意下面几点:
重载是适用于不一样数量的参数,不区分类型、参数名称或者其余东西
这样的重载方法会有一些函数调用的开销。咱们要考虑在高性能时的状况。
扫码关注个人我的微信公众号,分享更多原创文章。点击交流学习加我微信、qq群。一块儿学习,一块儿进步
欢迎兄弟们加入:
Node.js技术交流群:209530601
React技术栈:398240621
前端技术杂谈:604953717 (新建)