前端

事物总是在发展变化的,不可能总是停留在旧有的状态

引言

某位伟人曾教育我们,“世界是普遍存在联系且永恒发展的。”原意大概是指万事万物之间是互相依赖,互相影响,互相作用,互相制约,且有生有灭,旧事物灭亡的同时也意味着新事物的诞生。而现在我们将“编程语言”代入这段描述中的“万事万物”中自然是成立的,众所周知从ES6以来javascript已经发生了翻天覆地的变化,甚至到了有时一些萌新会将ES6当一门新的语言来看待的地步,实际上这也对也不对。对在于这变化相对以往是自上而下全面而新颖的,不对则在于ES6只是规范,而今已经到ES10了,虽然从6~10的变化远没有之前大,但它确实是在发展的。这变化最显著的特点是原生提供了很多特性或者语法糖的支持,将很多以往工具库提供的方法原生提供支持,带给编码人员无限遐想,同时催生了新一批的工具类库。比方说随着原生功能的变化及兼容性的提升去年开始一些大项目开源库开始去jQuery化,这款曾经在前端领域接近垄断的库虽然不会立刻退出历史舞台,但也逐渐走向属于他的角落。而随着滚滚向前的车轮变化的还有编程思想,通常来说编写jQuery是命令式的随着它在前端的流行大多前端代码都是面条式的,最多结合面向对象进行简单的封装,内部还是大量的面条代码,而现在伊人已逝其它的思想也慢慢在前端开发中普及开来。
在此,我将总结一些最近所见所学之感,主要包括:

  1. 花式操作数组
  2. 花式判断

关于新时代下for循环的替代方案

为什么要替换for循环?

当我第一次看到消灭for循环类的文章的时候,感觉很高端,但似乎不是很有必要。当我第二次看到消灭for循环类的文章的时候,感觉有点道理,并开始尝试。当我第三次看到消灭for循环类的文章时,我决定自己也输出一篇以加深理解。在这里我不是想说三人成虎,而是想说接受一个观点需要一个过程,而过程的开始越早越好。回到正题,js中所有的循环类操作几乎都可以用for循环来实现,换句话说学会用for循环后几乎所有需要循环的操作你都能做,那么为啥会需要替代方案呢?简单来说for循环分类上属于命令式操作且更偏向底层,在简单好上手的同时也具备了几乎无法复用,和不好理解的特点。想复用一个需要稍微改一点的for循环最快的办法是复制过来,而想理解一个三层for循环在做什么时最好的办法是打断点一个一个跟下去。

循环

基础:for循环
替换操作:forEach,map
衍生操作:every,some,filter,reduce等
举例:

    let arr = [1,2,3,4,5]
    //一个平淡无奇的for循环
    for(let i=0,l=arr.length;i<l;i++){
        //Do something
    }
    //平淡无奇只是一个for循环而已
    //forEach
    arr.forEach((item,index)=>{
        //Do something
    })
    //需要注意forEach可以更改原数组,但不会直接操作原数组,且没有返回值,通常做法是新建个空数组来操作。
    //Tip: IE8不支持forEach jQuery有一个$.forEach,但是它的返回值的位置与原生正好相反index是第一个参数,item是第二个。
    
    //map
    let newArr = arr.map((item,index)=>{
        //Do something
        return item
    })
    //看起来和forEach差不多,但map有个返回值 在这里对item做的操作会在newArr中表现出来
    //Tip:只考虑遍历map在数据量不大的时候遍历效率比forEach高,但当数据量比较大(比如大于7W左右)时和forEach差不多,而当大于10W时性能明显弱于forEach,这也许和它的实现有关,随着V8更新也许会变
    //TIP2:有两种情况我们不能用forEach和map来替换for循环,1、需要中断的时候. 2、异步操作需要await的时候 这个时候我们可以用for...of...
    //for...of... 循环
    for(let item of arr){
        //Do something
    }
    //怎么说还是比原始的for循环要优雅一些
    //实际上只论循环操作就可以替换掉大多数的for循环了,但是实际业务不会这么简单,我们往下看

判断是否在数组中

基础:for循环然后判断相等后break
替换操作:indexOf,lastIndexOf,findIndex
假如只是想判断是否存在:some,includes,find
举例:

    //假如我们想判断6是否在一个数组中
    let arr = [1,2,3,4,5]
    //for循环
    let isExist = false; 
    for(let i=0,l=arr.length;i<l;i++){
        if(arr[i]===6){
            isExist = true;
            break;
        }
    }
    //可以发现如果想判断别的这堆代码几乎得原封不动的复制出去,也许你会想说可以封装成一个函数,那么我们为什么不用原生的呢。
    //indexOf 正序查找
    arr.indexOf(6) //不存在返回 -1
    //lastindexOf 倒叙查找 略过
    //findIndex 自定义判断条件
    arr.findIndex(item=>item===6) //不存在返回-1

    //如果只是判断是否存在推荐使用includs
    arr.includes(6) //不存在返回 flase

    //如果需要判断的是元素中的属性比如
    let arr = [{name:'tom',age:22},{name:'tim',age:23}];
    //当然你可以使用findIndex来做,但如果要用推荐some
    //some 与findIndex类似 不过返回布尔值
    arr.some(item=>item.name==='tom') //存在 返回true

    //查找得到具体的元素,当然你可以先查找到下标再取值,但这里推荐find
    arr.find(item=>item.name==='tom')//返回 {name:tom,age:22}
    //无论如何,使用这些api总比摆一个for循环在那里好看好懂的多。

循环的衍生操作

这里我想单独把reduce拎出来,实际在业务中,这个api我用的很少,因为我对齐理解的不是特别透彻,所以想总结一下。
定义:reduce()方法对数组中的每个元素执行一个由你通过reduce提供的函数,最后返回一个汇总值。
参数:arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

  1. callback 你提供的那个回调函数

    • accumulator 累计值
    • currentValue 当前的值
    • index 当前索引
    • array 源数组
  2. initialValue 第一个输入值(即calllback得第一个参数)
    以上内容查自mdn,咋一看好像就是一个平淡无奇得循环,操作..最多就是实现累加更容易了..
//我们可以轻松的写出这么一个应用
let arr = [1,2,3];
let sum = arr.reduce((sum,value)=>{
    return sum+value
},0) //6

实际上在需要对所有数据做操作的时候,直接写个循环好像区别不大..那么我们为什么要用reduce呢..?
个人觉得也许是在遍历数组的最后的目的是生成一个单独的值用这个语义化程度更好...而且,这个东西可以写的很简洁简洁到,有些难懂的地步..比如

//一个管道函数
const pipe = (...functions) => input => functions.reduce(
    (acc, fn) => fn(acc),
    input
);
//实际上等效于
const pipe = (...functions) => {
    return (input) => {
        return functions.reduce(
            (acc, fn) => {
                return fn(acc)
            },
            input
        );
    }
}
//目的是把一组函数拼装起来执行
//简单的用法示例
let add3 = num => num+3;
let add5 = num => num+5;
let double = num => num*2;

let operA = pipe(add3,double) //先加3,再乘2
let operB = pipe(add3,double,add5) //先加3,再乘2,再加5

operA(1) //8
operB(1) //13
//主要目的是把函数组合起来,虽然实际上这种运算没必要写的这么花哨,这里只是举个列子
//通过同样的思想,也可以用来组织promise
const runPromiseInSequence = (arr, input)=>{
  return arr.reduce(
    (promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(input)
  );
}

let p1 = num => new Promise((resolve, reject)=>{
    resolve(num + 3);
})

let p2 = num=> new Promise((resolve, reject)=>{
    resolve(num * 2);
})

let p3 = num=> new Promise((resolve, reject)=>{
    resolve(num +5);
})


const promiseArr = [p1, p2, p3];
runPromiseInSequence(promiseArr, 1)
  .then(console.log);   // 13

感觉上,reduce的应用场景已经和循环本身没有太大关系了,但它依旧是循环思想的体现,某种意义上来说很有趣。

老生常谈的降低判断复杂度

实际应用中,我们经常能看到意大利面条一般的大量判断,及判断嵌套....也许会有人美其名曰航天飞机风格的判断代码..emmm,也不算错,然而作为并不打算自诩航天飞机开发者的凡人,我觉得在应用中尝试降低if else的复杂度是很有帮助的一件事。下面切入正题,首先我会略过switch case,个人认为治标不治本,然后我会略过三目运算符,短路,函数提前返回之类的小技巧,它们很常见也很有用,所以在这里提一嘴就足够了。

从判断的拆分说起

来看如下代码

    function judge(a,b,c) {
        if (f(a,b,c)) {
            if(g(a,b,c)){

            }
            ...
        }
        
        if (h(a,b,c)) {

        }
        .....
        else if (j(a,b,c)) {
            if(k(a,b,c)){
                
            }
            ...
        }
        else {

        }
    }

这种代码很多时候可能会让人觉得头晕目眩,却是基本不可避免的,尤其是某个地方最开始可能只是简单的一个if else 然而随着时间推进功能越扩越多,越扩越复杂..而且通常前端函数很少有写单元测试的,总有一天,针对这个函数校调和测试的时间会比开发还要长..
实际上,针对if if 的连用..完全可以将要执行的部分拆分成小函数,只返回必要的值,尤其是对判断条件做修改的时候,合适的判断条件及拆分能有效降低调试的难度,而各if中内部的判断写再各自的小函数里相对来说结果会比较可控。
而对于大量的if else 连用的情况我们可以将它们理成集中情况来把判断交到一个专门做判断的函数中,来保证结果的可控性,比如。

    function someThing(a,b,c){
        const map = {
            //具体的操作
            x:()=>{},
            y:()=>{},
            z:()=>{}
        }
        let actionKey = findAction(a,b,c) //做一堆 判断 返回 x y z
        return map[actionKey]()
    }

职责链

上面的操作,主要目的是使结果相对可读,可控,来使这些判断在开发和扩展时不会占用太多精力,如果需要你甚至还能把判断逻辑拿来复用。类似的思路还有一种做法就是职责链。
顾名思义指责链就是把一堆职责(处理函数)用链表串联起来,简单来说就是传入一组数据,再一组函数中每个函数都进行一次判断,能处理就处理掉,不能处理就像后传,有必要也可以处理掉再往后传,拿来处理判断是非常合适的。

//一个列子,根据权重来生成随机数,用来抽奖
//简单的来说,为了用随机数支持权重最简单的做法就是划分区间也就是if else 
//类似这样
function getReulst() {
    let num = Math.round(Math.floor() * 10) + 1;//1~10的随机数
    if (num <= 6) {
        return '三等奖'
    } else if (num > 6 && num <= 9) {
        return '二等奖'
    } else {
        return '一等奖'
    }
}
//上面是一个简单的权重抽奖,可以看见就是一组简单的if else 
//假如这里想做一个更灵活的,比如可以配置的,我们就可以借助职责链
let model = [
    {
        radio: 6,
        text: '三等奖'
    }, {
        radio: 3,
        text: '二等奖'
    }, {
        radio: 1,
        text: '一等奖'
    }]
function makeDrak(model){
    let sum =0;
    //生成职责链条
    let stack = model.map((item)=>{
        let step = sum + item.radio;
        return (num)=>{
             if(num<=step){
                 return item.text
             }else{
                 return false
             }   
        }
    });
    return function getReulst(){
        let num = Math.floor(Math.random() * 10) + 1;
        //调用
        for(let func of stack){
            let res = func(num);
            if(typeof res ==='string'){
                return res;
            }
        } 
    }
}

由此可以看出,职责链完全可以用于生成判断函数,扩展的时候只需要扩展model的结构就好,十分易用。
这虽然看起来不是一个通用的写法,但是思想已经包含在里面了,类似的你完全可以自定义一种model,每一个元素有一个单独的判断函数和处理函数,也可以稍加修改就能做到不断得向下传递(比如借助reduce)。

总结

本篇笔记开始的很早,但是中间因为各种原因一直没有写完,实际上续写的时候开篇时的思路已经忘掉了,但是实际续写时却感觉十分良好,也许是因为这段时间将这其中的东西应用过,收获颇丰的原因吧。总的来说,阅读的再多也只能说获得了相关的知识,如果不配合使用是不会有实际的产出的,某种意义上来说也是“无用”的,只有不断的在实践中检验,最终才能将这些知识化为自己技能的一部分,更好的服务于生产生活。

评论

This is just a placeholder img.