第一部分 基础知识
第 3 章 闭包和高阶函数
3.1 闭包
3.1.1 变量的作用域
变量的作用域,就是指变量的有效范围。
//作用域范围 var func = function(){ var a = 1; alert ( a ); // 输出: 1 }; func(); alert ( a ); // 输出:Uncaught ReferenceError: a is not defined
//嵌套函数 var a = 1; var func1 = function(){ var b = 2; var func2 = function(){ var c = 3; alert ( b ); // 输出:2 alert ( a ); // 输出:1 } func2(); alert ( c ); // 输出:Uncaught ReferenceError: c is not defined }; func1();
3.1.2 变量的生存周期
对于全局变量来说,全局变量的生存周期是永久的;
而对于在函数内用 var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁。
//生命周期 var func = function(){ var a = 1; // 退出函数后局部变量 a 将被销毁 alert ( a ); }; func();
3.1.3 闭包的更多作用
1. 封装变量
//1. 封装变量 var mult = (function(){ var cache = {}; var calculate = function(){ var a = 1; for(var i = 0,l = arguments.length;i<l;i++){ a = a * arguments[i]; console.log(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( 1,2,3 )) console.log(mult( 1,2,3 ))
2. 延续局部变量的寿命
3.1.4 闭包和面向对象设计
3.1.5 用闭包实现命令模式
命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。
<!--命令模式--> <div> <button id='open'>打开</button> <button id='close'>关闭</button> </div>
//命令模式 var Tv = { open:function(){ console.log("打开电视机"); }, close:function(){ console.log("关闭电视机"); } } var creatCommand = function(receiver){ var open = function(){ return receiver.open(); } var close = function(){ return receiver.close(); } return { open:open, close:close } } var setCommand = function(command){ document.getElementById("open").onclick = function(){ command.open(); } document.getElementById("close").onclick = function(){ command.close(); } } setCommand(creatCommand(Tv))
3.1.6 闭包与内存管理
如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null即可。
3.2 高阶函数
高阶函数是指至少满足下列条件之一的函数。
3.2.1 函数可以作为参数被传递;
1. 回调函数
2. Array.prototype.sort
3.2.2 函数可以作为返回值输出。
1. 判断数据的类型
//判断数据的类型 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' ); console.log( isArray( [ 1, 2, 3 ] ) ); // 输出:true
//判断数据的类型 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([ 1, 2, 3 ] )); console.log(Type.isString('str'));
2. getSingle单例模式
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(); console.log(script1);
3.2.3 高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,包括日志统计、安全控制、异常处理等。好处是保持业务逻辑模块的纯净和高内聚性。
在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 ref = _self.apply(this, arguments); afterfn.apply(this.arguments); return ref; } } var func = function(){ console.log(2); } func = func.before(function(){ console.log(1); }).after(function(){ console.log(3); }) func();
3.2.4 高阶函数的其他应用
1. currying
函数柯里化(function currying):currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数后,该函数不会立即求值,而是返回另一个函数,刚才传入的参数在函数中形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性求值。
var currying = function(fn){ var args = []; return function(){ if(arguments.length === 0){ return fn.apply(this, args); }else{ [].push.apply(args,arguments); return arguments.callee; } } } var cost = (function(){ var money=0; return function(){ for(var i = 0,l=arguments.length;i<l;i++){ money += arguments[i]; } return money; } })() var cost = currying(cost);//转化成currying函数 cost(600);//未真正求值 cost(900);//未真正求值 console.log(cost())//求值
2. uncurrying
在javaScript中,当我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有该方法,这是动态类型语方的特点,也就是鸭子类型思想。一个对象未必只能使用它自身的方法,可以让对象去借用一个原本不属于它的方法。call和apply都可以完成这个需求:
//借用call、apply var obj1 = { name:'name1' } var obj2 = { getName:function(){ return this.name; } } console.log(obj2.getName.apply(obj1));//obj1借用obj2的getName方法 // Array.prototype (function(){ Array.prototype.push.call(arguments,4,5);//arguments借用Array.prototype.push方法 console.log(arguments) })(1,2,3)
//把泛化this的过程提取出来 Function.prototype.currying = function(){ var _self = this;// self 此时是 Array.prototype.push return function(){ var obj = Array.prototype.shift.call(arguments);//arguments 对象的第一个元素被截去 return _self.apply(obj,arguments);// 相当于 Array.prototype.push.apply( obj, 2 ) } } for(var i=0,fn, ary = ['push','shift','forEach'];fn=ary[i++];){ Array[fn] = Array.prototype[fn].currying(); } var obj = { 'length':'3', '0':1, '1':2, '2':3 } Array.push(obj,4);//向对象中添加一个元素 console.log(obj); var first = Array.shift(obj);//截取第一个元素 console.log(first);//输出:1 Array.forEach(obj,function(item,i){ console.log(item);//输出2,3,4 })
//uncurrying另一种实现方式 Function.prototype.currying = function(){ var _self = this; return function(){ return Function.prototype.call.apply(_self,arguments); } }
3. 函数节流
函数的触发不是由用户直接控制的时候,函数有可能会频繁的被调用,造成大的性能问题。
(1)函数被频繁调用的场景:
window.onresize 事件。当浏览器窗口大小被拖动而改变的时候。
mousemove 事件。拖 拽事件。
上传进度。
(2) 函数节流的原理:
上述三个场景面临的共同问题是函数被触发频率太高。可以借助setTimeout来完成这件事情。
(3) 函数节流的代码实现
//函数节流 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) } }
4. 分时函数
由于用户调用的,导致一些函数严重影响页面性能。
/* * 分时函数 * ary:创建节点时用到的数据 * fn:封装创建节点辑的函数 * count每一批创建节点的数量 */ var timeChunk = function(ary, fn, count){ var timer; var start = function(){ for(var i = 0;i<Math.min(count || 1,ary.length);i++){ var obj = ary.shift(); fn(obj); } }; return function(){ timer = setInterval(function(){ if(ary.length === 0){//如果全部节点创建完毕 return clearInterval(timer); } start(); },200) } var ary = []; for(var i = 0;i<1000;i++){ ary.push(i); } var renderFriendList = timeChunk(ary,function(node){ var div = document.createElement('div'); div.innerHTML = node; document.body.appendChild(div); },8); renderFriendList();
5. 惰性加载函数
避免每次执行都会进行判断,让程序避免这些重复的执行过程。
//惰性加载函数 var addEvent = function(elem, type, handler){ if(window.addEventListener){
//对addEvent函数进行重写,当下次再进入addEvent的时候不再重复进行判断 addEvent = function(elem, type, handler){ elem.addEventListener(type, handler, false); } }else if(window.attachEvent){ addEvent = function(elem, type, handler){ elem.attachEvent('on'+type,handler,false); } } addEvent(elem,type,handler); }; var div = document.getElementById('div1'); addEvent(div,'click',function(){ console.log(1); }); addEvent(div,'click',function(){ console.log(2); });
3.3 小结
许多设计模式在 JavaScript 之中的实现跟在一些传统面向对象语言中的实现相差很大。在JavaScript 中,很多设计模式都是通过闭包和高阶函数实现的。