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
  • 2
  • 3

数组去扁平化实现:

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;
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
//使用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;
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2. 使用reduce简化代码

function flatten(arr) {
    return arr.reduce(function(pre, item){
        return pre.concat(Array.isArray(item) ? flatten(item) : item);
    }, [])
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5

3. 如果数组元素都为数字,则可以使用toString方法

function flatten(arr) {
    var newArr = arr.toString().split(',');
    return newArr.map(function(item){
        return +item;        // 将字符串转为数字
    });
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

二. 柯里化

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

2.2 来列一列Currying有哪些好处呢?

1. 参数复用
// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}

check(/\d+/g, ‘test’) //false
check(/[a-z]+/g, ‘test’) //true

// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber(‘test1’) // true
hasNumber(‘testtest’) // false
hasLetter(‘21212’) // false

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

2. 提前确认
var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

var on = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent(‘on’ + event, handler);
}
};
}
})();

//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
isSupport = isSupport || document.addEventListener;
if (isSupport) {
return element.addEventListener(event, handler, false);
} else {
return element.attachEvent(‘on’ + event, handler);
}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

3. 延迟运行
Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
<span class="token keyword">return</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">return</span> _this<span class="token punctuation">.</span><span class="token function">apply</span><span class="token punctuation">(</span>context<span class="token punctuation">,</span> args<span class="token punctuation">)</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

像我们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)
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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

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

// 支持多参数传递
function progressCurrying(fn, args) {
<span class="token keyword">var</span> _this <span class="token operator">=</span> <span class="token keyword">this</span>
<span class="token keyword">var</span> len <span class="token operator">=</span> fn<span class="token punctuation">.</span>length<span class="token punctuation">;</span>
<span class="token keyword">var</span> args <span class="token operator">=</span> args <span class="token operator">||</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>

<span class="token keyword">return</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">var</span> _args <span class="token operator">=</span> Array<span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>slice<span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>arguments<span class="token punctuation">)</span><span class="token punctuation">;</span>
    Array<span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>push<span class="token punctuation">.</span><span class="token function">apply</span><span class="token punctuation">(</span>args<span class="token punctuation">,</span> _args<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>_args<span class="token punctuation">.</span>length <span class="token operator">&lt;</span> len<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token keyword">return</span> progressCurrying<span class="token punctuation">.</span><span class="token function">call</span><span class="token punctuation">(</span>_this<span class="token punctuation">,</span> fn<span class="token punctuation">,</span> _args<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token comment">// 参数收集完毕,则执行fn</span>
    <span class="token keyword">return</span> fn<span class="token punctuation">.</span><span class="token function">apply</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> _args<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的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);

<span class="token comment">// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值</span>
<span class="token keyword">var</span> <span class="token function-variable function">_adder</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    _args<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token operator">...</span>arguments<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> _adder<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>

<span class="token comment">// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回</span>
_adder<span class="token punctuation">.</span><span class="token function-variable function">toString</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">return</span> _args<span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token keyword">function</span> <span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token keyword">return</span> a <span class="token operator">+</span> b<span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> _adder<span class="token punctuation">;</span>

}

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

参考链接:js数组去重与去扁平化 https://blog.csdn.net/xiaobing_hope/article/details/79458288 参考链接:详解JS函数柯里化 https://www.jianshu.com/p/2975c25e4d71
上一篇:Mybatis自动注册TypeHandler,自动扫描、自定义注解实现自定义TypeHandler


下一篇:java-如何在Wildfly中将外部属性文件加载到Spring Boot