JS函数扁平化、柯里化

一. 扁平化

数组的扁平化,就是将一个嵌套多层的数组array(嵌套可以是任何层数)转换为只有一层的数组。
例如:

var arr = [1, 2, 3, [4, 3, [2, 7], 2], 5, [5, 9, 10], 7]; 
// 去扁平化后 
arr = [1, 2, 3, 4, 3, 2, 7, 2, 5, 5, 9, 10, 7];

数组去扁平化实现:

1. 循环递归方式实现
// for循环,如果子元素还是数组,则递归调用该方法
function flatten(arr) {
    var rets = [];
    for(var i = 0; i < arr.length; i ++) {
        if (Array.isArray(arr[i])) {
            rets = rets.concat(flatten(arr[i]));
        } else {
            rets.push(arr[i]);
        }
    }
    return rets;
}
//使用forEach
function flatten(arr) {
    var rets = [];
    arr && arr.forEach(item => {
        if (Array.isArray(item)) {
            rets = rets.concat(flatten(item));
        } else {
            rets.push(item);
        }
    });
    return rets;
}
2. 使用reduce简化代码
function flatten(arr) {
    return arr.reduce(function(pre, item){
        return pre.concat(Array.isArray(item) ? flatten(item) : item);
    }, [])
}
3. 如果数组元素都为数字,则可以使用toString方法
function flatten(arr) {
    var newArr = arr.toString().split(',');
    return newArr.map(function(item){
        return +item;        // 将字符串转为数字
    });
}
4. 扁平化数组 Array.prototype.flat()方法

在这里插入图片描述

二. 柯里化

2.1 什么是柯里化:

维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
但是问题来了费这么大劲封装一层,到底有什么用处呢?没有好处想让我们程序员多干事情是不可能滴,这辈子都不可能。

2.2 来列一列柯里化有哪些优缺点呢?

优点:
1.当存在大量相同参数时,参数复用。
2.提前确认,避免每次都重复判断。
3.延迟计算/运行。
缺点:
使用柯里化让程序具有了更多的自由度,但柯里化用到了arguments对象、递归、闭包等,频繁使用会给性能带来影响。所以视情况使用。

1. 参数复用

比如我有如下需求,调用一个函数,将输出国家、职业、姓名:

function who(country, occupation, name) {
   console.log(`我来自${country},职业是${occupation},我叫${name}`);
}
who("中国", "前端开发", "张三");
who("中国", "前端开发", "李四");
who("中国", "前端开发", "王五");

在这里插入图片描述
有一个问题,就是他们都是来自中国,职业都是前端开发,只有名字不一样。但是每次调用函数都要声明,如果有很多个人都这样写岂不是很麻烦啰嗦,所以我们可以柯里化后简便写法:

  function curryWho(country) {
    return function (occupation) {
      return function (name) {
        console.log(`我来自${country},职业是${occupation},我叫${name}`);
      };
    };
  }
  var myName = curryWho("中国")("前端开发");
  myName("张三");
  myName("李四");
  myName("王五");

一样的结果,但是简便了许多,参数能复用,不用每次都传递:
在这里插入图片描述

2. 提前确认

比如在js中我们使用 addEventListener 来给元素绑定事件,然而在低版本的 ie 浏览器中只能使用 attachEvent ,这样我们在不确定何种浏览器运行情况下可以提前写一个函数在js一开始就执行,判断是否可以使用 addEventListener ,否则使用 attachEvent 。这样一来只用在开始的时候判断一次,不用后面每次都用到addEvenListener时都判断一次是否可用,浪费性能。

// 自创建一个whichEvent函数
 const whichEvent = (function () {
  // 如果存在 addEventListener
     if (window.addEventListener) {
       // 返回一个函数 element为元素,type为事件类型,listener为执行函数,useCapture为是否为捕获流
       return function (element, type, handler, useCapture) {
         element.addEventListener(
           type,
           function (e) {
           // 执行传入的执行函数,同时改变this指向
             handler.call(element, e);
           },
           useCapture
         );
       };
       //  如果浏览器版本低 ,使用 attachEvent
     } else if (window.attachEvent) {
      // 它只有三个参数,element为元素,type为事件类型,handler为执行函数
       return function (element, type, handler) {
         element.attachEvent("on" + type, function (e) {
           handler.call(element, e);
         });
       };
     }
   })();

使用下whichEvent看看,如点击按钮有输出:

<button>test</button>
---------。。。。。略
 var btn = document.querySelector("button");
 whichEvent(btn, "click", function () {
      console.log("北极光之夜。666666");
 });

在这里插入图片描述

3. 延迟运行

如果我没有传入三个参数是不会执行的:

  var r = curry(who);
  r("a");
  r("b")("c");
  // 只有它会执行
  r("d")("e")("f");

在这里插入图片描述
再例如:

Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
 
    return function() {
        return _this.apply(context, args)
    }
}

像我们js中经常使用的bind,实现的机制就是Currying。


说了这几点好处之后,发现还有个问题,难道每次使用Currying都要对底层函数去做修改,有没有什么通用的封装方法?
// 初步封装
var currying = function(fn) {
    // args 获取第一个方法内的全部参数
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        // 将后面方法里的全部参数和args进行合并
        var newArgs = args.concat(Array.prototype.slice.call(arguments))
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs)
    }
}

这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)©这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

// 支持多参数传递
function progressCurrying(fn, args) {

    var _this = this
    var len = fn.length;
    var args = args || [];

    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。

2.2 柯里化后的性能

curry的一些性能问题你只要知道下面四点就差不多了:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,所以curry是可以直接放心的使用。

最后再扩展一道经典面试题
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9

参考链接:js数组去重与去扁平化
参考链接:详解JS函数柯里化
参考链接:人类高质量JS函数柯里化

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值