纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

JS函数的柯里化 深入详解JS函数的柯里化

lsgxeva   2021-06-09 我要评论
想了解深入详解JS函数的柯里化的相关内容吗lsgxeva在本文为您仔细讲解JS函数的柯里化的相关知识和一些Code实例欢迎阅读和指正我们先划重点:JS,柯里化下面大家一起来学习吧

一、补充知识点之函数的隐式转换

来一个简单的思考题

function fn() {
    return 20;
}
console.log(fn + 10); // 输出结果是多少?

稍微修改一下再想想输出结果会是什么?

function fn() {
    return 20;
}
 
fn.toString = function() {
    return 10;
}
 
console.log(fn + 10);  // 输出结果是多少?

还可以继续修改一下

function fn() {
    return 20;
}
 
fn.toString = function() {
    return 10;
}
 
fn.valueOf = function() {
    return 5;
}
 
console.log(fn + 10); // 输出结果是多少?
// 输出结果分别为
function fn() {
    return 20;
}
10
 
20
 
15

当使用console.log或者进行运算时隐式转换就可能会发生从上面三个例子中我们可以得出一些关于函数隐式转换的结论

当我们没有重新定义toString与valueOf时函数的隐式转换会调用默认的toString方法它会将函数的定义内容作为字符串返回而当我们主动定义了toString/vauleOf方法时那么隐式转换的返回结果则由我们自己控制了其中valueOf的优先级会toString高一点

因此上面例子的结论就很容易理解了建议大家动手尝试一下

二、补充知识点之利用call/apply封数组的map方法

map(): 对数组中的每一项运行给定函数返回每次函数调用的结果组成的数组

通俗来说就是遍历数组的每一项元素并且在map的第一个参数(回调函数)中进行运算处理后返回计算结果返回一个由所有计算结果组成的新数组

// 回调函数中有三个参数
// 第一个参数表示newArr的每一项第二个参数表示该项在数组中的索引值
// 第三个表示数组本身
// 除此之外回调函数中的this当map不存在第二参数时this指向丢失当存在第二个参数时指向改参数所设定的对象
var newArr = [1, 2, 3, 4].map(function(item, i, arr) {
    console.log(item, i, arr, this);  // 可运行试试看
    return item + 1;  // 每一项加1
}, { a: 1 })
 
console.log(newArr); // [2, 3, 4, 5]

在上面例子的注释中详细阐述了map方法的细节现在要面临一个难题就是如何封装map

可以先想想for循环我们可以使用for循环来实现一个map但是在封装的时候我们会考虑一些问题我们在使用for循环的时候一个循环过程确实很好封装但是我们在for循环里面要对每一项做的事情却很难用一个固定的东西去把它封装起来因为每一个场景for循环里对数据的处理肯定都是不一样的

于是大家就想了一个很好的办法将这些不一样的操作单独用一个函数来处理让这个函数成为map方法的第一个参数具体这个回调函数中会是什么样的操作则由我们自己在使用时决定因此根据这个思路的封装实现如下

Array.prototype._map = function(fn, context) {
    var temp = [];
    if(typeof fn == 'function') {
        var k = 0;
        var len = this.length;
        // 封装for循环过程
        for(; k < len; k++) {
            // 将每一项的运算操作丢进fn里利用call方法指定fn的this指向与具体参数
            temp.push(fn.call(context, this[k], k, this))
        }
    } else {
        console.error('TypeError: '+ fn +' is not a function.');
    }
 
    // 返回每一项运算结果组成的新数组
    return temp;
}
 
var newArr = [1, 2, 3, 4]._map(function(item) {
    return item + 1;
})
// [2, 3, 4, 5]

在上面的封装中我首先定义了一个空的temp数组该数组用来存储最终的返回结果在for循环中每循环一次就执行一次参数fn函数fn的参数则使用call方法传入

在理解了map的封装过程之后我们就能够明白为什么我们在使用map时总是期望能够在第一个回调函数中有一个返回值了在eslint的规则中如果我们在使用map时没有设置一个返回值就会被判定为错误

ok明白了函数的隐式转换规则与call/apply在这种场景的使用方式我们就可以尝试通过简单的例子来了解一下柯里化了

三、由浅入深的柯里化

在前端面试中有一个关于柯里化的面试题流传甚广

实现一个add方法使计算结果能够满足如下预期:

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

很明显计算结果正是所有参数的和add方法每运行一次肯定返回了一个同样的函数继续计算剩下的参数

我们可以从最简单的例子一步一步寻找解决方案

当我们只调用两次时可以这样封装

function add(a) {
    return function(b) {
        return a + b;
    }
}
 
console.log(add(1)(2));  // 3

如果只调用三次:

function add(a) {
    return function(b) {
        return function (c) {
            return a + b + c;
        }
    }
}
 
console.log(add(1)(2)(3)); // 6

上面的封装看上去跟我们想要的结果有点类似但是参数的使用被限制得很死因此并不是我们想要的最终结果我们需要通用的封装应该怎么办?总结一下上面2个例子其实我们是利用闭包的特性将所有的参数集中到最后返回的函数里进行计算并返回结果因此我们在封装时主要的目的就是将参数集中起来计算

来看看具体实现

function add() {
    // 第一次执行时定义一个数组专门用来存储所有的参数
    var _args = [].slice.call(arguments);
 
    // 在内部声明一个函数利用闭包的特性保存_args并收集所有的参数值
    var adder = function () {
        var _adder = function() {
            [].push.apply(_args, [].slice.call(arguments));
            return _adder;
        };
 
        // 利用隐式转换的特性当最后执行时隐式转换并计算最终的值返回
        _adder.toString = function () {
            return _args.reduce(function (a, b) {
                return a + b;
            });
        }
 
        return _adder;
    }
    return adder.apply(null, [].slice.call(arguments));
}
 
// 输出结果可自由组合的参数
console.log(add(1, 2, 3, 4, 5));  // 15
console.log(add(1, 2, 3, 4)(5));  // 15
console.log(add(1)(2)(3)(4)(5));  // 15

上面的实现利用闭包的特性主要目的是想通过一些巧妙的方法将所有的参数收集在一个数组里并在最终隐式转换时将数组里的所有项加起来因此我们在调用add方法的时候参数就显得非常灵活当然也就很轻松的满足了我们的需求

那么读懂了上面的demo然后我们再来看看柯里化的定义相信大家就会更加容易理解了

柯里化(英语:Currying)又称为部分求值是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数并且返回一个新的函数的技术新函数接受余下参数并返回运算结果

  • 接收单一参数因为要携带不少信息因此常常以回调函数的理由来解决
  • 将部分参数通过回调函数等方式传入函数中
  • 返回一个新函数用于处理所有的想要传入的参数

在上面的例子中我们可以将add(1, 2, 3, 4)转换为add(1)(2)(3)(4)这就是部分求值每次传入的参数都只是我们想要传入的所有参数中的一部分当然实际应用中并不会常常这么复杂的去处理参数很多时候也仅仅只是分成两部分而已

咱们再来一起思考一个与柯里化相关的问题

假如有一个计算要求需要我们将数组里面的每一项用我们自己想要的字符给连起来我们应该怎么做?想到使用join方法就很简单

var arr = [1, 2, 3, 4, 5];
 
// 实际开发中并不建议直接给Array扩展新的方法
// 只是用这种方式演示能够更加清晰一点
Array.prototype.merge = function(chars) {
    return this.join(chars);
}
 
var string = arr.merge('-')
 
console.log(string);  // 1-2-3-4-5

增加难度将每一项加一个数后再连起来那么这里就需要map来帮助我们对每一项进行特殊的运算处理生成新的数组然后用字符连接起来了实现如下:

var arr = [1, 2, 3, 4, 5];
 
Array.prototype.merge = function(chars, number) {
    return this.map(function(item) {
        return item + number;
    }).join(chars);
}
 
var string = arr.merge('-', 1);
 
console.log(string); // 2-3-4-5-6

但是如果我们又想要让数组每一项都减去一个数组之后再连起来呢?当然和上面的加法操作一样的实现

var arr = [1, 2, 3, 4, 5];
 
Array.prototype.merge = function(chars, number) {
    return this.map(function(item) {
        return item - number;
    }).join(chars);
}
 
var string = arr.merge('~', 1);
 
console.log(string); // 0~1~2~3~4

机智的小伙伴肯定发现困惑所在了我们期望封装一个函数能同时处理不同的运算过程但是我们并不能使用一个固定的套路将对每一项的操作都封装起来于是问题就变成了和封装map的时候所面临的问题一样了我们可以借助柯里化来搞定

与map封装同样的道理既然我们事先并不确定我们将要对每一项数据进行怎么样的处理我只是知道我们需要将他们处理之后然后用字符连起来所以不妨将处理内容保存在一个函数里而仅仅固定封装连起来的这一部分需求

于是我们就有了以下的封装

// 封装很简单一句话搞定
Array.prototype.merge = function(fn, chars) {
    return this.map(fn).join(chars);
}
 
var arr = [1, 2, 3, 4];
 
// 难点在于在实际使用的时候操作怎么来定义利用闭包保存于传递num参数
var add = function(num) {
    return function(item) {
        return item + num;
    }
}
 
var red = function(num) {
    return function(item) {
        return item - num;
    }
}
 
// 每一项加2后合并
var res1 = arr.merge(add(2), '-');
 
// 每一项减2后合并
var res2 = arr.merge(red(1), '-');
 
// 也可以直接使用回调函数每一项乘2后合并
var res3 = arr.merge((function(num) {
    return function(item) {
        return item * num
    }
})(2), '-')
 
console.log(res1); // 3-4-5-6
console.log(res2); // 0-1-2-3
console.log(res3); // 2-4-6-8

大家能从上面的例子发现柯里化的特征吗?

四、柯里化通用式

通用的柯里化写法其实比我们上边封装的add方法要简单许多

var currying = function(fn) {
    var args = [].slice.call(arguments, 1);
 
    return function() {
        // 主要还是收集所有需要的参数到一个数组中便于统一计算
        var _args = args.concat([].slice.call(arguments));
        return fn.apply(null, _args);
    }
}
 
var sum = currying(function() {
    var args = [].slice.call(arguments);
    return args.reduce(function(a, b) {
        return a + b;
    })
}, 10)
 
console.log(sum(20, 10));  // 40
console.log(sum(10, 5));   // 25

五、柯里化与bind

Object.prototype.bind = function(context) {
    var _this = this;
    var args = [].prototype.slice.call(arguments, 1);
 
    return function() {
        return _this.apply(context, args)
    }
}

这个例子利用call与apply的灵活运用实现了bind的功能

在前面的几个例子中我们可以总结一下柯里化的特点:

  • 接收单一参数将更多的参数通过回调函数来搞定?
  • 返回一个新函数用于处理所有的想要传入的参数
  • 需要利用call/apply与arguments对象收集参数
  • 返回的这个函数正是用来处理收集起来的参数

希望大家读完之后都能够大概明白柯里化的概念如果想要熟练使用它就需要我们掌握更多的实际经验才行


相关文章

猜您喜欢

  • Java字节流复制图片音频 Java使用字节流实现图片音频的复制

    想了解Java使用字节流实现图片音频的复制的相关内容吗wasane在本文为您仔细讲解Java字节流复制图片音频的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Java字节流复制图片音频,Java字节流下面大家一起来学习吧..
  • java多种锁和阻塞队列 Java多线程之多种锁和阻塞队列

    想了解Java多线程之多种锁和阻塞队列的相关内容吗若能绽放光丶在本文为您仔细讲解java多种锁和阻塞队列的相关知识和一些Code实例欢迎阅读和指正我们先划重点:java锁,java阻塞队列下面大家一起来学习吧..

网友评论

Copyright 2020 www.fresh-weather.com 【世纪下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式