闭包高阶函数如何运用

349 阅读4分钟

闭包

  • 闭包的作用:保存数据

变量作用域

  • 函数作用域,函数内的变量只有在函数内才能访问到
var func = function(){ 
     var a = 1; 
     alert ( a ); // 输出: 1 
};
func(); 
alert ( a ); // 输出:Uncaught ReferenceError: a is not defined
  • 变量的作用域,是由函数内向往依次查找,直到查找到顶层的全局作用域

  • 变量 a 会沿着作用域链依次查找,先在函数 func2 查找,然后函数 func1 作用域,最后找到全局变量 a = 1

变量的保存周期

  • 函数内的变量,函数执行完毕后就销毁
  • 全局变量会一直存在,除非主动销毁
var func = function(){ 
     var a = 1; // 退出函数后局部变量 a 将被销毁
     alert ( a ); 
}; 
func();
  • 全局函数f 的函数作用域保存变量a, 一直未销毁过
var func = () =>{
    var a = 1
    return () => {
        a++
        alert(a)
    }
}
var f = func()
f(); //2 
f(); //3

闭包保存数据.png

  • 在不使用let时,闭包的经典使用场景
for(var i = 0; i< nodes.length ; i++){
   ( function(){
       nodes[i].onclick = function(){
           console.log(i)
       }
   })(i)
}
  • 根据经典的闭包场景,可以写出判断数据类型的变式
var Type = {}
for(var i = 0,type = [ 'String', 'Array', 'Number']; i<type.length; i++){
    (function(type){
        Type['is' + type] = (obj) => {
            return Object.prototype.toString.call(obj) === '[object ' + type + ']'
        }
    })(type[i])
}
Type.isArray([]) //true

闭包的更多作用

  • 函数内部一直保存对全局变量的引用实现缓存机制
  • 注意:arguments 是伪数组无法使用数组的api,所以需要借用 Array.prototype.join方法,call,bind,apply可以实现借用
const cache = {}
var multi = function (){
    let a = 1
    // const args = arugments.join(',') 伪数组没有join方法
    const args =  Array.prototype.join.call(arguments,',')
    if(cache[args]){
	return cache[args]
    }    
    for(let i = 0; i < arguments.length; i++){
        a = arguments[i] * a
    }
    cache[args] = a
    return a
}

  • 如果变量一直引用全局变量会导致污染,需要使用自调用函数,再返回一个函数(再封装一层)
var multi = (function(){
    var cache = {}
    return () => {
        var a = 1
        var args = Array.prototype.join.call(arguments,',')
        if(cache[args]) return
        for(var i = 0; i < arguments.length; i++){
            a = arguments[i] * a
        }
        return cache[args] = a
    }
})()
  • 大函数拆分:大函数中有很多个单独的功能块能独立处理,如mutli 可以分为 1 求乘积 2 取缓存 这2个部分
var multi = (function(){
    var cache = {}
    var calculate = () => {
        var a = 1
        for( var i = 0 ; i < arguments.length; i++){
            a = a * arguments
        }
        return a
    }
    // 此处为什么一定要返回一个函数? 因为需要函数作用域一直保持外层变量cache的引用
    // 此处不能使用箭头函数,箭头函数没有arguments对象
    return function(){
        var args = Array.prototype.join.call(arguments,'')
        if(args in cache){
            return cache[args]
        }
        // 不能写成,cache[args] = calculate()
        return cache[args] = calculate.appply(null,arguments)
    }
})()
  • 延续局部变量的寿命
var report = function(src){
    var img = new Image()
    img.src = src
}
  • 当局部变量img被销毁,而report还未来得及发HTTP请求,此时请求就会丢失如何解决这个问题?
var report = (function(){
    var imgs = []
    return function(src){
        var img = new Image()
        imgs.push(img)
        img.src = src
    }
})()
  • 以上通过闭包保存函数内部的数据,来延长变量的生命周期

闭包和面向对象设计

  • 通过闭包保存数据的,面向对象的写法为
var extend = function(){
    var value = 0
    return {
        call:function(){
            value++
            console.log(value)
        }
    }
}
var extend = extend()
extend.call()
extend.call()
  • 上面为通过函数的形式实现,将extend改为对象实现
var extend = {
    value:0,
    call:function(){
        this.value++
        console.log(this.value)
    }
}
extend.call()
  • 也可以通过构造函数的方式实现
var  Extent = function(){
    this.value = 0
}
 Extent.prototype.call = function(){
     this.value++
     console.log(this.value)
 }
var extend = new Extend()
extend.call()

闭包与内存管理

  • 闭包和内存泄露的关系:使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露

  • 解决内存泄漏问题:们只需要把循环引用中的变量设为 null即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。

高阶函数

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

函数作为参数

  • 常见的函数最为参数,就是回调函数
var getUserInfo = function(userId,callback){
    $.ajax('http://xxx.com/getUserInof?' + userId,function(data){
        if(typeof callback === 'function'){
            callback(data)
        }
    })
}
getUserInfo(13157,function(data){
    alert(data.userName)
})
  • 函数作为参数,常见的还有事件委托,不适合在函数内部执行一些动作,需要委托给外部方法去执行
var appendDiv = function(){
	for(var i = 0; i < 100; i++){
		var div = document.createElement('div')
        div.innerHTML = i
        document.body.appendChild(div)
        div.style.display = 'none'
    }
}
  • 并不是所有的元素都是隐藏操作,需要将div委托给另外一个函数
var appendDiv = function(callback){
	for(var i = 0; i < 100; i++){
		var div = document.createElement('div')
        div.innerHTML = i
        document.body.appendChild(div)
        if(typeof callback === 'function'){
            callback(div)
        }
    }
}
appendDiv(function(node){
    node.style.display = 'none'
})

函数作为返回值

  • 判断数据类型
function isType(typing) {
  return function (val) {
    return Object.prototype.toString.call(val) == `[object ${typing}]`;
  };
}
let util = {};

["String", "Number", "Boolean"].forEach((mehtod) => {
  util["is" + mehtod] = isType(mehtod);
});
console.log(util.isString("hello"));
console.log(util.isNumber(222));

高阶函数实现APO

  • 动态织入,到另外一个函数汇总,具体的实现技术有很多,Function.prototype 就是其中一个
Function.prototype.before = function(beforeFn){
    var _self = this
    return function a(){
        beforeFn.apply(this,arguments)
        return _self.apply(this,arguments)
    }
}
Function.prototype.after = function(afterFn){
    var _self = this
    return function b(){
        var ret = _self.apply(this,arguments)
        afterFn.apply(this,arguments)
        return rete
    }
}
var func = function(){
    console.log(2)
}
const result = func.before(function(){
    console.log(1)
}).after(() => {
    console.log(3)
})
result() // 1 2 3 
  • 如果工作上希望在某个别人的方法上加上某些方法,最好的办法就是在原来的基础上再包一层
  • 如他人的工具 otherTool, 使用 Object.craete(otherTool)产生自己的myTool,然后再加方法
let otherTool = {
    add:function(a,b){
        console.log('调用别人的方法')
        return a + b
    }
}

let myTool = Object.create(otherTool)

myTool.add = function(a,b){
    console.log('调用自己的方法')
    return a + b * 2
}
console.log("使用add方法",myTool.add(1,2))

高阶函数的应用场景

柯里化

  • 什么是柯里化:柯里化又称分部分求值,即柯里化完后的函数,不会直接求值,而是继续返回一个函数
function add(){
    return function(a){
        return function(b){
        	return function(c){
        		return function(d){
        			return a + b + c + d
    			}
    		}
    	}
    }
}
// 第一次调用返回一个函数,第二次调用开始保存参数,最后一次传入参数才执行相加
add()(1)(2)(3)(4)
  • 如何将一个函数柯里化
//柯里化 
12个参数,第一个参数是一个函数,第二个参数是每次传入的参数    
2 返回值为一个函数 
3 如果传入的参数个数与函数的形参的个数一致才执行该函数,否则仍然返回一个函数
function curring(fn,arr = []){
    return function(...args){
        let params = [...arr,...args]
        if(fn.length === params.length){
            fn(...params)
    	} else {
            return curring(fn,params)
        }
    }
}

反柯里化

  • 反柯里化不是柯里化的反向操作,不是将一个分多步调用的函数一次性调用,而是借用其他对象的方法并调用
  • 如果让对象可以调用push方法?
Function.prototype.uncurring = function(){
    var self = this
    return function(){
        // 取出形参的第一个,即为反柯里化的函数
        var curFn = Array.prototype.shift.call(arguments)
        // 执行当前函数,并调用当前函数,并将上下文指向当前执行的函数,并传递参数
        return self.apply(curFn,arguments)
    }
}
var push  = Array.prototype.push.uncurring()
var obj2 = {
    a:1,
    b:3
}
push(obj2,2) // {0: 2, a: 1, b: 3, length: 1}

函数节流防抖

image.png

  • 节流:记录每次函数执行的时间戳,类似于到点就发车的班车,每次发车的时间间隔是固定的
    function throttle(fn, wait, options) {
        let args, context, start = 0;
        // 必须在函数的外部申明形成闭包,这样才能保存数据,不然每次都是重新赋值
        let throttled = function (e) {
            context = this
            args = arguments;
            let now = Date.now();
            if ((now - start) < wait) {
                // 当前时间小于事件间隔则无需触发
            } else {
                start = now;
                fn.apply(context, args);
                timeout = null;
            }
        }
        return throttled
    }
  • 防抖:每次用户点击都会开启一个定时器,目标函数只会执行一次
function debounce(fn,wait,options){
    let timeout,args,context;
    return function() {
        const context = this
        const args = arguments
        //每次用户点击都会清空定时器
        timeout = setTimeout(() => {
            fn.apply(context,args)
        },wait)
    }
}
  1. 防抖函数是在触发事件的单位时间后执行一次函数,在单位时间内多次触发不执行函数,重新计时

分时函数

  • 有大的同步任务,需要分成多个小任务
function computeBigData(data,cb){
    let result = []
    function next() {
        const chunk = data.splice(0,5)       
        if(data.length > 0 ){
            setTimeout(() =>{
                 // 假设此处为大量的同步计算
                result = result.concat(chunk.map => chunk *2)
                // 如果数据未处理完毕,则继续调用next函数
                next()                
            },0)
            
        } else {
            cb(result)
        }
    }
    next()
}