转载请注明预见才能遇见的博客:http://my.csdn.net/
原文地址:https://mp.csdn.net/postedit/100583697
JavaScript设计模式系列—基础篇(三)闭包和高阶函数
目录
JavaScript设计模式系列—基础篇(三)闭包和高阶函数
1.函数作为参数传递-回调函数 和 Array.prototype.sort
虽然 JavaScript是一门完整的面向对象的编程语言,但这门语言同时也拥有许多函数式语言的特性。在 JavaScript版本的设计模式中,许多模式都可以用闭包和高阶函数来实现。
1.闭包
闭包的形成与变量的作用域以及变量的生存周期密切相关。
1.变量的作用域
当在函数中声明一个变量的时候,如果该变量前面没有带上关键字 var,这个变量就会成为 全局变量,这当然是一种容易造成命名冲突的做法。
另外一种情况是用 var 关键字在函数中声明变量,这时候的变量即是局部变量,只有在该函 数内部才能访问到这个变量,在函数外面是访问不到的。在 JavaScript 中,函数可以用来创造函数作用域。
var a = 1;
var func1 = function(){
var b = 2;
var func2 = function(){
var c = 3;
L ( b ); // 输出:2
L ( a ); // 输出:1
}
func2();
L ( c ); // 输出:Uncaught ReferenceError: c is not defined
};
func1();
2.变量的生存周期
对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。 而对于在函数内用 var 关键字声明的局部变量来说,当退出函数时,它们都会随着函数调用的结束而被销毁
var func = function(){
var a = 1;
return function(){
a++;
alert ( a );
}
};
var f = func();
f(); // 输出:2
f(); // 输出:3
f(); // 输出:4
f(); // 输出:5
跟我们之前的推论相反,当退出函数后,局部变量 a 并没有消失,而是似乎一直在某个地方 存活着。这是因为当执行 var f = func();时,f 返回了一个匿名函数的引用,它可以访问到 func() 被调用时产生的环境,而局部变量 a 一直处在这个环境里。
//一个闭包的经典案例
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<script>
var nodes = document.getElementsByTagName( 'div' );
for ( var i = 0, len = nodes.length; i < len; i++ ){
nodes[ i ].onclick = function(){
alert ( i ); //一直打印5
}
};
</script>
</body>
//解决
for ( var i = 0, len = nodes.length; i < len; i++ ){
(function( i ){
nodes[ i ].onclick = function(){
console.log(i);
}
})( i )
};
//经典案例
//判断一个类型是字符串、数字、或者是数组等
var Type = {};
for (var i = 0,type; type = ["Array","Number","String"][i++];) {
;(function(type){
Type["is"+type] = function(obj){
return Object.prototype.toString.call(obj) === "[object "+type+"]";
}
})(type)
}
L(Type.isArray([])); // 输出:true
L(Type.isString("str")); // 输出:true
L(Type.isNumber(1)); // 输出:true
3.闭包的更多作用
1)封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。
// 计算多个参数的乘积
var mult = function(){
var count = 1;
for (let index = 0,len = arguments.length; index < len; index++) {
count *= arguments[index];
}
return count;
}
//优化一
//加入缓存
var cache = {}
var mult = function(){
var argstr = Array.prototype.join.call(arguments,",");
if(cache[argstr]){
// L("缓存的结果")
return cache[argstr];
}
var count = 1;
for (let index = 0,len = arguments.length; index < len; index++) {
count *= arguments[index];
}
return cache[argstr] = count;
}
//优化二
/***
cache仅仅在mult函数中被使用,与其让cache暴露在全局作用域下,不如把它封闭
在mult函数内部,减少页面中的全局变量,避免不小心修改而引发错误。
***/
var mult = (function(){
var cache = {}
return function(){
var argstr = Array.prototype.join.call(arguments,",");
if(cache[argstr]){
L("缓存的结果")
return cache[argstr];
}
var count = 1;
for (let index = 0,len = arguments.length; index < len; index++) {
count *= arguments[index];
}
return cache[argstr] = count;
}
})();
提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码复用,如果这些小函数不需要在程序的其他 地方使用,最好是把它们用闭包封闭起来。
var mult = (function(){
var cache = {}
//计算乘积
var calculate = function(){
var count = 1;
for (let index = 0,len = arguments.length; index < len; index++) {
count *= arguments[index];
}
return count;
}
return function(){
var argstr = Array.prototype.join.call(arguments,",");
if(argstr in cache){
L("缓存的结果")
return cache[argstr];
}
return cache[argstr] = calculate.apply(null,arguments);
}
})();
2)延续局部变量的寿命
img 对象经常用于进行数据上报
var report = function( src ){
var img = new Image();
img.src = src;
};
report( 'http://xxx.com/getUserInfo' );
report 函数的调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以所以可能导致数据丢失。
//现在我们把img 变量用闭包封闭起来,便能解决请求丢失的问题:
var report = (function(){
var imgs = [];
return function( src ){
var img = new Image();
imgs.push( img );
img.src = src;
}
})();
4.闭包和面向对象设计
过程与数据的结合是形容面向对象中的“对象”时经常使用的表达。对象以方法的形式包含 了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用 闭包也能实现。
//闭包实现
function util(){
var count = 0;
return {
oprCount:function(){
count++;
L(count);
}
}
}
var utilObj = util();
utilObj.oprCount();
utilObj.oprCount();
utilObj.oprCount();
//面向对象实现
var utilObj = {
count:0,
oprCount:function(){
this.count++;
L(this.count);
}
};
//或者
function Util(){
this.count = 0;
}
Util.prototype.oprCount = function(){
this.count++;
L(this.count);
}
var utilObj = new Util();
5.用闭包实现命令模式
后面命令模式再看
6.闭包与内存管理
变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,如果在将来需要回收这些变量,我们可以手动把这些变量设为 null。跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作 用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露。
2.高阶函数
高阶函数是指至少满足下列条件之一的函数。
1)函数可以作为参数被传递;
2)函数可以作为返回值输出。
1.函数作为参数传递-回调函数 和 Array.prototype.sort
Array.prototype.sort 接受一个函数当作参数,这个函数里面封装了数组元素的排序规则。目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分(把可变的部分封装在函数参数里,动态传入)。
var arr = [2,8,5];
//小到大
arr.sort(function(a,b){
return a - b;
})//[2,5,8]
//大到小
arr.sort(function(a,b){
return b - a;
})//[8,5,2]
2.函数作为返回值输出-判断数据类型和getSingle
//判断一个类型是字符串、数字、或者是数组等
var isType = function( type ){
return function( obj ){
return Object.prototype.toString.call( obj ) === '[object '+ type +']';
}
};
var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumber = isType( 'Number' );
L( isArray( [ 1, 2, 3 ] ) ); // 输出:true
//单例模式案例
var getSingle = function ( fn ) {
var ret;
return function () {
return ret || ( ret = fn.apply( this, arguments ) );
};
};
var getScript = getSingle(function(){
return document.createElement( 'script' );
});
var script1 = getScript();
var script2 = getScript();
alert ( script1 === script2 ); // 输出:true
3.高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。AOP 是 JavaScript 与生俱来的能力,通常,在 JavaScript 中实现 AOP,都是指把一个函数“动态织入”到另外一个函数之中。装饰者模式
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();
4 高阶函数的其他应用
1.函数柯里化-Currying
柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。参考资料:详解JS函数柯里化
var sum = (function(){
var args = [];
return function(){
if(arguments.length === 0){
var countRes = 0;
for (var index = 0; index < args.length; index++) {
countRes += args[index];
}
return countRes;
}else{
[].push.apply(args,arguments);
}
}
})();
sum(1);sum(3);sum(5);L(sum());//9
//通用柯里化
var currying = function(fn){
var args = [];
return function(){
if(arguments.length === 0){
return fn.apply(null,args);
}else{
[].push.apply(args,arguments);
// return arguments.callee;
}
}
}
// var sum = (function(){
// var countRes = 0;
// return function(){
// for (var index = 0,len = arguments.length; index < len; index++) {
// countRes += arguments[index];
// }
// return countRes;
// }
// })()
var sum = function(){
var countRes = 0;
for (var index = 0,len = arguments.length; index < len; index++) {
countRes += arguments[index];
}
return countRes;
}
sum = currying(sum);
sum(1);sum(3);sum(5);L(sum());//9
sum(6);L(sum());//使用闭包这里你的结果是24,不使用是15,不使用闭包的场景更多
2.函数反柯里化-uncurrying
让对象去借用一个原本不属于它的方法,常让类数组对象去借用 Array.prototype 的方法,这是 call 和 apply 最常见的应用场 景之一:参考资料:JavaScript高阶函数之currying和uncurrying
;(function(){
// arguments 借用 Array.prototype.push 方法
//泛化this
Array.prototype.push.call(arguments,4);
//Arguments(4) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
L(arguments);
})(1,2,3)
方法中用到 this 的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性。
var push = function(){
var obj = Array.prototype.shift.call( arguments );
Array.prototype.push.apply(obj,arguments);
}
//通用反柯里化
Function.prototype.uncurrying = function () {
var self = this;
return function() {
var obj = Array.prototype.shift.call( arguments );
return self.apply( obj, arguments );
};
};
var push = Array.prototype.push.uncurrying();
;(function(){
push( arguments, 4 );
L( arguments ); // 输出:[1, 2, 3, 4]
})( 1, 2, 3 )
通过 uncurrying 的方式,Array.prototype.push.call 变成了一个通用的 push 函数,push 函数的作用就跟Array.prototype.push 一样。还可以一次性地把 Array.prototype 上的方法“复制”到 array 对象上,这些方法可操作的对象也不仅仅只是 array 对象:
for ( var i = 0, fn, ary = [ 'push', 'shift', 'forEach' ]; fn = ary[ i++ ]; ){
Array[ fn ] = Array.prototype[ fn ].uncurrying();
};
var obj = {
"length": 3,
"0": 1,
"1": 2,
"2": 3
};
Array.push( obj, 4 ); // 向对象中添加一个元素
L( obj.length ); // 输出:4
var first = Array.shift( obj ); // 截取第一个元素
L( first ); // 输出:1
L( obj ); // 输出:{0: 2, 1: 3, 2: 4, length: 3}
Array.forEach( obj, function( i, n ){
L( n ); // 分别输出:0, 1, 2
});
甚至 Function.prototype.call 和 Function.prototype.apply 本身也可以被 uncurrying,不过这 没有实用价值,只是使得对函数的调用看起来更像 JavaScript 语言的前身 Scheme:
var call = Function.prototype.call.uncurrying();
var fn = function( name ){
console.log( name );
}
call( fn, window, 'sven' ); // 输出:sven
var apply = Function.prototype.apply.uncurrying();
var fn = function( name ){
console.log( this.name ); // 输出:"sven"
console.log( arguments ); // 输出: [1, 2, 3]
};
apply( fn, { name: 'sven' }, [ 1, 2, 3 ] );
3. 函数节流--解决不是用户直接控制
函数有可能被非常频繁地调用,造成大的性能问题。函数被频繁调用的场景:
1)window.onresize 事件,如果在onresize中操作DOM 节点,可能会出现卡顿。
2)mousemove 事件 和 上传进度等
函数节流的原理:
window.onresize在1s内执行了10次,但是我们实际上可能只要2-3次就可以了,所以我们按时间段来忽略掉一些事件请求,借助 setTimeout 来完成。
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. 分时函数--解决用户直接控制
在短时间内往页面中大量添加 DOM 节点显然也会让浏览器吃不消,我们可以让创建节点的工作分批进行,比如把 1 秒钟创建 1000 个节点,改为每隔 200 毫秒创建 8 个节点。
//第1个参数是创建节点时需要用到的数据,第2个参数是封装了创建节点逻辑的函数,
//第3个参数表示每一批创建的节点数量。
var timeChunk = function( ary, fn, count ){
var interval;
var start = function(){
for ( var i = 0,len = ary.length; i < Math.min( count || 1, len ); i++ ){
fn( ary.shift() );
}
};
return function(){
interval = setInterval(function(){
if ( ary.length === 0 ){ // 如果全部节点都已经被创建好
return clearInterval( interval );//return 终止执行后面的代码
}
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();
5. 惰性加载函数
兼容性处理时,做了很多if判断,使用惰性加载函数优化。案例:比如我们需要一个在各个浏览器中能够通用的事件绑定函数 addEvent
var addEvent = function( elem, type, handler ){
if ( window.addEventListener ){
return elem.addEventListener( type, handler, false );
}
if ( window.attachEvent ){
return elem.attachEvent( 'on' + type, handler );
}
};
//上面的代码每次调用的时候都会if判断
//优化后的代码
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 );
};
addEvent( document.getElementById( 'div01' ), 'click', function(){
L(1);
});
JavaScript设计模式系列—基础篇(三)闭包和高阶函数