一 背景
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化。可以通过将一个函数参数部分应用到函数中,从而动态的创建一个新函数,这个新函数将会保存重复的参数,因此不必每次都传递这些重复的参数