这一篇是函数部分的最后一篇。我们来聊聊Curry化。

十、Curry

  这部分我们主要讨论Curry化和部分函数应用的内容。但是在深入讨论之前,我们需要先了解一下函数应用的含义。

函数应用

  在一些纯粹的函数式编程语言中,函数并不描述为被调用(即called或invoked),而是描述为应用(applied)。在JavaScript中,我们可以做同样的事情,使用方法Function.prototype.apply()来应用函数,这是由于JavaScript中的函数实际上是对象,并且它们还具有如下方法。

// 定义函数
var sayHi = function(who) {
console.log("Hello" + (who? ", " + who : "") + "!");
}; // 调用函数
sayHi(); // 输出"Hello"
sayHi('world'); // 输出"Hello, world!" // 应用函数
sayHi.apply(null, ["hello"]); // 输出"Hello, hello!"

  正如上面的例子所看到的,调用(invoking)函数和应用(applying)函数可以得到完全相同的结果。apply()带有两个参数:第一个参数为将要绑定到该函数内部this的一个对象,而第二个参数是一个数组或多个参数变量,这些参数将变成可用于该函数内部的类似数组的arguments对象。如果第一个参数为null(空),那么this将指向全局对象,此时得到的结果就恰好如同调用一个非指定对象时的方法。

  当函数是一个对象的方法时,此时不能传递null引用。这种情况下,这里的对象将成为apply()的第一个参数:

// 定义函数
var alien= {
sayHi: function(who) {
console.log("Hello" + (who? ", " + who : "") + "!");
}
}
alien.sayHi('world'); // 输出"Hello, world!"
sayHi.apply(alien, ["humans"]); // 输出"Hello, humans!"

  在上面的代码中,sayHi()内部的this指向了alien对象。而在之前的例子中,this指向了全局对象。

  正如上面的两个例子所展示的那样,这些都表明我们考虑的“调用函数”并不只是“句法糖(syntactic sugar)”,而是等价于函数应用。

  请注意,除了apply()以外,Function.prototype对象还有一个call()方法,但是这仍然只是建立在apply()之上的语法糖而已。有时候最好使用该语法糖:即当函数仅带有一个参数时,可以根据实际情况避免创建只有一个元素的数组的工作。

// 在这种情况下,第二种更有效率,节省了一个数组
sayHi.apply(alien,["humans"]);
sayHi.call(alien,"humans");

部分应用

  现在我们知道,调用函数实际上就是将一个参数集合应用到一个函数中,那有没有可能只传递部分参数,而不是所有参数?这种情况就和手动处理一个数学函数所常采用的方法是相似的。假定有一个函数add()用以将两个数字加在一起:x和y。下面的代码片段展示了给定x值为5,且y值为4的情况下的解决方案。

// 出于演示的目的
// 并不是合法的JavaScript
function add(x,y) {
return x + y;
}
// 有以下函数
add(5,4); // 第1步,替换一个参数
function add(5, y){
return 5 + y;
} // 第2步,替换其他参数
function add(5, 4) {
return 5 + 4;
}

  再提醒一遍,第1、2步的代码是不合法的,仅演示目的。

  上面的代码段演示了如何手工解决部分函数应用的问题。可以获取第一个参数的值,并且在整个函数中用已知的值5替代未知的x,然后重复同样的步骤直至用完了所有的参数。

  对这个例子中的步骤1可以称为部分应用(partial application),即我们金鹰用了第一个参数。当执行部分应用时,并不会获得结果,相反会获得另一个函数。

  下面的代码片段演示了家乡的partialApply()方法的使用示例:

var add = function (x,y) {
return x + y;
}; // 完全应用
add.apply(null,[5,4]); // // 部分应用
var newadd = add.partialApply(null,[5]);
// 应用一个参数到新函数中
newadd.apply(null,[4]); //

  如上面的代码所示,部分应用向我们提供了另一个新函数,随后再以其他参数调用该函数。这种运行方式实际上与add(5)(4)有一些类似,这是由于add(5)返回了一个可在后来用(4)来调用的函数。

  此外,我们所熟悉的add(5, 4)调用方式可能并不像是“句法糖(syntactic sugar)”,相反,使用add(5)(4)才像是“句法糖(syntactic sugar)”。

  现在,返回到现实,JavaScript中并没有partialApply()方法和函数,默认情况下也并不会出现与上面类似的行为。但是可以构造出这些函数,因为JavaScript的动态性足够支持这种行为。

  使函数理解并处理部分应用的过程就成为Curry过程(Currying)。

Curry化

  这里的curry源于数学家Haskell Curry的名字。Curry化是一个转换过程,即我们执行函数转换的过程。那么,我们如何Curry化一个函数?其他的函数式语言可能已经将这种Curry化转换构建到语言本身中,并且所有的函数已经默认转换过,在JavaScript中,可以将add()函数修改成一个用于处理部分应用的Curry化函数。

  下面,我们来看个例子:

// curry化的add()函数
// 接受部分参数列表
function add(x,y) {
var oldx = x,oldy = y;
if(typeof oldy === 'undefined') { // 部分
return function(newy) {
return oldx + newy;
};
}
// 完全应用
return x + y;
} // 测试
console.log(typeof add(5)); // 输出“function”
add(3)(4); //
// 创建并存储一个新函数
var add2000 = add(2000);
add2000(19); //输出2010

  在上面的代码段中,当第一次调用add()时,它为返回的内部函数创建了一个闭包。该闭包将原始的x和y值存储到私有变量oldx和oldy中。第一个私有变量oldx将在内部函数执行的时候使用。如果没有部分应用,并且同时传递x和y值,该函数则继续执行,并简单将其相加。这种add()实现与实际需求相比显得比较冗长,在这里只是出于演示的目的这样实现。下面将显示一个更为精简的实现版本。其中并没有oldx和oldy,仅是因为原始x隐式的存储在闭包中,并且还将y作为局部变量复用,而不是像之前那样创建一个新的变量newy:

// curry化的add()函数
// 接受部分参数列表
function add(x, y) {
if(typeof y === 'undefined') { //部分
return function(y) {
return x + y;
};
}
// 完全应用
return x + y;
}

  在这些例子中,函数add()本身负责处理部分应用。但是能够以更通用的方式执行相同给的任务么?也就是说,是否可以将任意的函数转换成一个新的可以接收部分参数的函数?

function schonfinkelize(fn) {
var slice = Array.prototype.slice,
stored_args = slice.call(arguments,1);
return function () {
var new_args = slice.call(arguments),
args = stored_args.concat(new_args);
return fn.apply(null,args);
}
}

  schonfinkelize()函数可能不应该有这么复杂,只是由于JavaScript中arguments并不是一个真实的数组。从Array.prototype中借用slice()方法可以帮助我们将arguments变成一个数组,并且使用该数组更加方便。当schonfinkelize()第一次调用时,它存储了一个指向slice()方法的私有引用(名为slice),并且还存储了调用该方法后的参数(存入stored_args中),该方法仅剥离了第一个参数,这是因为第一个参数是将被curry化的函数。然后,schonfinkelize()返回了一个新函数。当这个新函数被调用时,它访问了已经私有存储的参数stored_args以及slice引用。这个新函数必须将原有的部分应用参数(stored_args)合并到新参数(new_args),然后再将它们应用到原始函数fn中(也仅在闭包中私有可用)。

  我们来测试下上面的转换方法:

function schonfinkelize(fn) {
var slice = Array.prototype.slice,
stored_args = slice.call(arguments,1);
return function () {
var new_args = slice.call(arguments),
args = stored_args.concat(new_args);
return fn.apply(null,args);
}
} // 普通函数
function add(x, y){
return x + y;
} // 将一个函数curry化并获得一个新的函数
var newadd = schonfinkelize(add,5);
console.log(newadd(4)); //输出9 // 另一种选择,直接调用新函数
console.log(schonfinkelize(add,6)(7)); //输出13 // 转换函数并不局限于单个参数或者单步Curry化
// 普通函数
function addSome(a, b, c, d, e) {
return a + b + c + d + e;
} // 可运行于任意数量的参数
console.log(schonfinkelize(addSome,1,2,3)(5,5)); // 两步curry化
var addOne = schonfinkelize(addSome,1);
console.log(addOne(10,10,10,10)); //
var addSix = schonfinkelize(addOne,2,3);
console.log(addSix(5,5)); //

  上面是完整的例子和测试。

  那什么时候适合使用Curry化呢?当发现正在调用同一个函数,并且传递的参数绝大多数都是相同的,那么该函数可能是用于Curry化的一个很好的候选参数。可以通过将一个函数集合部分应用到函数中,从而动态创建一个新函数。这个新函数将会保存重复的参数(因此,不必每次都传递这些参数),并且还会使用预填充原始函数所期望的完整参数列表。

小结

  在JavaScript中,有关函数的部分是十分重要的,我们本系列文章相关的主要函数部分已经到此告一段落了。本篇讨论了有关函数的背景和术语。学习了JavaScript中两个重要的特征。即:

  • 函数是第一类对象,可以作为带有属性和方法的值以及参数进行传递。
  • 函数提供了局部作用域,而其他打括号并不能提供这种局部作用域(当然现在的let是可以的)。此外还需要记住的是,声明的局部变量可被提升到局部作用域的顶部。

  创建函数的语法包括:

  • 1.  函数命名表达式。
  • 2. 函数表达式(与上面的相同,但是缺少一个名字),通常也称为匿名函数。
  • 3. 函数声明,与其他语言中的函数的语法类似。

  在涵盖了函数的背景和语法之后,我们学习了一些有用的模式:

  1、API模式,它们可以帮助您为函数提供更好且更整洁的接口:

    回调模式:将函数作为参数进行传递。

    配置对象:有助于保持受到控制的函数的参数数量。

    返回函数:当一个函数的返回值是另一个函数时。

    Curry化:当新函数是基于现有函数,并加上部分参数列表创建时。

  2、初始化模式,它们可以帮助您在不污染全局命名空间的情况下,使用临时变量以一种更加整洁、结构化的方式执行初始化以及设置任务(当涉及web网页和应用程序时是非常普遍的)。这些模式包括:

    即时函数:只要定义之后就立即执行。

    即时对象初始化:匿名对象组织了初始化任务,提供了可被立即调用的方法。

    初始化时分支:帮助分支代码在初始化代码执行过程中仅检测一次,这与以后在程序生命周期内多次检测相反。

  3、性能模式,可以帮助加速代码运行,这些模式包括:

    备忘模式:使用函数属性以便使得计算过的值无须再次计算。

    自定义模式:以新的主体重写本身,以使得在第二次或以后调用时仅需执行更少的工作。

  好了,函数部分到此结束了。我们下面会开始学习对象模式部分。加油!fighting!

  

《JavaScript 模式》读书笔记(4)— 函数5的更多相关文章

  1. JavaScript模式读书笔记 第4章 函数

    2014年11月10日 1.JavaScript函数具有两个特点: 函数是第一类对象    函数能够提供作用域         函数即对象,表现为:         -1,函数能够在执行时动态创建,也 ...

  2. JavaScript模式读书笔记 文章3章 文字和构造

    1.对象字面量     -1.Javascript中所创建的自己定义对象在任务时候都是可变的.能够从一个空对象開始,依据须要添加函数.对象字面量模式能够使我们在创建对象的时候向其加入函数.       ...

  3. 《你不知道的javascript》读书笔记2

    概述 放假读完了<你不知道的javascript>上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用. 这篇笔记是这本书的下半部分,上半部分请见<你不知道的java ...

  4. 《编写可维护的javascript》读书笔记(中)——编程实践

    上篇读书笔记系列之:<编写可维护的javascript>读书笔记(上) 上篇说的是编程风格,记录的都是最重要的点,不讲废话,写的比较简洁,而本篇将加入一些实例,因为那样比较容易说明问题. ...

  5. C语言深度解剖读书笔记(6.函数的核心)

    对于本节的函数内容其实就没什么难点了,但是对于函数这节又涉及到了顺序点的问题,我觉得可以还是忽略吧. 本节知识点: 1.函数中的顺序点:f(k,k++);  这样的问题大多跟编译器有关,不要去刻意追求 ...

  6. Javascript & JQuery读书笔记

    Hi All, 分享一下我学JS & JQuery的读书笔记: JS的3个不足:复杂的文档对象模型(DOM),不一致的浏览器的实现和便捷的开发,调试工具的缺乏. Jquery的选择器 a. 基 ...

  7. 《你不知道的javascript》读书笔记1

    概述 放假读完了<你不知道的javascript>上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用. js的工作原理 引擎:从头到尾负责整个js的编译和运行.(很大一部 ...

  8. JavaScript中的this—你不知道的JavaScript上卷读书笔记(三)

    this是什么? this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件.this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式.当一个函数被调用时,会 ...

  9. JavaScript词法作用域—你不知道的JavaScript上卷读书笔记(一)

    前段时间在每天往返的地铁上抽空将 <你不知道的JavaScript(上卷)>读了一遍,这本书很多部分写的很是精妙,对于接触前端时间不太久的人来说,就好像是叩开了JavaScript的另一扇 ...

  10. SQL反模式读书笔记思维导图

    在写SQL过程以及设计数据表的过程中,我们经常会走一些弯路,会做一些错误的设计.<SQL反模式>这本书针对这些经常容易出错的设计模式进行分析,解释了错误的理由.允许错误的场景,并给出更好的 ...

随机推荐

  1. 码海拾遗:简单Socket(TCP)类实现

    最近刚开始啃Unix网络编程(卷1:套接字联网API),为加深TCP连接的建立和终止的理解与记忆,记下本文,方便以后翻看. 同时留下的还有简单的Socket(TCP)类: mySocket.h #pr ...

  2. MyBatis 判断条件为等于的时候,常量需要加 .toString()

    当MyBatis 判断条件为等于的时候,常量需要加 .toString() 来转换,这种方法是稳定的,推荐使用,比如: <!-- 正确的,稳定,推荐使用 --> <if test=& ...

  3. C++走向远洋——45(警察和厨师、UML)

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...

  4. 微信小程序入门讲解

    微信小程序 注册 由于发文限制,请自行到微信公众平台注册 项目结构 project.config.json 配置文件(不需要动) app.json(用户配置) 路由pages window 整个程序样 ...

  5. go微服务框架kratos学习笔记十(熔断器)

    目录 go微服务框架kratos学习笔记十(熔断器) 什么是熔断 熔断器逻辑 kratos Breaker kratos 熔断逻辑 kratos熔断器使用说明 bladmaster client br ...

  6. Python几个简单实用的模块

    今天整理了下,工作中常用的一些高阶函数,后面持续更新...... 一.collections 二.itertools 三.functools

  7. iMX287A基于嵌入式Qt的新冠肺炎疫情监控平台

    目录 1.前言 2.数据接口的获取 3.Qt界面的实现 4.在开发板上运行Qt程序 5.最终效果 6.代码下载 @ 1.前言 之前我使用在桌面版本Qt实现了肺炎疫情监控平台:基于Qt的新冠肺炎疫情数据 ...

  8. 阿里sentinel说明及使用

    使用说明 如果只是为了让使 用Sentinel 的限流功能,只需要引入相关的jar包依赖. 添加依赖 添加相关模块的Adapter Sentinel为每个构建项目的各个组件都打包成了相应的Adapte ...

  9. LeetCode 162.Find Peak Element(M)(P)

    题目: A peak element is an element that is greater than its neighbors. Given an input array where num[ ...

  10. 微信APP生命周期、页面生命周期

    目录 小程序的启动流程 app生命周期 页面的生命周期 页面的生命周期(图) 小程序的启动流程 我们画一个图来表示一下,整个小程序的启动流程,我们就知道了: app生命周期 执行App()函数也就是注 ...