闭包引发的高阶编程技巧

javaScript

# 惰性函数

  • 过去绑定事件的方法
    • DOM0事件绑定:xxx.onclick=function(){}
    • DOM2事件绑定:
      1. xxx.addEventListenter('click',function(){}) 高版本浏览器
      2. xxx.attachEvent('onclick',function(){}) 低版本浏览器IE678
    function abserverEvent(element,type,func){
        if(element.addEventListener){
            element.addEventListener(type,func)
        }else if(element.attachEvent){
            element.attachEvent('on'+type,func)
        }else{
            element['on'+type] = func
        }
    }
    
    // abserverEvent(xxx,'click',function(){})
    // abserverEvent(xxx,'click',function(){})
    // abserverEvent(xxx,'click',function(){})
    // ......
    
  • 重构赋值
    这种写法虽然可以实现我们的需求和效果,但是在相同页面中每一次执行函数,进来后都要重复的兼容判断(但是理论上第一次执行,我们就知道兼容性了,后期再执行没必要每一次都判断兼容,也就是把兼容处理只处理一次=>'懒')
function abserverEvent(element,type,func){
    // 第一次执行observerEvent,根据兼容判断,重构了这个函数,重构后的小函数是不需要做兼容处理的
    if(element.addEventListener){
        abserverEvent = function(element,type,func){
            element.addEventListener(type.func)
        }
    }else if(element.addEventListener){
        abserverEvent() = function(element,type,func){
            element.attachEvent('on'+type,func)
        }
    }else{
        abserverEvent() = function(element,type,func){
            element['on'+type] = func
        }
    }
    // 第一次也要执行重构的方法,实现事件绑定
    abserverEvent(element,type,func)
}

// abserverEvent(xxx,'click',function(){})
// abserverEvent(xxx,'click',function(){})
// abserverEvent(xxx,'click',function(){})
// ......

# 单例设计模式

JS中一个最基础,但是也是业务逻辑开发中常用到的设计思想:'单例设计模式'(框架开发模式下无需用单利处理)最早的模块开发思想(AMD/CMD/CommonJS/ES6Module)

// A开发天气板块
// weatherModule除了是全局变量、模块名称、对象名,更专业的叫法是'命名空间',这样单例设计模式就是把描述相同事务(相同板块)中的属性和方法归拢到相同的命名空间下,实现分组管理(既可以避免全局变量污染、也可以实现模块之间的相互调用)
let weatherModule = (function(){
    let index = 0
    function querData(){}
    function getElement(){}
    // 想让闭包之外的东西调用方法,可以基于window.xxx把其暴露到全局上(如果像全局暴露东西过多,也会存在冲突问题)
    // window.getElement = getElement
    // 建议使用return的方式
    return {
        // getElement: getElement
        getElement
    }
})()

// B开发资讯板块
let informationModule = (function (){
    let index = 0
    function querData(){}
    function bindHTML(){}
    function handleEvent(){}

    // 调用A的方法
    // window.getElement()
    // weartherModule.getElement() 

    return {
        // init:function(){}
        init(){
            // 在单例设计模式的基础上,增加一个命令模式。init作为当前模块业务的入口,以后只需要执行informationModule.init(),我们在init中根据也无需求,把编写的方法按照循序依次调用执行即可
            queryData()
            bindHTML()
            handleEvent()
        }
    }
})()

# 柯理化函数

预先存储或者叫预先处理的概念,把参数预先存起来,小函数会用到这些参数

function fn(x,y){
    // 第一次执行函数,形成一个临时不被释放的上下文(闭包),我们保存下来传递的参数信息,当后期执行小函数的时候可以基于作用域链机制,找到闭包中存储的信息,并且拿来使用,所以形成的闭包类似于预先把一些信息进行存储
    return function(z){
        //最后小函数执行的时候,需要把之前传递的值和最新传递的值进行累加
        return x + y + z
    }
}

let res = fn(1,2)(3)
console.log(res) //=>6 1+2+3

// function.prototype.bind 预先处理this的,利用的就是柯理化思想
// vue中的很多核心源码
// React中的redux源码大量用到柯理化思想 如createStors.js、combineReducers.js、bindActionCreator.js...
// react-redux connect.js

# compose函数

在函数式编程当中有一个很重要的概念是函数结合,实际上就是把处理数据的函数像管道一样链接起来,然后让数据穿过管道得到最终的结果

const add1 = (x) => x + 1
const muld = (x) => x * 3
const div2 = (x) => x / 2
div2(mul3(add1(add1(0)))) //=>3

而这样的写法可读性明显太差了,我们可以构建一个compose函数,他接受任意多个函数作为参数(而这些函数都只接受一个参数),然后compose返回的也是一个函数,达到以下的效果

const operate = compose(div2,mul3,add1,add1)
operate(0) //=> 相当于div2(mul3(add1(add1(0))))
operate(2) //=> 相当于div2(mul3(add1(add1(2))))

简而言之:compose可以把类似于f(g(h(x)))这种写法简化成compose(f,g,h)(x),请你完成函数的编写

const add1 = (x) => x + 1
const muld = (x) => x * 3
const div2 = (x) => x / 2

function compose(...funcs){
    // funcs接收的就是所有传递进来的函数[div2,mul3,add1,add1]
    return function anonymous(val){
        // val 是第一个函数的实参 0
        if(funcs.length === 0) return val
        if(funcs.length === 1) return funcs[0](val)
        // severse将数组倒过来[add1,add1,div2,mul3],或者用reduceRight替换
        return funcs.severse().reduce((N,item) => {
            return typeof N === "function"? item(N(val)):item(N)
        })
        // 简单写法,reduce N的初始值传val 这样每次进来item执行,将val传进来就好了
        // return funcs.reverse().reduce((N,item) => item(N), val)
    }
}
let result = compose(div2,mul3,add1,add1)(0)
console.log(result) //=>3

redux源码中的compose是这么做的

function compose(...funcs){
    if(funcs.length === 0){
        return arg => arg
    }
    if(funcs.length === 1){
        return funcs[0]
    }
    
    return funcs.reduce((a,b)=>{...args}=> a(b(...args)))
}

# 总结

谈谈你对闭包的理解,以及在项目中的应用!

  1. 阐述闭包是什么?(引申:堆栈、EC、AO、VO、SCOPE...)
  2. 闭包的作用及在真实项目中的应用场景,以及所带来的问题!
  3. 由闭包引发的高阶编程技巧
  4. 突出自己在分析研究框架的源码(逼格更高的是自己写类库,插件的时候)是怎么应用这些东西的!
  5. 自己的见解(慎重)

回答建议:

  • 浏览器加载页面会把代码放到栈内存(ECStack)中执行,函数进栈执行会产生一个私有上下文(EC),此上下文能保护里面的私有变量(AO)不受外界干扰,并且如果当前上下文中的内容,被上下文以外的内容所占用,当前上下文是不会出栈释放的,这样可以保存里面的变量和变量值,所以我认为闭包是一种保存和保护内部私有变量的机制...
  • 在真实项目中,其实我应用闭包的场景还是很多的,例如:
    1. 我会基于闭包把自己编写的模块包起来,这样自己编写的代码都是私有的,防止和全局变量或者别人的代码冲突,这一点利用的是闭包的保护机制
    2. 在之前没有用let之前,我们循环处理事件绑定,在事件触发需要用到索引值的时候,我们基于闭包,把每一轮循环的索引值保存起来,这样来实现我们的需求,只不过现在都是基于let来完成,因为let会产生块级作用域来保存需要的内容(机制和闭包类似)
    3. 但是不建议过多使用闭包,因为形成不会释放的上下文,是占用栈内存空间的,过多使用会导致页面渲染变慢,所以要合理应用闭包
  • 除了这些传统的业务开发中会应用闭包,我之前在研究别人源码和自己写一些插件的时候,往往会利用一些JS高阶编程技巧来实现代码的管理和功能的开发,他们的底层机制其实就是闭包,例如:
    1. 惰性函数
    2. 柯理化函数
    3. compose函数 ......
Last Updated: 2021-04-26 14:38:08