虽然javascript是一门面向对象的编程语言,但这门语言同时也同时拥有许多函数式语言的特性。
函数式语言的鼻祖是LISP,javascript设计之初参考了LISP两大方言之一的Schenme,引入了Lambda表达式,闭包,高阶函数等特性。使用这些特性,我们就可以灵活的编写javascript代码。
一:闭包
对于javascript程序员来说,闭包(closure)是一个难懂又必须征服的概念。闭包的形成与变量作用域以及变量的声明周期密切相关。
1.变量作用域
变量的作用域就是指变量的有效范围,我们最常谈到的是在函数中声明的变量作用域。
当在函数中声明一个变量时,如果没有使用var关键字,这个变量就会变成全局变量(当然这是一种容易造成命名冲突的做法。)
另外一种情况是用var关键字在函数中声明变量,这时候的变量即局部变量,只有在函数内部才能访问到这变量,在函数外面是访问不到的,代码如下:
var func = function() { var a = 1; console.log(a) } func() console.log(a);//Uncaught ReferenceError: a is not defined
下面这段包含了嵌套函数的代码,也许能帮助我们加深对遍历搜索过程中的理解
var a = 1; var func = function() { var b = 2; var func2 = function(){ var c = 3; console.log(b); console.log(a) } func2() console.log(c) //Uncaught ReferenceError: c is not defined } func()
2.变量的生成周期
var func = function(){ var a =1; console.log(a) //退出函数后局部变量a将销毁 } func()
var func2 = function(){ var a = 2; return function() { a++; console.log(a) } } var f = func2(); f() //3 f() //4 f() //5 f() //6
func2根我们之前的推论相反,当退出函数后,局部变量a并没有消失,而是停留在某个地方。这是因为,当执行 var f = func2()时,f返回了一个匿名函数的引用,它可以访问到func()被调用时的所产生的环境,而局部变量a一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部的变量就有了不被销毁的理由。在这里产生了一个闭包环境,局部变量看起来被延续了。
利用闭包我们可以完成很多奇妙的工作,下面介绍一个闭包的经典应用。
假设页面上有5个div节点,我们通过循环给div绑定onclick,按照索引顺序,点击第一个时弹出0,第二个输出2,依次类推。
<div>div1</div> <div>div2</div> <div>div3</div> <div>div4</div> <div>div5</div> <div>div6</div> <script type="text/javascript"> var nodes = document.getElementsByTagName('div') console.log(nodes.length) for (var i = 0; i < nodes.length; i++) { nodes[i].onclick = function() { console.log(i) } } </script>
在这种情况下,发现无论点击那个div都输出6,这是因为div节点的onclick是被异步触发的,当事件被触发的时候,for循环早已经结束,此时的变量i已经是6。
解决的办法是,在闭包的帮助下,把每次循环的i都封闭起来,当事件函数顺着作用域链中从内到外查找变量i时,会先找到被封闭在闭包环境中的i,如果有6个div,这里的i就是0,1,2,3,4,5
var nodes = document.getElementsByTagName('div') for (var i = 0; i < nodes.length; i++) { (function(i){ nodes[i].onclick = function(){ console.log(i+1) } })(i) }
根据同样的道理,我们还可以编写如下一段代码
var Type = {}; for (var i = 0 , type; type = ['String','Array','Number'][i++];){ (function ( type ){ Type['is' + type] = function( obj ) { return Object.prototype.toString.call( obj ) === '[object '+ type +']' } })( type ) } console.log( Type.isArray([]) ) //true console.log( Type.isString('') )//true
3.闭包的更多的作用
在实际开发中,闭包的运用十分广泛
(1)封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”,假设一个计算乘积的简单函数。
var mult = function(){ var a = 1; for (var i = 0, l = arguments.length; i < l; i++) { a = a * arguments[i] } return a } console.log(mult(10,2,4)) //80
mult函数每次都接受一些number类型的参数,并返回这些参数的乘积,现在我们觉得对于那些相同的参数来说,每次都进行一次计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能。
var cache = {}; var mult = function(){ var args = Array.prototype.join.call( arguments, ',' ); if (cache[ args ]) { return cache[ args ] } var a = 1; for ( var i = 0, l = arguments.length; i<l;i++ ) { a = a * arguments[i] } return cache[ args ] = a; } console.log(mult(10,2,4)) //80
看到cache这个变量仅仅在mult函数中被使用,与其让cache变量跟mult函数一起暴露在全局作用域下,不如将它封装在mult内部,这样可以减少页面的全局变量,以避免在其它地方不小心修改而引发错误。
var mult = (function(){ var cache = {}; return function(){ var args = Array.prototype.join.call( arguments, ',' ); if (args in cache){ return cache[ args ] } var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i] } return cache[ args ] = a; } })() console.log(mult(10,2,4,2)) //160
提炼函数是重构中一种常见的技巧。如果在一个大函数中有一些代码能独立出来,我们常常把这些小代码块封装在独立的小函数里面。独立的小函数有助于代码复用 ,如果这些小函数有一个良好的命名,它们本身起到了注释的作用,这些小函数不需要在程序的其它地方使用,最好是他们用闭包封闭起来。代码如下:
var mult = (function(){ var cache = {}; var calculate = function(){//封闭calculate函数 var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i] } return a; } return function(){ var args = Array.prototype.join.call( arguments, ',' ); if ( args in cache ){ return cache[ args ]; } return cache[ args ] = calculate.apply( null, arguments ) } })() console.log(mult(10,2,4,2,2)) //320
(2)延续局部变量的寿命
img对象经常用于数据的上报,如下所示
var report = function( src ){ var img = new Image() img.src = src; } report('http://.com/getUserinfo')
但是我们结果查询后,得知,因为一些低版本浏览器的实现存在bug,在这些浏览器下使用report函数数据的上报会丢失30%,也就是说,reprot函数并不是每次都发起了请求。
丢失的原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随即被销毁,而此时或许还没有来的及发出http请求。所有此次的请求就会丢失掉。
现在我们将img变量用闭包封闭起来,便能解决请求丢失的问题。
var report = (function(){ var img = []; return function( src ){ var img = new Image(); img.push( img ); img.src = src; } })()
4.闭包和面向对象设计
下面我们来看看跟闭包相关的代码:
var extent = function(){ var value = 0; return { call : function(){ value++; console.log(value) } } }; var bb = extent(); bb.call() //1 bb.call() //2 bb.call() //3
如果换成面向对象的写法,就是:
var extent = { value : 0, call : function(){ this.value++; console.log(this.value) } } extent.call();//1 extent.call();//2 extent.call();//3
或者,
var extent = function(){ this.value = 0; } extent.prototype.call = function(){ this.value++; console.log(this.value) } var dd = new extent() dd.call();//1 dd.call();//2 dd.call();//3
二.高阶函数
高阶函数是至少满足以下两点的函数
- 函数可以作为参数被传递
- 函数可以作为返回值输出
1)。函数作为参数传递
1.回调函数
在ajax的请求应用中,回调函数使用的特别频繁,当我们想在ajax请求返回之后做一些事情。但又不知道确切的返回时间时,最常见的方案就是把callback函数作为参数传入发起ajax请求的方法中,待请求完成时执行callback函数
var getUserInfo = function( userid, callback) { $.ajax('http://xxx.com/getUserInfo?' + userid, function( data ){ if (typeof callback === 'function') { callback( data ) } }); } getUserInfo(1233,function( data ){ console.log( data ) });
回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把一些请求封装成一个函数,并把它作为参数传递给另外一个函数,“委托”给另外一个函数来执行。
比如,我们想在页面中创建100个DIV节点,然后把这些DIV节点都设置为隐藏。下面是一种编写代码的方式:
var appendDiv = function(){ for (var i = 0; i < 100; i++){ var div = document.createElement('div'); div.innerHTML = i; document.body.appendChild( div ) div.style.display = 'none' } } appendDiv()
将 div.style.display = 'none' 的逻辑编码在appendDiv里面是不合理的,appendDiv未免有点个性化,成为了一个难以复用的的函数,并不是每人创建了节点之后就希望它们立即隐藏。
于是我们将div.style.display = 'none'这行代码抽出来,用回调函数传入appendDiv方法:
var appendDiv = function( callback ){ for (var i = 0; i < 100; i++){ var div = document.createElement('div'); div.innerHTML = i; document.body.appendChild( div ); if (typeof callback === 'function'){ callback( div ) } } } appendDiv( function( node ){ node.style.display = 'none' });
可以看到,隐藏节点的请求实际上是由客户端发起的,但是客户并不知道节点什么时候会创建好,于是把隐藏节点的逻辑放在回调函数中,“委托”给appendDiv方法。appendDiv方法方然知道节点什么时候创建好,所以在节点创建好的时候,appendDiv会执行之前客户传入的回调函数。
2.Array.prototype.sort
Array.prototype.sort接受一个函数当做参数,这个函数里面封装了数组元素的排序规则。从Array.prototype.sort的使用可以看出,我们的目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分,把可变的部分封装在函数参数里,动态传入Array.prototype.sort,使Array.prototype.sort方法编程一个非常灵活的方法,代码如下:
//从小到大 var cc = [1,4,3].sort(function( a, b ){ return a - b; }); console.log(cc);//[1, 3, 4] //从大到小 var dd = [1,5,2,57,22].sort(function( a, b){ return b - a; }) console.log(dd) ;//[57, 22, 5, 2, 1]
2)。函数作为返回值输出
相比把函数当做参数传递,函数当返回作返回值输出的应用场景或更多,也能更体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。
1.判断数据的类型。
var isString = function ( obj ){ return Object.prototype.toString.call( obj ) === '[object string]'; } var isArray = function( obj ){ return Object.prototype.toString.call( obj ) === '[object Array]'; } var isMumber = function( obj ){ return Object.prototype.toString.call( obj ) === '[object Number]' }
我们发现,这些函数的大部分都是相同的,不同的只是Object.prototype.toString.call( obj )返回的字符串。,为了避免多余的代码,我们尝试把这些字符串作为参数提前值入isStype函数。代码如下:
var isType = function( type ){ return function( obj ){ return Object.prototype.toString.call( obj ) === '[object ' + type +']'; } } var isString = isType( 'String' ); var isArray = isType( 'Array' ); var isNumer = isType( 'Number' ); console.log( isArray([1,2,3]) );
2.getSingle
下面是一个单例模式的例子,在后面的设计模式的学习中,我们将更深入的讲解,这是暂且只理解其代码的实现
var getSingle = function( fn ){ var ret; return function(){ return ret || ( ret = fn.apply( this, arguments) ); } };
3)高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志的统计,安全控制,异常处理等。
这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是很方便的复用日志统计等功能模块。
在java中,可以通过反射和动态代理机制来实现AOP技术。而在javascript这种动态语言中,AOP的实现更加简单。这是javascript与生俱来的能力。
通常,在javascript中实现AOP,都是指把一个函数“动态织入”到另外一个函数中,具体的实现方式有很多,本书我们将通过扩展Function.prototype来做到这一点,代码如下:
Function.prototype.before = function( beforefn ) { var _self = this;//保存对原函数的引用 return function(){ //返回包含了原函数和新函数的“代理”的函数 beforefn.apply( this, arguments ); //执行新函数,修正this return _self.apply( this, arguments );//执行原函数 } } Function.prototype.after = function( afterfn ) { var _self = this; return function(){ var ret = _self.apply( this, arguments ); afterfn.apply( this, arguments ); return ret; } } var func = function() { console.log(2) } func = func.before(function(){ console.log( 1 ); }).after(function(){ console.log(3) }) func()
我们把负责打印数字1和打印数字3的两个函数通过AOP的方式动态植入func函数。通过执行,我们看到控制台返回1 2 3
三:函数节流
javascript中的函数大部分的情况是由用户主动是触发的,除非函数本身不合理,否则我们一般不会遇到跟性能相关的问题。但在一些少数的情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能非常频繁的被调用,从而造成大的性能问题,下面将举例说明下这个问题。
(1)函数被频繁调用。
- window.onresize事件。我们给window绑定了resize事件,当浏览器的大小窗口被改变时,这个事件的触发频率非常高。如果我们在window.resize事件函数里有一些跟DOM相关的节点操作,往往是非常消耗性能的。这个时候浏览器有可能吃不消,或者卡顿。
- mosemove事件。 同样,如果我们给一个div绑定了拖拽事件(主要是mousemove),当div节点被拖动时,也会频繁的触发该拖拽事件。
- 上传进度。在一个文件被浏览器扫描并上传文件之前,会对文件进行扫描并随时通知javascript函数,以便在当前页面中显示当前正真的进度。但通知频率非常高,大概一秒种10次,很显然我们在页面中没有必要这么频繁的去通知。
(2)函数节流的原理
我们通过整理上面提到的是哪个场景,发现它们共同面临的问题是函数触发的频率太高。
(3)函数节流的实现。
关于函数节流的实现代码有很多种,下面的throttle函数的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该执行延迟还没有完成,则忽略接下来调用该函数的请求。throttle函数接受两个参数,第一个参数需要被延迟执行的函数,第二个参数为延迟执行的时间。具体代码如下:
var throttle = function( fn, interval ){ var __self = fn, //保存需要被延迟执行的函数引用 timer, //定时器 firstTime = true; //是否第一次调用 return function() { var args = arguments, __me = this; if( firstTime ) { //如果是第一次调用,不需要延迟执行 __self.apply( __me, args ); return firstTime = false; } if (timer) { //如果定时器还在,说明潜一次延迟执行还没有完成 return false; } timer = setTimeout(function(){//延迟一段时间执行 clearTimeout(timer); timer = null; __self.apply(__me, args) }, interval || 500) } } window.onresize = throttle(function(){ console.log( 1 ) },500)
(4)分时函数
在前面的函数节流的讨论中,我们提供了一种限制函数被频繁调用的解决方案,下面我们将遇到另外一个问题,某些函数确实是用户主动调用的,但是因为一些客观的原因,这些函数会严重的影响页面的性能。
一个列子是webQQ创建QQ好友列表,列表中会有成千上万的好友,如果一个节点一个节点的表示,当我们渲染这个列表时,可能一次要往一个页面中创建成千上万的节点。
在短时间内往页面中添加大量DOM节点,会让浏览器吃不消,造成假死。
var ary = []; for (var i = 1; i <= 1000; i++){ ary.push(i); //假设ary装在了1000个好友 } var renderList = function( data ){ for (var i = 0, l = data.length; i < l; i++){ var div = document.createElement('div'); div.innerHTML = i; document.body.appendChild( div ) } } renderList( ary )
下面这个问题的解决方案之一是下面的timeChunk函数,timeChunk函数让创建节点工作分批进行,比如1秒钟创建1000个节点变为每隔200秒创建8个节点。
timeChunk函数接受3个参数,第一个参数是创建节点时需要用到的数据,第2个参数是封装了创建节点逻辑的函数,第3个参数表示每一批创建节点的数量。
代码如下:
var timeChunk = function( ary, fn, count ){ var obj, t; var len = ary.length; var start = function(){ for (var i = 0; i < Math.min( count || 1, ary.length ); i++) { var obj = ary.shift(); fn( obj ) } }; return function(){ t = setInterval(function(){ if ( ary.length === 0 ){//如果全部节点以及都已经创建好 return clearInterval(t) } start() },200) }; };
最后我们进行一些小测试,假设我们有1000个好友的数据,我们利用timeChunk函数,每一批只往页面中创建8个节点:
var ary = []; for (var i = 1; i <= 1000; i++){ ary.push(i); //假设ary装在了1000个好友 } var renderList = timeChunk( ary, function( n ){ var div = document.createElement('div'); div.innerHTML = n; document.body.appendChild( div ); }, 8 ); renderList()
(5)惰性加载函数
在web开发中,因为浏览器的实现差异,一些嗅探工作总是不可避免。比如我们需要在一个各个浏览器中都能够通用的事件绑定函数addEvent,常见写法是如下:
var addEvent = function( elem, type, hander ){ if ( window.addEventListener ){ return elem.addEventListener( type, hander, false ); } if (window.attachEvent) { return elem.attachEvent( 'on' + type, hander ) } };
这个函数的缺点是,当它每次被调用时都会执行里面的if条件分支,虽然执行这些if分支的开销不算大,但也许有一些方法可以让程序避免这些重复执行的过程。
第二种方案是这样 ,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就进行一次判断,以便让addEvent返回一个包裹了正确的逻辑函数,代码如下:
var addEvent = (function(){ if ( window.addEventListener ){ return function( elem, type, hander ){ elem.addEventListener( type, hander, false ); } } if ( window.attachEvent ){ return function( elem, type, hander ){ elem.attachEvent( 'on' + type, hander ) } } })();
目前addEvent函数依然有个缺点,也许我们从头到尾都没有使用过addEvent函数,这样看来,前一次的浏览器嗅探就是完全多余的操作,而且这样也会稍微延长页面的ready时间。
第三种方法我们将要讨论惰性载入函数方案。此时addEvent依然被声明为一个普通函数,在函数里依然有一些分支判断,但是在第一次进入条件分支后,在函数内部会重写这个函数,重写之后的函数就是我们期望的addEvent函数,在下一次几个人addEvent函数的时候,addEvent函数里不再存在条件分支语句。
var addEvent = function( elem, type, handler ){ if ( window.addEventListener ){ addEvent = function( elem, type, handler ){ elem.addEventListener( type, handler, false ) } } else if ( window.attachEvent ){ addEvent = function( elem, type, handler ){ elem.attachEvent( 'on' + type, handler); } } addEvent( elem, type, handler ) } var div = document.getElementById('div1'); addEvent( div, 'click', function(){ alert('1') }); addEvent( div, 'click', function(){ alert('2') })
(本文完结)
上一篇文章: (二)this、call和apply 下一篇文章 (四)设计模式
一:javascript基础系列(已完结) | 二:javascript基础系列之DOM(已完结) | 三:jQuery系列文章(已完结) |
四:AJAX | 五:JavaScript权威指南(核心篇,已完结) | 六:JavaScript框架设计(已完结) |
七:数据结构与算法javascript描述 |