闭包和高阶函数
看本文之前需要有JavaScript的基础知识
一、闭包
变量的作用域
当在函数中声明一个变量的时候,如果该变量前面没有带上关键字
var
,这个变量就会成为全局变量。另一种情况是用var
关键字在函数中声明变量,这时候变量即是局部变量。在JavaScript中,函数可以用来创造函数作用域,此时的函数就像一层半透明的玻璃,在函数里面可以看到外面的变量,而在函数外面无法看到函数里面的变量。这是因为当在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止。变量的搜索是从内到外而非从外到内的。
var a = 1;
var func1 = function(){
var b = 2;
var func2 = function(){
var c = 3;
alert(b); // 2
alert(a); // 1
}
func2();
alert(c); // c is not defined
};
func1();变量的生存周期
var func function(){
var a = 1; // 退出函数后局部变量a将被销毁
alert(a);
};
func();var func function(){
var a = 1;
alert(a);
return function(){
a++;
alert(a);
}
};
var f = func();
f(); // 2
f(); // 3
f(); // 4
f(); // 5当退出函数后,局部变量a并没有消失,而是似乎一直在某个地方存活着。这是因为当执行了
var f = func()
时,f返回了一个匿名函数的引用,它可以访问到func()
被调用时产生的环境,二局部变量a一直处于这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,局部变量的生命看起来被延续了。闭包的更多作用
闭包的面向对象设计
对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。
下面看看这段跟闭包相关的代码:
var extent = function(){
var value = 0;
return {
call: function(){
value++;
console.log(value);
}
}
};
var extent = extent();
extent.call(); // 1
extent.call(); // 2
extent.call(); // 3如果换成面向对象的写法,就是:
var extent = {
value: 0;
call: function(){
this.value++;
console.log(this.value);
}
};
extent.call(); // 1
extent.call(); // 2
extent.call(); // 3用闭包实现命令模式
命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。
闭包与内存管理
局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。使用闭包的原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的。不能说成是内存泄露。
跟闭包和内存泄漏有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄漏,但这本身并非闭包问题,也并非JavaScript的问题,在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。
同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为null即可。将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
二、高阶函数
高阶函数是指至少满足下列条件之一的函数:
函数可以作为参数被传递
函数可以作为返回值输出
一个函数可以接收另一个函数作为参数
函数作为参数传递
回调函数
在ajax异步请求的应用中,回调函数的使用非常频繁。当我们想在ajax请求返回之后做一些事情,但又并不知道请求返回的确切时间时,最常见的方案就是把callback函数当作参数传入发起ajax请求的方法中,待请求完成之后执行callback函数:
var getUserInfo = function(userId, callback){
$.ajax('http://xxx.com/getUserInfo?'+userId,function(data){
if(typeof callback === 'function'){
callback(data);
}
});
}
getUserInfo(13157,function(data){
alert(data.userName);
});回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这些请求封装成一个函数,并把它作为参数传递给另外一个函数,”委托”给另外一个函数来执行。
Array.prototype.sort
Array.prototype.sort
接受一个函数当作参数,这个函数里面封装了数组元素的排列顺序。从Array.prototype.sort
的使用可以看到,我们的目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分。把可变的部分封装在函数的参数里,动态传入Array.prototype.sort
,使Array.prototype.sort
方法成为了一个非常灵活的方法,代码如下:[1, 4, 3].sort(function(a, b){
return a - b; // 从小到大排
});
函数作为返回值输出
判断数据类型
判断一个数据是否是数组,在以往的实现中,可以基于鸭子类型的概念来判断,比如判断这个数据有没有length属性,有没有sort方法或者slice方法等。但更好的方式是用
Object.prototype.toString
来计算。Object.prototype.toString.call(obj)
返回一个字符串,比如Object.prototype.toString.call([1,2,3])
总是返回"[object Array]"
。所以我们可以编写一系列的isType函数:var isString = function(obj){
return Object.prototype.call(obj) === '[object String]';
};
var isArray = function(obj){
return Object.prototype.call(obj) === '[object Array]';
};
var isNumber = function(obj){
return Object.prototype.call(obj) === '[object Number]';
};getSingle
这个高阶的例子,既把函数当作参数传递,又让函数执行后返回了另一个函数。可以看看getSingle的效果:
var getScript = getSingle(function(){
return document.createElement('script');
});
var script1 = getScript();
var script2 = getScript();
alert(script1 === script2); // true
高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来后,再通过”动态织入”的方式掺入业务逻辑模块中。
高阶函数的其他应用
currying(柯里化函数)
currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来,待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
假设我们要编写一个计算每个月开销的函数。在每天结束之前,我们都要记录今天花掉了多少钱:
var monthlyCost = 0;
var cost = function(money){
monthlyCost += money;
};
cost(100); // 第一天开销
cost(200); // 第二天开销
cost(300); // 第三天开销
//cost(700); // 第三十天开销
alert(monthlyCost); // 600每天结束后我们都会记录并计算到今天为止花掉的钱。但我们其实并不太关心每天花掉了多少钱,而只想知道月底的时候会花掉多少钱。也就是说,实际上只需要在月底计算一次。
如果在每个月前29天,我们都只是保存好当天的开销,直到第30天才进行求值计算,这样就达到了我们的要求。
// 接受一个函数fn作为参数,返回一个新函数(会收集所有传入的参数,并将它们一次性应用到fn函数上
var currying = function(fn){
// 存储传递给柯里化函数的参数
var args = [];
return function(){
// 如果没有传入参数,就将存储到args中的参数一次性应用到fn函数上
if(arguments.length === 0){
// 调用fn函数,将args数组中的所有参数传递给fn
return fn.apply(this, args);
}else{
// 如果传递了参数则将参数增加到args中
[].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);
cost(100);
cost(200);
cost(300);
alert(cost());当调用cost()时,如果明确地带上了一些参数,表示此时并不进行真正的求值计算,而是把这些参数保存起来,此时让cost函数返回另外一个函数。只有当我们以不带参数的形式执行cost()时,才利用前面保存的所有参数,真正开始进行求值计算。
uncurrying
在类数组对象arguments借用
Array.prototype
的方法之前,先把Array.prototype.push.call
这句代码转换为一个通用的push函数:var push = Array.prototype.push.uncurrying();
(function(){
push(arguments, 4);
console.log(arguments); // [1, 2, 3, 4]
})(1, 2, 3);通过uncurrying的方式,
Array.prototype.push.call
变成了一个通用的push函数。这样一来。push函数的作用就跟Array.prototype.push
一样了,同样不仅仅局限于只能操作array对象。函数节流
函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则我们一般不会遇到跟性能相关的问题。但是在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。
函数被频繁调用的场景
window.onresize事件
mousemove事件
上传进度
函数节流的原理
上面提到的三个场景,发现他们面临的共同问题是函数被触发的频率太高。通过使用定时器来控制函数的频率。
分时函数
某些函数确实是用户主动调用的,但因为一些客观的原因,这些函数会严重影响页面性能。处理大量数据或高频率事件时,通过将任务拆分成多个小块来提高性能和响应速度的技术,确保在执行复杂的操作时不会阻塞浏览器UI线程。
比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点。
惰性加载函数
函数执行的分支只会在函数第一次调用才执行。