JavaScript设计模式系列—基础篇(三)闭包和高阶函数

转载请注明预见才能遇见的博客:http://my.csdn.net/

原文地址:https://mp.csdn.net/postedit/100583697

JavaScript设计模式系列—基础篇(三)闭包和高阶函数

目录

JavaScript设计模式系列—基础篇(三)闭包和高阶函数

1.闭包

1.变量的作用域

2.变量的生存周期

3.闭包的更多作用

4.闭包和面向对象设计

5.用闭包实现命令模式 

6.闭包与内存管理

2.高阶函数

1.函数作为参数传递-回调函数 和 Array.prototype.sort

2.函数作为返回值输出-判断数据类型和getSingle

3.高阶函数实现AOP

4 高阶函数的其他应用

1.函数柯里化-Currying

2.函数反柯里化-uncurrying

3. 函数节流--解决不是用户直接控制

4. 分时函数--解决用户直接控制

5. 惰性加载函数


虽然 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设计模式系列—基础篇(三)闭包和高阶函数

博客地址:https://mp.csdn.net/postedit/100583697

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值