博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Javascript模式(第四章函数)------读书笔记

Posted on 2015-10-27 13:35  Amy-lover  阅读(341)  评论(0编辑  收藏  举报

一 背景

js函数的两个特点:1 函数是第一类对象(first-class object);2 函数可以提供作用域

  1 函数是对象:

    1 函数可以在运行时动态创建,还可以在程序执行过程中创建

    2 可以被赋值给变量,还可以被删除

    3 可以作为参数传递给别的函数,可以作为返回值,被别的函数返回,

    4 可以拥有自己的属性和方法

  2 由于JS没有块级作用域的概念,因此在涉及到控制变量作用域的时候,函数是必不可少的工具

  1.1 消除术语歧义:我们来看一下命名函数表达式、你们函数表达式以及函数声明的定义

    1 一般的函数表达式即为匿名函数表达式,简称匿名函数,即var add=function(){};

    2 命名函数表达式:var add1=function add(){};

    3 命名函数表达式是函数表达式的一种特殊情况,命名函数表达式与函数表达式的区别在于:其name属性

    4 函数声明:function add(){}

var add1=function add(){};//命名函数表达式
console.log(add1.name);//add


//函数表达式/匿名函数表达式/匿名函数
var add2=function (){};
console.log(add2.name);//""


//函数声明
function add3(){} 
console.log(add3.name);//add3

 

  1.2 声明VS表达式:名称与变量声明提升

  函数声明只能出现在“程序代码”中,即函数声明只能存在于其他函数体内或者全局空间中,它们不能分配给变量或者某个属性,也不能作为参数出现在函数调用中

//命名函数表达式
callMe(function me(){
});

//匿名函数表达式
callMe(function(){
});


//函数表达式
var obj={
   say:function(){
    } 
};

 

  1.3 函数的name属性

  函数的name属性是只读的

  name属性的用途:调试代码的时候,可以根据name属性作为一个标识符;可以用于自身的递归运算

  1.4、函数的提升

  虽然函数声明与函数命名表达式很相似,但是二者之间还是有很大区别的,这个区别就是函数的提升

  我们知道对于所有变量,无论是在函数体的何处进行的声明,都会在后台被提升到函数的顶部,函数声明也一样,无论函数声明在何处,都会被提升到顶部,

  而函数表达式不会得到提升,下面我们来看一个例子

var name="Jim";
fun();//函数声明!
var fun=function(){
    alert("函数表达式!");
}
fun();//函数表达式!
function fun(){
    alert("函数声明!");
}
fun();//函数表达式! 

二 回调模式

  2.1 回调模式:将函数A作为参数传递给函数B,且A在函数B中得到执行,那么函数A就被称之为回调函数,该模式称之为回调模式

function A(){
    console.log("I am a callback !");
}
function B(callback){
    if(typeof callback!=="function"){
        callback=false;
    }
    if(callback){
        callback();
    }
}
B(A);//I am a callback ! 

  2.2 回调示例

  假设我们需要抓取页面上的DOM树,并返回相应的DOM数组,对该数组里的DOM进行操作,例如隐藏

  根据函数的通用性,一个函数是找到相应的DOM,并返回数组(findNodes),另外一个函数是隐藏功能(hide)

  先执行findNodes,然后获取到返回的DOM数组,再进行for循环进行遍历,隐藏,效率有些低下,我们采取回调模式

  

var findNodes(callback){
    var nodes=[],found;
    if(typeof callback!="function"){
        callback=false;
    }
    while(条件为true){
        find;//......查找相关节点的操作
        if(callback){
            callback(found);
        }
    }
    return nodes;    
};
findNodes(hide);

 

  2.3、回调模式与作用域

问题:在某些情况下,我们的回调函数不是一次性的匿名函数也不是全局函数,而是某个对象的方法,如果在该方法中使用了this来引用它所属的对象,将会出现以下问题

color="black";
//自定义对象myapp
var myapp={
    color:"red",
    paint:function(arg){
        arg=this.color;
        //将myapp的color赋值给参数的color属性
        console.log(arg);
    }
};

function B(callback){
    if(typeof callback!=="function"){
        callback=false;
    }
    var B_color={};
    if(callback){
        callback(B_color);
    }
}
B(myapp.paint);//black
//由于B函数是一个全局函数,因此,对象中的this指向全局对象window,而不是预期的myapp 

解决方案:将回调函数及其所属对象一并传递给函数B

color="black";
var myapp={
         color:"red",
         paint:function(arg){
                   arg=this.color;
                   console.log(arg);
         }
};
//将回调函数callback与回调函数所属的对象一并传递进去
function B(callback,callback_obj){
         var B_color;
         if(typeof callback==="function"){
                   callback.call(callback_obj,B_color);    
                   //注意,这里不再只是单纯的直接调用回调函数,而是回调函数所属的对象对回调函数加以调用            
         }
}

B(myapp.paint,myapp);//red
 

 进一步优化方案:由上面可以看出,在调用myapp的paint方法时,需要输入两次对象名myapp,我们可以对其进行如下优化,将该方法作为字符串来传递,无需两次输入该对象的名称

function B(callback,callback_obj){
    var B_name={};
    if(typeof callback==="string"){
        callback=callback_obj[callback];
    }
    if(typeof callback==="function"){
        callback.call(callback_obj,B_name);
    }
}
B("paint",myapp);//red

 

 六、回调函数的用途

  1 异步事件监听器

  例如:给页面元素提供一个回调函数的指针,使得当该事件触发时可以得到调用,该模式支持异步方式,即允许以乱序的方式运行

  2  超时调用

  当使用window提供的超时方法:setTimeout和setInterval中的参数是一个函数指针,也使用了回调模式,在某一个时刻触发

     3  JS库中回调模式的应用

  在设计JS库的时候,回调模式可以帮助JS库实现通用性,使开发者不必预测和实现每一个功能

  因为一方面过多的功能会使JS库过于庞大,另一方面,有些功能可以绝大多数用户永远也不会使用到。

  因此在开发js库的时候:专注于核心功能的开发,提供“挂钩”形式的回调函数,使得JS库可以很容易的扩展。

三 返回函数:函数是一个对象,因此可以作为返回值

  1   应用场景:一个函数执行一部分工作,这些工作可能包含一些一次性的初始化,后续调用它的时候,是其返回值,其返回值也是一个函数,后续操作就由其返回函数来执行

function init(){
    console.log(1);
    return function(){
        console.log(2);
    };
};
var s=init();//1
s();//2 

注意:init函数返回了一个匿名函数,即创建了一个闭包,(这里提到闭包的一个作用:创建私有数据,只有该匿名函数可以访问,外部代码不能访问),下面我们来看一下对该特点的应用

function init(){
    var count=0;
    return function(){
        return count+=1;
    };
};
var s=init();
console.log(s());//1
console.log(s());//2
console.log(s());//3 

四 自定义函数(惰性函数模式):一个函数有一些初始化的准备工作,且只需要执行一次,使用自定义函数模式可以使重新定义的函数执行更少的工作

function init(){
    console.log("Boo!");
    init=function(){
        console.log("Boo Boo!");
    };
};
init();//Boo!
init();//Boo Boo!
init();//Boo Boo! 

  2   该模式又被称为惰性函数模式,即该函数直到第一次使用才会被正确定义,并且具有后向惰性,即得到正确定义后,会执行更少的工作  

  3   该模式的缺陷:当它重新定义自身时已经添加到原函数的任何属性都会丢失,如果再将其赋值给其他变量,那么使用新的变量来调用该函数的话,重定义的部分永远也得不到执行

var a,b;
a=b=function(){
    console.log("1");
    b=function(){
        console.log("2");
    }
};
b.age=18;
a();//1
a();//1
b();//2
b();//2
console.log(b.age);//undefined

 

五 即时函数:函数创建后立即执行该函数的语法

         1  该模式的实现方法:使用函数表达式定义一个函数;在该函数表达式末尾加一组括号,让其立即执行;将这个包装到括号中

(function(){
    console.log("Oops!");
})(); 

  2   应用场景:该模式提供了一个作用域沙箱,当页面加载时,代码必须执行一些设置任务,例如设置事件监听器、创建对象等,但这些工作只需要执行一次,因此没有 必要定义一个可复用的函数,如果写在全局作用域下,有可能初始化操作还需要一些临时变量,这些变量可能会污染全局作用域,因此我们可以使用即时函数模式, 将所有临时变量包装到它的局部作用域中

(function(){
    var days=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
    today=new Date(),
    msg="今天"+days[today.getDay()];//这里days,today以及msg都是临时变量
    console.log(msg);// 今天星期五
})(); 

  5.1 即时函数的参数:这里不建议过多的参数传递给即时函数,避免造成阅读负担

(function(who,when){
    var days=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];    
    console.log("我在"+days[when.getDay()]+"遇见了"+who);
})("我的偶像",new Date());//我在星期五遇见了我的偶像 

  4    另外,应该注意全局对象也可以作为参数传递给即时函数,为了使代码可以在浏览器之外的环境有更好的互操作性,这里不建议在即时函数内部使用window

(function(global){
    //通过global访问全局变量
})(this);

   5.2 即时函数的返回值

var dd=(function(){
  var count=0;
  return function(){
    count++;
    console.log(count);
  };
})();
dd();//1
dd();//2
dd();//3
console.log(count);//ReferenceError: count is not defined

 

   5.3 优点和用法

  不会污染全局空间,用于书签工具,因为书签工具可以在任何网页上运行,并保持全局命名空间的整洁;确保页面在存在或不存在该代码的两种情况下都能良好运行

六 即时函数对象化

   1 保护全局作用域不被污染的方法,除了上面的即时执行函数,还有即时对象初始化模式

  2 该模式的init方法在创建对象后将会立即执行,init方法负责所有的初始化任务

  3 缺点:js压缩不能有效的缩减代码

({
    max:600,
    min:400,
    getMax:function(){
        return this.max;
    },
    init:function(){
        console.log(this.getMax());
        return this;//如果想保存对该对象的一个引用,可以返回this
    }
}).init();

 

七 初始化时分支:加载时分支

当知道某个条件在整个程序的生命周期内是不会发生改变的时候,仅对该条件进行一次测试即可,例如浏览器嗅探(浏览器版本的检测)等

 第一个demo,我们在使用utils.addListener()函数的时候,每次调用该函数都需要去typeof window.addEventListener....

但是第二个demo,我们在使用utils.addListener()函数的时候,我们只需要进行一次到底是window.addEventListener还是document.attachEvent还是el["on"+type]

/*每次调用utils.addListener方法给dom元素绑定事件的时候,都需要对其进行检测
typeof window.addEventListener
typeof document.attachEvent
*/
var utils={
    addListener:function(el,type,fn){
        if(typeof window.addEventListener==="function"){
            el.addEventListener(type,fn,false);
        }else if(typeof document.attachEvent==="function"){
            el.attachEvent('on'+type,fn);
        }else{
            el["on"+type]=fn;
        }
    }
}; 
/*
typeof window.addEventListener
typeof document.attachEvent
只需要执行一次就可以了
*/
var utils={
    addListener:null
};
if(typeof window.addEventListener==="function"){
    utils.addListener=function(el,type,fn){
        el.addEventListener(type,fn,false);
    };
}else if(typeof document.attachEvent==="function"){
    utils.addListener=function(el,type,fn){
        el.attachEvent('on'+type,fn);
    }
}else{
    utils.addListener=function(el,type,fn){
        el["on"+type]=fn;
    }
} 

八 函数属性---备忘模式

   1 函数是对象,因此可以拥有属性,使用任何语法定义的函数都会自动获取一个length的属性,该属性是函数期望的参数的数量

  2 自定义一个属性,用来缓存函数的结果,下次调用函数的话就不需要做潜在的繁重的计算了,这种缓存函数就诶过的方式称之为备忘

  3 但是如果有两个名称一致,但是值不一致的话,就会得不到想要的结果

var fun=function(param){
    if(!fun.cache[param]){
        var result={};
        //...计算
        fun.cache[param]=result;
    }
    return fun.cache[param];
}
fun.cache={};

九 配置对象

随着需求的不断变化,我们所需要的参数可能不断增多,这样我们向构造函数传递的参数也越来越多,参数会越来越长,实参与形参的顺序,也必须保持一致

例如addPerson(firstName,lastName,age,gender,address,telphone,birthday,.......);

配置对象模式

addPerson(conf);
conf={
    firstName:"...",
    lastName:"...",
    age:"...",
    gender:"...",
    address:"...",
    telphone:"...",
    birthday:"..."
};
/*优点:
    不需要记住众多参数及其顺序,
    可以忽略可选参数
    易于阅读和维护
    易于添加和删除
缺点:
    需要记住参数的名称
    属性名称不能压缩
*/

 

十 curry

  什么是curry,术语:一个转换过程,即我们执行函数转换的过程

  当我们调用某一个函数的时候,发现多个调用函数的参数大部分都一致,我们想这些函数只执行一遍,执行其中相同的一部分,然后再各自执行剩余的部分

  例如add(1,2,3,4,5,6,10);add(1,2,3,4,5,6,100);add(1,2,3,4,5,6,1000);add(1,2,3,4,5,6,10000),其中前面的6个参数都是一致的,这种情况下,我们先执行add(1,2,3,4,5,6),然后再各自与add(1,2,3,4,5,6)相加

 

function add(a,b,c,d,e,f,g){
    return a+b+c+d+e+f+g;
}
add(1,2,3,4,5,6,10);
add(1,2,3,4,5,6,100);
add(1,2,3,4,5,6,1000);
add(1,2,3,4,5,6,10000);

/*下面只是一个示意图或者说我们想要的一个效果图,
并不是真正的要这样计算

*/ var newAdd=add(1,2,3,4,5,6); newAdd(10); newAdd(100); newAdd(1000); newAdd(10000);

 

 

 

    10.1  Curry化

/*特殊函数的curry化*/
function add(x,y){
    if(typeof y==="undefined"){
        return function(y){
            return x+y;
        };
    }
    return x+y;
}
var newAdd=add(1);
var result=newAdd(2);
console.log(result);//3

 

 

/*特殊函数的curry化*/
function add(a,b,c,d,e,f,g){    
    return a+b+c+d+e+f+g;
}

/*下面是通用的curry化*/
function schonfinkelize(fn){
    var slice=Array.prototype.slice,
    stored_args=slice.call(arguments,1);
    return function(){
        var new_args=slice.call(arguments),
        args=stored_args.concat(new_args);
        return fn.apply(null,args);
    };
}
var newAdd=schonfinkelize(add,1,2,3,4,5,6);
console.log(newAdd(10));//31
console.log(newAdd(100));//121
console.log(newAdd(1000));//1021
console.log(newAdd(10000));//10021

 

 

 

 10.2 何时使用Curry化

 当发现正在调用同一个函数,且传递的参数大多数是相同的,那么这个函数可能用于Curry化。可以通过将一个函数参数部分应用到函数中,从而动态的创建一个新函数,这个新函数将会保存重复的参数,因此不必每次都传递这些重复的参数