1、闭包
1.1 变量的生命周期
除了变量的作用域,另一个域闭包有关的概念是变量额生存周期。
函数内部用var声明的局部变量,退出函数时,会随着函数调用的结束而被销毁.
var func = function() { var a = 1; alert(a); } func();
var func = function(){ var a = 1; return function(){ a++; alert ( a ); } }; var f = func(); f(); // 输出:2 f(); // 输出:3 f(); // 输出:4 f(); // 输出:5
这里当执行var f = func()后,f返回了一个匿名函数的引用,它可以访问到func()被调用时产生的环境,而局部变量a一直处在这个环境里,既然局部变量所在的环境还能被外界访问,这个局部变量就有不被销毁的理由。
var nodes = document.getElemensByTagName('div'); for(var i = 0; i < node.length; i++) { (function(i){ nodes[i].onclick = function() { alert(i) } })(i) }
使用闭包把每次循环的i封闭,当事件函数顺着作用域链从内到外查找i,会先找到在被封闭在闭包环境的i。
1.2 闭包的更多作用
1.封装变量
闭包可以把不需要再全局的变量封装成“私有变量”。
//假设有一个计算乘积的简单函数: var mult = function(){ var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return a; };
//我们可以加入缓存机制来提高这个函数的性能: //将输入的参数和输出的结果保存到cache对象 var cache = {}; var mult = function(){ var args = Array.prototype.join.call( arguments, ',' ); //每次执行先查询cache里有没执行过使用这个参数 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; }; alert ( mult( 1,2,3 ) ); // 输出:6 alert ( mult( 1,2,3 ) ); // 输出:6
//将cache放在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; } })();
//把能够独立出来的代码封装在独立地小函数 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 ); } })();
2、 延续局部变量寿命
//img 对象经常用于进行数据上报 var report = function( src ){ var img = new Image(); img.src = src; }; report( 'http://xxx.com/getUserInfo' );
img是report的局部变量,report函数执行完,img局部变量会被销毁,可能没来得及发出HTTP请求。
现在我们把img 变量用闭包封闭起来,便能解决请求丢失的问题:
var report = (function(){ var imgs = []; return function( src ){ var img = new Image(); imgs.push( img ); img.src = src; } })();
闭包和面向对象设计
//下面来看看这段跟闭包相关的代码: var extent = function(){ var value = 0; return { call: function(){ value++; console.log( value ); } } }; var extent = extent(); extent.call(); // 输出:1 extent.call(); // 输出:2 extent.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 extent = new Extent(); extent.call(); extent.call(); extent.call();
命令模式的意图是把请求封装成对象,从而分离请求的发起者和请求的接收者(接收者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。
但在JavaScript,函数作为一等对象,本身就可以四处传递,用函数对象而不是普通对象来封装请求显得更自然。如果需要往函数对象中预先植入 命令的接收者,可以使用闭包。
var Tv = { open: function(){ console.log( '打开电视机' ); }, close: function(){ console.log( '关上电视机' ); } }; var createCommand = function( receiver ){ var execute = function(){ return receiver.open(); // 执行命令,打开电视机 } var undo = function(){ return receiver.close(); // 执行命令,关闭电视机 } return { execute: execute, undo: undo } }; var setCommand = function( command ){ document.getElementById( 'execute' ).onclick = function(){ command.execute(); // 输出:打开电视机 } document.getElementById( 'undo' ).onclick = function(){ command.undo(); // 输出:关闭电视机 } }; setCommand( createCommand( Tv ) );
2 高阶函数
高阶函数是指至少满足下列条件之一的函数:
- 函数可以作为参数被传递
- 函数可以作为返回值输出
2.1 函数作为参数传递
1 回调函数
AJAX异步请求中,回调函数应用非常频繁。我们想在AJAX请求返回都做一些事,但不知道请求返回的时间,最常用的方案是把callback函数当做参数传入AJAX请求的方法。
var getUserInfo = function( userId, callback ){ $.ajax( 'http://xxx.com/getUserInfo?' + userId, function( data ){ if ( typeof callback === 'function' ){ callback( data ); } }); } getUserInfo( 13157, function( data ){ alert ( data.userName ); });
当一些函数不适合执行一些请求,可以把这些请求封装成一个函数,并把它作为参数传递给另一个函数,“委托”给另一个函数执行。
例如,在页面创建100个节点并把它们设置为隐藏,但每次操作都要把它们隐藏,因此可以把隐藏操作的代码抽出。
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'; });
2 Array.prototype.sort
Array.prototype.sort接受一个函数作为参数,这个函数封装了数组的排序规则。
//从小到大 [ 1, 4, 3 ].sort( function( a, b ){ return a - b; }); //从大到小 [ 1, 4, 3 ].sort( function( a, b ){ return b - a; });
2.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 isNumber = function( obj ){ return Object.prototype.toString.call( obj ) === '[object Number]'; };
将相同的部分提取,只有Object.prototype.toString.call(obj)返回值不同。
var isType = function(type) { return function(obj) { return Object.prototype.toString.call( obj ) === '[object '+ type +']'; } }
可以用循环语句注册
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 ) };
2.3高阶函数其他应用
1 函数节流(throttle)
定义
如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。
也就是会说预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。
为什么需要函数节流
在某些情况下,函数可能被频繁调用而造成大的性能问题。
- window.onresize事件。如果在window上绑定了resize事件,当拖动浏览器窗口来改变大小,这个事件触发的频率很高,如果resize事件函数里有DOM操作,可能造成浏览器卡顿。
- mousemove事件。如果给DOM节点绑定了拖曳事件(主要是mousemove),当div节点被拖动,也会频繁触发该事件。
- 上传进度。
实现
下面的throttle函数的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行完成,则忽略接下来调用该函数的请求。throttle函数接受2个参数,第一个为需要被延迟执行的函数,第二个为延迟执行的事件。
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 );
2 分时函数
在短时间内忘页面大量添加DOM节点会让浏览器卡顿甚至假死。例如WebQQ的QQ好友列表,如果一个好友用一个节点表示,可能会创建成百上千的节点。
使用timeChunk函数让创建节点分批进行,如一秒创建1000个节点改为每隔200毫秒创建8个节点。
//第一个参数是创建节点所需数据,第二个参数封装了创建节点逻辑的函数,第三个参数表示每一批创建的节点数据 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 ); // 分批执行的时间间隔,也可以用参数的形式传入 }; };
var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); }; var renderFriendList = timeChunk( ary, function( n ){ var div = document.createElement( 'div' ); div.innerHTML = n; document.body.appendChild( div ); }, 8 ); renderFriendList();
3 惰性加载函数
一般我们定义浏览器通用的事件绑定函数addEvent如下。它的缺点是,每次被调用都会执行if分句。
var addEvent = function( elem, type, handler ){ if ( window.addEventListener ){ return elem.addEventListener( type, handler, false ); } if ( window.attachEvent ){ return elem.attachEvent( 'on' + type, handler ); } };
改进后,把嗅探浏览器操作提前到代码加载时,在加载时进行一次判断,让addEvent返回一个包裹了正确逻辑的函数。
但它仍然有缺点。如果外面没有调用过addEvent函数,它就进行了多余的操作。
var addEvent = (function(){ if ( window.addEventListener ){ return function( elem, type, handler ){ elem.addEventListener( type, handler, false ); } } if ( window.attachEvent ){ return function( elem, type, handler ){ elem.attachEvent( 'on' + type, handler ); } } })();
使用惰性加载,在第一次进入分支后,函数内部重写这个函数,下次进入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 ); };