本文共 12883 字,大约阅读时间需要 42 分钟。
第3章 闭包和高阶函数
3.1 闭包
对于程序员来说闭包(closure)是一个难懂又必须征服的概念。闭包的形成与变量的作用域以及变量的生存周期密切相关。(虽然前面写了好多遍了,但还是过一遍吧,不想看就跳过这一节)
3.1.1作用域
函数中用var声明的变量是只有在函数内部访问得到,函数外部访问不到的。在javascript中,函数可以用来创造函数作用域。此时的函数像一个半透明的玻璃,在函数里可以看到外面的变量,而在函数外面则无法看到函数里面的变量。如果该函数内并没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止。
3.1.2 变量的生存周期
对于全局的作用域,生存周期当然是永久的,除非我们主动销毁这个全局变量。而对于函数内用var关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁。
- var func = function(){
- var a=1;
- alert(a);
- }
func(); //退出函数后局部变量a将被销毁
再看下面的:
- var func = function(){
- var a = 1;
- return function(){
- a++;
- alert(a);
- }
- };
- var f = func();
- f();
- f();
..
跟我们之前的推论相反,当退出函数后,局部变量a并没有消失,而是似乎一直在某个地方存活着。这是因为当执行var f=func();f指向了一个匿名函数的引用,而这个引用可以访问到func()被调用时产生的环境,而且局部变量a一直处在这个环境里。既然这个局部变量在环境还能被外界访问,这个局部变量就有了不被销毁的理由。
如:
- 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)
- }
-
- Type.isArray([]);
- Type.isString(“str”)
3.1.3 闭包的更多作用
1. 封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”
如:
- var mult = function(){
- var a=1;
- for(vari=0,l=arguments.length;i<l;i++){
- a = a*arguments[i];
- }
- return a;
- }
该函数每次都计算其实是一种浪费,我们可以添加一个缓存机制,使其不必每次都进行计算
- var cache={};
- var mult = function(){
- var args =Array.prototype.join.call(arguments,”,”);
- if(cache[args]){
- return cache[args];
- }
- var a=1;
- for(vari=0,l=arguments.length;i<l;i++){
- a = a*arguments[i];
- }
- return cache[args]=a;
- }
- alert(mult(1,2,3));
- alert(mult(1,2,3));
当看到cache这个变量仅仅在mult函数中被使用,与其让cache变量跟mult函数一起平行地暴露在全局作用域下,不如把它封闭在mult函数的内部。如下:
- var mult = (function(){
- var cache = {};
- return function(){
-
- var args =Array.prototype.join.call(arguments,”,”);
- if(cache[args]){
- return cache[args];
- }
- var a=1;
- for(vari=0,l=arguments.length;i<l;i++){
- a = a*arguments[i];
- }
- return cache[args]=a;
- }
- })();
提炼函数是代码重构中一种常见技巧。如果在一个大函数中有一些代码能够独立出来,我们常常把这些代码封装在独立的小函数里面。独立出来的小函数有利于代码复用,如果这些小函数有一个良好的命名,我们本身也起到注释的作用。如果这些小函数不需要在程序的其它地方使用,最好是把它们用闭包封闭起来。
如下:
- var mult = (function(){
- var cache = {};
- var calculate = function(){
- var a=1;
- for(vari=0,l=arguments.length;i<l;i++){
- a = a*arguments[i];
- }
- return a;
-
- };
- return function(){
-
- var args =Array.prototype.join.call(arguments,”,”);
- if(cache[args]){
- return cache[args];
- }
- return cache[args]= calculate.apply(null,arguments);
-
- }
- })();
2. 延续局部变量的寿命
img对象经常用于进行数据上报,如
- var report =function(src){
- var img = new Image();
- img.src = src;
- }
这种方式会在低版本浏览器中会存在问题,原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随即被销毁,而此时或许还没来得及发出HTTP请求,所以此次请求会丢失掉。如下解决
- var report =(function(){
- var imgs =[];
- return function(src){
- var img = new Image();
- imgs.push(img);
- img.src = src;
- }
- })();
3.1.4 闭包和面向对象设计
过程与数据的结合是形容面象对象中的“对象”时经常使用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用闭包也能实现。反过来也一样的!!!
如下:
- var extent = function(){
- var value = 0;
- return {
- call:function(){
- value++;
- console.log(value);
- }
- }
- }
- var extent = extent();
- extent();
- extent();
- extent();
如果换页面象对象的写法如下:
- var extent = {
- value:0,
- call:function(){
- this.value++;
- console.log(this.value);
- }
- }
- extent.call();
- extent.call();
- extent.call();
或
- 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();
3.1.5 用闭包实现命令模式
命令模式的意图是把请求封闭为对象,从而分离请求的发起者和请求的接收者之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。
前:
- var Tv = {
- open:function(){
- console.log(“打开电视机”);
- },
- close:function(){
- console.log(“关闭电视机”);
- }
- };
-
- var OpenTvCommand = function(receiver){
- this.receiver = receiver;
- }
- OpenTvCommand.prototype.execute =function(){
- this.receiver.open();
- }
- OpenTvCommand.prototype.undo = function(){
- this.receiver.close();
- }
- var setCommand = function(command){
- document.getElementById(‘execute’).onclick = function(){
- command.execute();
- };
- document.getElementById(‘undo’).onclick = function(){
- command. undo();
- };
-
- }
-
- setCommand(new OpenTvCommand(Tv));
做为函数是一等对象的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(setCommand(Tv));
3.2 高阶函数
高阶函数是指至少满足下列条件之一的函数:
o 函数可以作为参数被传递
o 函数可以作为返回值输出
3.2.1 函数作为参数传递
把函数作为参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要的应用场景就是常见的回调函数。
1. 回调函数
在ajax异步请求的应用中,回调函数的使用非常频繁。当我们想在ajax请求返回之后做一些事情,但又并不知道请求返回的确切时间,最常见的方案就是把callback函数作为参数传入发起ajax请求的方法中,待请求完成之后执行callback函数:
- var getUserInfo= function(userId,callback){
- $.ajax(‘http:
- if(typeof callback===’function’){
- callback(data)
- }
- });
- );
- }
2. Array.prototype.sort
该函数接受一个函数作为参数,这个函数里面封装了数组元素的排序规则。我们的目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分。
3.2.2 函数作为返回值输出
相比把函数当作参数传递,函数作为返回值输出的场景也许更多。也更能体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。
1. 判断数据类型
- var isString =function(obj){
- returnObject.prototype.toString.call(obj)===’[object String]’;
- }
- var isArray = function(obj){
- returnObject.prototype.toString.call(obj)===’[object Array]’;
- }
- var isNumber =function(obj){
- returnObject.prototype.toString.call(obj)===’[object Number]’;
- }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
我们发现,这些函数的大部分实现都是相同的,不同的只是Object.prototype.toString.call(obj)返回的字符串。为了避免多余的代码,我们尝试把这些字符串作为参数提前值入isType函数
- var isType = {};
- for(vari=0,type;type = [‘String’,’Array’,’Number’][i++];){
- (function(type){
- Type[‘is’+type]=function(obj){
- returnObject.prototype.toString.call(obj)===’[object ‘+type+’]’;
- }
- })(type)
- }
-
- Type.isArray([]);
- 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();
- var script2 =getScript();
- alert(script1===script2);
3.2.3 高阶函数实现AOP
AOP的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地利用日志统计等功能模块。
通常,在javascript中实现AOP,都是指把一个函数“动态织入”到另外一个函数之中,具体的实现技术有很多。
- Function.prototype.before =function(beforefn){
- var __self=this;
- return function(){
- beforefn.applty(this,arguments);
- 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();
3.2.4 高阶函数的其它应用
1. currying(函数柯里化)
currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的函数形成闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。看看演化过程
- var monthlyCost= 0;
- var cost =function(money){
- monthlyCost+=money;
- }
- cost(100);
- cost(100);
- cost(100);
- …
- cost(100);
如果在每个月的前29天,我们都只保存好当天的开销,直到30天才进行求值计算,这样的就达到了我们的要求。
- var cost=(function(){
- var args = [];
- return function(){
- if(arguments.length==0){
- var money = 0;
- for(var i=0,l=args.length;i<l;i++){
- money+=args[i];
- }
- return money;
- }else{
- [].push.apply(args,arguments);
- }
- }
- })();
- cost(100);
- cost(200);
- console.log(cost());
接下来我们编写一个通用的function currying(){},functioncurrying(){}接受一个参数,即将要被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(vari=0,l=arguments.length;i<l;i++){
- money+=arguments[i];
- }
- return money;
- }
- })();
- var cost =currying(cost);
- cost(100);
- cost(200);
- cost(300);
- alert(cost());
2. uncurrying
在javascript中,我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点,也是学说的鸭子类型思想。如:
- var obj1={
- name:’sven’
- }
- var obj2 = {
- getName:function(){
- return this.name;
- }
- };
- console.log(obj2.getName.call(obj1))
call和apply可以把任意对象当作this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性。
那么有没有办法把泛化this的过程提取出来呢?uncurrying就是用来解决这个问题的。
- Function.prototype.uncurrying= function(){
- var self = this;
- return function(){
- var obj =Array.prototype.shift.call(arguments);
- return self.apply(obj,arguments);
- }
- }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
把Array.prototype.push.call这句代码转换为一个通用的push函数:
- var push =Array.prototype.push.uncurrying();
- (function(){
- push(arguments,4);
- console.log(arguments);
- })(1,2,3)<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
3. 函数节流
1) 函数被频繁调用的场景
window.resize事件。当浏览器窗口大小被搬动而改变的时候,这个事件触发的频率非常之高。如果我们在window.onresize事件函数里有一些跟dom节点相关的操作,而往往dom节点的相关操作是非常消耗性能的。这时浏览器会出现卡顿现象。
mousemove事件
上传进度(使用插件,一秒轮询10次)。
2) 函数节流的原理
我们发现共性是函数被触发的频率太高。而我们实际上只需要2次或者3次。就需要我们来忽略一些事件请求。可以借助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);
- }
- }
-
- window.οnresize= throttle(function(){
- console.log(1);
- },500)();<span style="font-weight: bold; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
4) 分时函数
节流的讨论中,我们提供了一种限制函数被频繁调用的解决方案。下面我们将遇到另一个问题,某函数确实是用户主动调用的,但因为客观原因,这些函数会严重地影响页面性能。
如:一次性在web页面上添加上千个dom节点。往往我们看到的结果就是浏览器卡顿甚至假死
- var ary=[];
- for(vari=0;i<=1000;i++){
- ary.push(i);
- }
- var renderFriendList = function(data){
- for(var i=0,l=data.length;i<l;i++){
- var div =document.createElement(‘div’);
- div.innerHTML = I;
- document.body.appendChild(div);
- }
- }
- renderFriendList(ary);
这个问题的解决方案之一是下面的timeChunk函数,timeChunk 函数让创建节点的工作分批进行,比如1秒钟创建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=0;i<1000;i++){
- ary.push(i);
- }
- var renderFirendList = timeChunk(ary,function(n){
- var div = document.createElement('div');
- div.innerHTML = n;
- document.body.appendChild(div);
- },8);
- renderFirendList();
5) 惰性加载函数
在web开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免的。如我们需要一个在各个浏览器中能够能用的事件绑定函数addEvent如
- var addEvent =function(elem,type,handler){
- if(window.addEventListener){
- return elem.addEventListener(type,handler,false);
- }else if(window.attachEvent){
- return elem.attachEvent(‘on’+type,handler);
- }
- }
这个函数的缺点是,当它每次被调用时都会执行里面的if分支,虽然执行这些if分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。
第二种方案是这样,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断以便让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);
- }
- }
- })();<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
目前的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);
- }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
调用:
- var div =document.getElementById(‘xx’);
- addEvent(div,’click’,function(){
- alert(1);
- });
- addEvent(div,’click’,function(){
- alert(2);
- });
转载地址:http://xgzxi.baihongyu.com/