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

来一个简单的思考题。

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对象收集参数;
  • 返回的这个函数正是用来处理收集起来的参数。

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

以上就是深入详解JS函数的柯里化的详细内容,更多关于JS函数的柯里化的资料请关注Devmax其它相关文章!

深入详解JS函数的柯里化的更多相关文章

  1. html5 拖拽及用 js 实现拖拽功能的示例代码

    这篇文章主要介绍了html5 拖拽及用 js 实现拖拽,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  2. amaze ui 的使用详细教程

    这篇文章主要介绍了amaze ui 的使用详细教程,本文通过多种方法给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. swift皮筋弹动发射飞机ios源码

    这是一个款采用swift实现的皮筋弹动发射飞机游戏源码,游戏源码比较详细,大家可以研究学习一下吧。

  4. swift 学习笔记之在柯里化Currying

    有时候我们看到的方法中有很多组的参数,可能会让大家觉得疑惑,其实是因为这个方法使用了柯里化。现在result的结果是3。柯里化的好处是提供了一个模板,可以写出具有很高复用性的代码,比如上例可以指定一个运算数建立一套方法。

  5. Swift 新特性之柯里化函数

    前言此次文章,讲述的是Swift的一个新特性,可能很多iOS开发人员是第一次听这个词汇,包括我自己也是,自己也用了几天时间才总结出来,希望能帮助到各位咯,个人感觉偏向有开发经验的码友,如果零基础的看懂,希望能给个赞!什么是柯里化函数?important;font-family:Consolas,返回值为Int类型的函数functionC=functionB//result:函数的返回值res=functionC//打印60,60,60说明手动实现的柯里化函数,和系统的一样。

  6. Currying-柯里化[Swift笔记]

    Currying(柯里化)是什么?

  7. Swift中实例方法柯里化及自己实现Target-action模式

    但是target-action又是Cocoa中如此重要的一种设计模式,无论如何我们都想安全地使用的话,应该怎么办呢?OleBegemann在这篇帖子里提到了一种很好封装,这为我们如何借助柯里化,安全地改造和利用target-action提供了不少思路。示例二Swift中实现Target-ActionChristofferLern在开发者论坛上展示了如何使用纯swift语言将Swift的类型系统这一特点应用到target-action模式中。使用target-action模式,提供API的对象可以做内部类型

  8. Swift中的实例方法--柯里化方法(Curried Functions)

    Swift中的实例方法–柯里化方法声明:本文为本人翻译自国外的一片文章,英文水平有限,还望大家不吝赐教。注意方法二中BankAccount.deposit后面没有小括号,所以在这里我们并没有调用方法,只是对它的一个引用。在第二部分中你应该意识到实例方法deposit()的签名。当然,我们也可以把这些放在一行,这使得类型方法和实例方法之间的关系更加清晰:通过实例BankAccount.deposit(),被绑定到实例函数。

  9. Swift中的柯里化Currying

    什么是柯里化首先,我们来看个简单的例子:这个函数很容易理解,就是一个整型求和的函数,函数接收两个整型作为参数,并返回两个参数相加的结果。进一步理解柯里化我们看看下面的简单实例:我们最常见的用法:下面看一个非常规的用法:我们看见下面的操作实现的效果于上面的那个是一样的。熟悉了解柯里化的特性后,以后就可以自己写个高复用的类库了,当然也可以使用上篇文章中提到的Curry类库

  10. Swift函数柯里化(Currying)简谈

    这样不仅不优雅而且容易把新手绕晕,于是我们把函数摇身一变:柯里化思密达:我们这样调用:注意,以上函数的写法貌似返回类型是Spider,其实不然!

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部