闭包与高阶函数
- 本文描述了作用域,闭包,内存管理的理解及高阶函数面向切面编程,节流防抖函数多种实现方案
作用域
理解作用域:var a = 2
,引擎干了那些活
-
遇见 var a,编译器会询问当前作用域是否存在该变量,存在,忽略;不存在,在当前作用域下,声明该变量,并命名为 a;
-
运行处理 a=2 时,引擎会询问当前作用域集合是否存在 a 的变量;存在就使用该变量,不存在继续向外层作用域查找该变量;找到了,将 2 赋值给 a,没找到,则抛出异常;
总结: 变量赋值会有两个动作,当前作用域声明该变量,查找变量,找到变量进行赋值; -
LHS 和 RHS 查询:LHS 可以简单说是左侧查询,RHS 右侧查询;LHS 查询,没找到变量会创建变量,RHS 查询,没找到变量抛出异常;
function(a){ var b = a; return a+b } var c = foo(2); LHS:3 RHS:4
变量作用域:
- 变量有效范围。
函数作用域:
- 函数可以用来创造函数作用域;函数可以获取外部变量,外部无法获取函数内部变量;如果函数内部没有该变量,函数会根据执行环境创造的作用域链,逐次向外层去寻找该变量。
- 下面代码,也会帮大家理解函数作用域
var a = 123; var b = 'b1' var fn = function () { var b = 1; var fn1 = function(){ var c = 'c' console.log(c) // c console.log(b) // 1 } fn1(); console.log(c) // c is not defined } fn()
变量生存周期
- 在函数内部,函数调用结束就会被销毁;函数外部全局变量,需要手动销毁,不销毁会一直存在内存中,销毁的方案:
var a = null
;
闭包
-
上面将作用域和变量生存周期简单的讲述了一下,接下来将会谈谈闭包,闭包跟这些概念非常紧密。
-
定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行;
实例:不是闭包的例子function foo() { var a = 2; function bar() { console.log( a ); // 2 } bar(); } foo();
实例:闭包效果的实例
function foo() { var a = 2; function bar() { console.log( a ); // 2 } return bar; } var baz = foo(); baz();
分析: 闭包函数外部作用域,可以访问函数内部词法作用域的变量。baz()对该作用域进行引用,这个引用就叫做闭包
-
为了更好的理解闭包,下面在写两个例子
实例 1:
function fo (){ var a = 2 function ba(){ console.log(a) } bg(ba) } function bg(fn){ fn() // 闭包 } fo()
实例 2:
var bg; function fo (){ var a = 2 function ba(){ console.log(a) } bg = ba; } function bar(){ bg() //闭包 } fo(); bar();
分析: 总而言之,闭包能将函数内部的作用域,传递到函数外部作用域,进行引用,都会使用闭包。
闭包与内存管理
- 经常也会听说闭包,会造成内存泄漏,这个说法并不正确。内存泄漏,得看根对象里有多少引用,没有被根对象引用得变量或者引用类型,都会被标记-清除算法给回收。其实大量得闭包引用被滥用,才是导致内存得元凶。函数内部得局部变量在函数调用完就会被回收。对于全局得引用变量,则是可以通过引用计数进行垃圾回收。循环引用,造成的内存泄漏,只要将循环引用类型的变量设置为null,浏览器就会通过标记清除算法,进行回收内存。记住一点,闭包并不会造成内存泄漏。
闭包封装函数
var fn4 = (function(){
var cache = {};
return function(){
var args = Array.prototype.join.call(arguments, ',')
if(cache[args]){
return cache[args]
}
var a = 1;
for(var i = 0;i<arguments.length; i++){
a = a*arguments[i]
}
return cache[args] = a;
}
})()
高阶函数
- 高阶函数可以说是日常开发中经常用的,其实可以说高阶函数是闭包的一种运用。那咱们先来看看高阶函数的定义吧;
- 定义: 函数可以作为参数被传递; 函数可以作为返回值输出;这两个条件满足其中一个就可以称为高阶函数。
- 函数作为参数,像
Array.prototype.sort
,Array.prototype.filter
等等,都属于高阶函数 - 函数作为返回值:示例,数据类型判断
var isType = function(type){ return function(obj){ return Object.prototype.toString.call(obj) === '[object '+type+']' } } var isArray = function(obj){ return isType('Array').call(null, obj); } var isNumber = function(obj){ return isType('Number').call(null, obj) }
高阶函数实现AOP编程:
AOP就是面向切面编程,把核心功能无关写逻辑抽离出来,通过动态植入方式,掺进逻辑模块中;达到模块的纯净与高内聚。
1. 示例:
Function.prototype.before = function(fn){
var that = this; // function
return function(){
fn.apply(this, arguments);
return that.apply(this,arguments)
}
}
Function.prototype.after = function(fn){
var that = this;
return function(){
var self = that.apply(this, arguments)// 执行原函数,形成链式调用
fn.apply(this, arguments) // 执行掺入函数,修正this
return self
}
}
var fng1 = function(){
console.log(1)
}
var fng2 = fng1.before(function(){
console.log(0)
}).after(function(){
console.log(2)
})
高阶函数实现柯里化函数:currerying
var cont = (function(){
var money = [];
return function(){
if(arguments.length === 0){
var mon = 0;
for(var i = 0; i<money.length; i++){
mon +=money[i]
}
return mon
}else{
Array.prototype.push.apply(money, arguments)
}
}
})()
cont(100);
cont(200);
console.log(cont())
uncurrying函数实现
Function.prototype.unCurrying = function(){
var that =this;
return function(){
var o = Array.prototype.shift.call(arguments);
return that.apply(o, arguments)
}
}
var push = Array.prototype.push.unCurrying();
var num = (function(){
push(arguments, 4)
console.log(arguments)
})(1,2,3)
分析: 上面的代码是由javascript的之父 Brendan Eich写的一段代码,其实上面的一段代码的效果也可以通过Array.prototype.push.call(arguments, 4)
是一样的结果,关键在这种思维方法。
push方法接受两个参数,num函数的argument和4,Array.prototype.push.unCurrying();
表示unCurrying方法复制了数组的push方法;而unCurrerying方法的返回值就是Function对象中的unCurreying方法并且实现了arguments继承数组的push方法,并且把4中对应argumentsd的值,通过apply的显性绑定,给添加到第一个arguments的值中去。这个时候闭包中nums对应的arguments
的长度就是4,参数就是1234;
节流与防抖
1. setTimeout节流
var setThrottle = (function(){
var throFlag = true; //节流判断
return function(fn, time){
var that = this;
if(!throFlag){
return false;
}
throFlag = false;
setTimeout(()=>{
fn.apply(this, arguments)
throFlag = true;
}, time)
}
})();
2. date时间值节流
const setDateThro = (function(){
let old = Date.now();
return function(fn, time){
if(Date.now()- old >= time){
fn.apply(this, arguments);
old = Date.now();
}
}
})()
3. 混合节流,最后一次也需要执行该函数
const componentThro = (function(){
let old = Date.now();
let timeout;
return function(fn, time){
const args = arguments;
clearTimeout(timeout)
if(Date.now()- old > time){
fn.apply(this, args);
old = Date.now();
}else{ // 事件完结后再执行一次
timeout = setTimeout(()=>{
fn.apply(this, args);
}, time)
}
}
})()
div1.onclick = function(){
componentThro(()=>{
console.log('节流')
}, 1000)
}
4. 防抖,第一次执行
var deben = (function(){
let timeout = undefined;
let preFlag = true;
return function(fn, time){
const args = arguments;
if(preFlag){ // 第一次执行一次;
fn.apply(this, args)
}
preFlag = false
clearTimeout(timeout)
timeout = setTimeout(()=>{
preFlag = true;
}, time)
}
})()
div1.onclick = function(){
deben(()=>{
console.log('防抖')
}, 1000)
}
5. 防抖,最后一次点击执行
var deben = (function(){
let timeout = undefined;
return function(fn, time){
const args = arguments;
preFlag = false
clearTimeout(timeout)
timeout = setTimeout(()=>{
fn.apply(this, args)
}, time)
}
})()
div1.onclick = function(){
deben(()=>{
console.log('防抖')
}, 1000)
}
6. 防抖,第一次和最后一次都需要执行
var deben = (function(){
let timeout = undefined;
let preFlag = true;
return function(fn, time){
const args = arguments;
if(preFlag){ // 第一次执行一次;
fn.apply(this, args)
}
preFlag = false
clearTimeout(timeout)
timeout = setTimeout(()=>{
fn.apply(this, args)
preFlag = true;
}, time)
}
})()
div1.onclick = function(){
deben(()=>{
console.log('防抖')
}, 1000)
}
参考文档
- 节流
- javascript 设计模式与开发实践
- 你不知道的javascript