翻译连载 |《你不知道的JS》姊妹篇 |《JavaScript 轻量级函数式编程》- 第 2 章:函数基础

Hello跟我学

关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。通过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,但愿能够帮助你们在学习函数式编程的道路上走的更顺畅。比心。前端

译者团队(排名不分前后):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry萝卜vavd317vivaxy萌萌zhouyaogit

第 2 章:函数基础

函数式编程不是仅仅用 function 这个关键词来编程。若是真这么简单,那我这本书能够到此为止了!重点在于:函数函数式编程的核心。这也是如何使用函数(function)才能使咱们的代码具备函数式(functional)的方法。程序员

然而,你真的明白函数的含义吗?github

在这一章,咱们将会介绍函数的基础知识,为阅读本书的后续章节打下基础。从某些方面来说,这章回顾的函数知识并非针对函数式编程者,非函数式编程者一样须要了解。但若是咱们想要充分、全面地学习函数式编程的概念,咱们须要从里到外地理解函数。编程

请作好准备,由于还有好多你未知的函数知识。segmentfault

什么是函数?

针对函数式编程,很天然而然的我会想到从函数开始。这太明显不过了,可是我认为咱们须要扎实地走好旅程的第一步。设计模式

因此......什么是函数?数组

简要的数学回顾

我知道我曾说过,离数学越远越好,可是让咱们暂且忍一小段时间,在这段时间里,咱们会尽快地回顾在代数中一些函数和图像的基本知识。浏览器

你还记得你在学校里学习任何有关 f(x) 的知识吗?还有方程 y = f(x) ?安全

现有方程式定义以下:f(x) = 2x2 + 3。这个方程有什么意义?它对应的图像是什么样的呢?以下图:

你能够注意到:对于 x 取任意值,例如 2,带入方程后会获得 11。这里的 11 表明函数的返回值,更简单来讲就是 y 值。

根据上述,如今有一个点 (2,11) 在图像的曲线上,而且当咱们有一个 x 值,咱们都能得到一个对应的 y 值。把两个值组合就能获得一个点的坐标,例如 (0,3)(-1,5)。当把全部的这些点放在一块儿,就会得到这个抛物线方程的图像,如上图所示。

因此,这些和函数式编程有什么关系?

在数学中,函数老是获取一些输入值,而后给出一个输出值。你能听到一个函数式编程的术语叫作“态射”:这是一个优雅的方式来描述一组值和另外一组值的映射关系,就像一个函数的输入值与输出值之间的关联关系。

在代数数学中,那些输入值和输出值常常表明着绘制坐标的一部分。不过,在咱们的程序中,咱们能够定义函数有各类的输入和输出值,而且它们不须要和绘制在图表上的曲线有任何关系。

函数 vs 程序

为何全部的讨论都围绕数学和图像?由于在某种程度上,函数式编程就是使用在数学意义上的方程做为函数

你可能会习觉得常地认为函数就是程序。它们之间的区别是什么?程序就是一个任意的功能集合。它或许有许多个输入值,或许没有。它或许有一个输出值( return 值),或许没有。

而函数则是接收输入值,并明确地 return 值。

若是你计划使用函数式编程,你应该尽量多地使用函数,而不是程序。你全部编写的 function 应该接收输入值,而且返回输出值。这么作的缘由是多方面的,咱们将会在后面的书中来介绍的。

函数输入

从上述的定义出发,全部的函数都须要输入。

你有时听人们把函数的输入值称为 “arguments” 或者 “parameters” 。因此它究竟是什么?

arguments 是你输入的值(实参), parameters 是函数中的命名变量(形参),用于接收函数的输入值。例子以下:

function foo(x,y) {
    // ..
}

var a = 3;

foo( a, a * 2 );

aa * 2(即为 6)是函数 foo(..) 调用的 argumentsxyparameters,用于接收参数值(分别为 36 )。

注意: 在 JavaScript 中,实参的个数不必彻底符合形参的个数。若是你传入许多个实参,并且多过你所声明的形参,这些值仍然会原封不动地被传入。你能够经过不一样的方式去访问,包含了你之前可能听过的老办法 —— arguments 对象。反之,你传入少于声明形参个数的实参,全部缺乏的参数将会被赋予 undefined 变量,意味着你仍然能够在函数做用域中使用它,但值是 undefined

输入计数

一个函数所“指望”的实参个数是取决于已声明的形参个数,即你但愿传入多少参数。

function foo(x,y,z) {
    // ..
}

foo(..) 指望三个实参,由于它声明了三个形参。这里有一个特殊的术语:Arity。Arity 指的是一个函数声明的形参数量。 foo(..) 的 Arity 是 3

你可能须要在程序运行时获取函数的 Arity,使用函数的 length 属性便可。

function foo(x,y,z) {
    // ..
}

foo.length;             // 3

在执行时要肯定 Arity 的一个缘由是:一段代码接受一个函数的指针引用,有可能这个引用指向不一样来源,咱们要根据这些来源的 Arity 传入不一样的参数值。

举个例子,若是 fn 可能指向的函数分别指望 一、2 或 3 个参数,但你只但愿把变量 x 放在最后的位置传入:

// fn 是一些函数的引用
// x 是存在的值

if (fn.length == 1) {
    fn( x );
}
else if (fn.length == 2) {
    fn( undefined, x );
}
else if (fn.length == 3) {
    fn( undefined, undefined, x );
}

提示: 函数的 length 属性是一个只读属性,而且它是在最初声明函数的时候就被肯定了。它应该当作用来描述如何使用该函数的一个基本元数据。

须要注意的是,某些参数列表的变量会让 length 属性变得不一样于你的预期。别紧张,咱们将会在后续的章节逐一解释这些特性(引入 ES6):

function foo(x,y = 2) {
    // ..
}

function bar(x,...args) {
    // ..
}

function baz( {a,b} ) {
    // ..
}

foo.length;             // 1
bar.length;             // 1
baz.length;             // 1

若是你使用这些形式的参数,你或许会被函数的 length 值吓一跳。

那咱们怎么获得当前函数调用时所接收到的实参个数呢?这在之前很是简单,但如今状况稍微复杂了一些。每个函数都有一个 arguments 对象(类数组)存放须要传入的参数。你能够经过 argumentslength 值来找出有多少传入的参数:

function foo(x,y,z) {
    console.log( arguments.length );    // 2
}

foo( 3, 4 );

因为 ES5(特别是严格模式下)的 arguments 不被一些人认同,不少人尽量地避免使用。尽管如此,它永远不会被移除,这是由于在 JS 中咱们“永远不会”由于便利性而去牺牲向后的兼容性,但我仍是强烈建议不要去使用它。

然而,当你须要知道参数个数的时候,arguments.length 仍是能够用的。在将来版本的 JS 或许会新增特性来替代 arguments.length,若是成真,那么咱们能够彻底把 arguments 抛诸脑后。

请注意:不要经过 arguments[1] 访问参数的位置。只要记住 arguments.length

除此以外,你或许想知道如何访问那些超出声明的参数?这个问题我一下子会告诉你,不过你先要问本身的问题是,“为何我想要知道这个?”。认真地思考一段时间。

发生这种状况应该是很是罕见的。由于这不会是你平常须要的,也不会是你编写函数时所必要的东西。若是这种状况真的发生,你应该花 20 分钟来试着从新设计函数,或者命名那些多出来的参数。

带有可变数量参数的函数被称为 variadic。有些人更喜欢这样的函数设计,不过你会发现,这正是函数式编程者想要避免的。

好了,上面的重点已经讲得够多了。

例如,当你须要像数组那样访问参数,颇有可能的缘由是你想要获取的参数没有在一个规范的位置。咱们如何处理?

ES6 救星来了!让咱们用 ... 操做符声明咱们的函数,也被当作 “spread”、“rest” 或者 “gather” (我比较偏心)说起。

function foo(x,y,z,...args) {
    // ..
}

看到参数列表中的 ...args 了吗?那就是 ES6 用来告诉解析引擎获取全部剩余的未命名参数,并把它们放在一个真实的命名为 args 的数组。args 不管是否是空的,它永远是一个数组。但它不包含已经命名的 xyz 参数,只会包含超出前三个值的传入参数。

function foo(x,y,z,...args) {
    console.log( x, y, z, args );
}

foo();                  // undefined undefined undefined []
foo( 1, 2, 3 );         // 1 2 3 []
foo( 1, 2, 3, 4 );      // 1 2 3 [ 4 ]
foo( 1, 2, 3, 4, 5 );   // 1 2 3 [ 4, 5 ]

因此,若是你诚心想要设计一个函数,而且计算出任意传入参数的个数,那就在最后用 ...args (或任何你喜欢的名称)。如今你有一个真正的、好用的数组来获取这些参数值了。

你须要注意的是: 4 所在的位置是 args 的第 0 个,不是在第 3 个位置。它的 length 值也不包含 123...args 剩下全部的值, 但不包括 xyz

甚至能够直接在参数列中使用 ... 操做符,没有其余正式声明的参数也不要紧:

function foo(...args) {
    // ..
}

如今 args 是一个由参数组成的完整数组,你能够尽情使用 args.length 来获取传入的参数。你也能够安全地使用 args[1] 或者 args[317]。固然,别真的传 318 个参数!

说到 ES6 的好,你确定想知道一些小秘诀。在这里将会介绍一些,更多的内容推荐你阅读《You Don't Know JS: ES6 & Beyond》这本书的第 2 章。

关于实参的小技巧

若是你但愿调用函数的时候只传一个数组代替以前的多个参数,该怎么办?

function foo(...args) {
    console.log( args[3] );
}

var arr = [ 1, 2, 3, 4, 5 ];

foo( ...arr );                      // 4

咱们的新朋友 ... 在这里被使用到了,但不只仅在形参列表,在函数调用的时候,一样使用在实参列表。在这里的状况有所不一样:在形参列表,它把实参整合。在实参列表,它把实参展开。因此 arr 的内容是以函数 foo(..) 引用的单独参数进行展开。你能理解传入一个引用值和传入整个 arr 数组二者之间的不一样了吗?

顺带一提,多个值和 ... 是能够相互交错放置的,以下:

var arr = [ 2 ];

foo( 1, ...arr, 3, ...[4,5] );      // 4

在对称的意义上来考虑 ... :在值列表的状况,它会展开。在赋值的状况,它就像形参列表同样,由于实参会赋值到形参上。

不管采起什么行为, ... 都会让实参数组更容易操做。那些咱们使用实参数组 slice(..)concat(..)apply(..) 的日子已通过去了。

关于形参的小技巧

在 ES6 中,形参能够声明默认值。当形参没有传入到实参中,或者传入值是 undefined,会进行默认赋值的操做。

思考下面代码:

function foo(x = 3) {
    console.log( x );
}

foo();                  // 3
foo( undefined );       // 3
foo( null );            // null
foo( 0 );               // 0

注意: 咱们不会更加详细地解释了,可是默认值表达式是惰性的,这意味着仅当须要的时候,它才会被计算。它一样也能够是一些有效的 JS 表达式,甚至一个函数引用。许多很是酷的小技巧用到了这个方法。例如,你能够这样在你的参数列声明 x = required(),而且在函数 required()抛出 "This argument is required." 来确信总有人用你指定的实参或形参来引用你的函数。

另外一个咱们能够在参数中使用的 ES6 技巧,被称为“解构”。在这里咱们只会简单一提,由于要说清这个话题实在太过繁杂。在这里推荐《ES6 & Beyond》这本书了解更多信息。

还记得咱们以前提到的能够接受 318 个参数的 foo(..) 吗?

function foo(...args) {
    // ..
}

foo( ...[1,2,3] );

若是咱们想要把函数内的参数从一个个单独的参数值替换为一个数组,应该怎么作?这里有两个 ... 的写法:

function foo(args) {
    // ..
}

foo( [1,2,3] );

这个很是简单。但若是咱们想要命名传入数组的第 一、2 个值,该怎么作?咱们不能用单独传入参数的办法了,因此这彷佛看起来无能为力。不过解构能够回答这个问题:

function foo( [x,y,...args] = [] ) {
    // ..
}

foo( [1,2,3] );

你看到了在参数列出现的 [ .. ] 了吗?这就是数组解构。解构是经过你指望的模式来描述数据(对象,数组等),并分配(赋值)值的一种方式。

在这里例子中,解构告诉解析器,一个数组应该出现的赋值位置(即参数)。这种模式是:拿出数组中的第一个值,而且赋值给局部参数变量 x,第二个赋值给 y,剩下的则组成 args

你能够经过本身手动处理达到一样的效果:

function foo(params) {
    var x = params[0];
    var y = params[1];
    var args = params.slice( 2 );

    // ..
}

如今咱们能够发现,在咱们这本书中要屡次提到的第一条原则:声明性代码一般比命令式代码更干净。

声明式代码,如同以前代码片断里的解构,强调一段代码的输出结果。命令式代码,像刚才咱们本身手动赋值的例子,注重的是如何获得结果。若是你稍晚再读这一段代码,你必须在脑子里面再执行一遍才能获得你想要的结果。这个结果是编写在这儿,可是不是直接可见的。

只要可能,不管咱们的语言和咱们的库或框架容许咱们达到什么程度,咱们都应该尽量使用声明性的和自解释的代码

正如咱们能够解构的数组,咱们能够解构的对象参数:

function foo( {x,y} = {} ) {
    console.log( x, y );
}

foo( {
    y: 3
} );                    // undefined 3

咱们传入一个对象做为一个参数,它解构成两个独立的参数变量 xy,从传入的对象中分配相应属性名的值。咱们不在乎属性值 x 到底存不存在对象上,若是不存在,它最终会如你所想被赋值为 undefined

可是我但愿你注意:对象解构的部分参数是将要传入 foo(..) 的对象。

如今有一个正常可用的调用现场 foo(undefined,3),它用于映射实参到形参。咱们试着把 3 放到第二个位置,分配给 y。可是在新的调用现场上用到了参数解构,一个简单的对象属性表明了实参 3 应该分配给形参(y)。

咱们不须要操心 x 应该放在哪一个调用现场。由于事实上,咱们不用去关心 x,咱们只须要省略它,而不是分配 undefined 值。

有一些语言对这样的操做有一个直接的特性:命名参数。换句话说,在调用现场,经过标记输入值来告诉它映射关系。JavaScript 没有命名参数,不过退而求其次,参数对象解构是一个选择。

使用对象解构来传入多个匿名参数是函数式编程的优点,这个优点在于使用一个参数(对象)的函数能更容易接受另外一个函数的单个输出。这点会在后面讨论到。

回想一下,术语 Arity 是指指望函数接收多少个参数。Arity 为 1 的函数也被称为一元函数。在函数式编程中,咱们但愿咱们的函数在任何的状况下是一元的,有时咱们甚至会使用各类技巧来将高 Arity 的函数都转换为一元的形式。

注意: 在第 3 章,咱们将从新讨论命名参数的解构技巧,并使用它来处理关于参数排序的问题。

随着输入而变化的函数

思考如下函数

function foo(x,y) {
    if (typeof x == "number" && typeof y == "number") {
        return x * y;
    }
    else {
        return x + y;
    }
}

明显地,这个函数会根据你传入的值而有所不一样。

举例:

foo( 3, 4 );            // 12

foo( "3", 4 );          // "34"

程序员这样定义函数的缘由之一是,更容易经过同一个函数来重载不一样的功能。最广为人知的例子就是 jQuery 提供的 $(..)。"$" 函数大约有十几种不一样的功能 —— 从 DOM 元素查找,到 DOM 元素建立,到等待 “DOMContentLoaded” 事件后,执行一个函数,这些都取决于你传递给它的参数。

上述函数,显而易见的优点是 API 变少了(仅仅是一个 $(..) 函数),但缺点体如今阅读代码上,你必须仔细检查传递的内容,理解一个函数调用将作什么。

经过不一样的输入值让一个函数重载拥有不一样的行为的技巧叫作特定多态(ad hoc polymorphism)。

这种设计模式的另外一个表现形式就是在不一样的状况下,使函数具备不一样的输出(在下一章节会提到)。

警告: 要对方便的诱惑有警戒之心。由于你能够经过这种方式设计一个函数,即便能够当即使用,但这个设计的长期成本可能会让你后悔。

函数输出

在 JavaScript 中,函数只会返回一个值。下面的三个函数都有相同的 return 操做。

function foo() {}

function bar() {
    return;
}

function baz() {
    return undefined;
}

若是你没有 return 值,或者你使用 return;,那么则会隐式地返回 undefined 值。

若是想要尽量靠近函数式编程的定义:使用函数而非程序,那么咱们的函数必须永远有返回值。这也意味着他们必须明确地 return 一个值,一般这个值也不是 undefined

一个 return 的表达式仅可以返回一个值。因此,若是你须要返回多个值,切实可行的办法就是把你须要返回的值放到一个复合值当中去,例如数组、对象:

function foo() {
    var retValue1 = 11;
    var retValue2 = 31;
    return [ retValue1, retValue2 ];
}

解构方法可使用于解构对象或者数组类型的参数,也可使用在平时的赋值当中:

function foo() {
    var retValue1 = 11;
    var retValue2 = 31;
    return [ retValue1, retValue2 ];
}

var [ x, y ] = foo();
console.log( x + y );           // 42

将多个值集合成一个数组(或对象)作为返回值,而后再解构回不一样的值,这无形中让一个函数能有多个输出结果。

提示: 在这里我十分建议你花一点时间来思考:是否须要避免函数有可重构的多个输出?或许将这个函数分为两个或更多个更小的单用途函数。有时会须要这么作,有时可能不须要,但你应该至少考虑一下。

提早 return

return 语句不只仅是从函数中返回一个值,它也是一个流量控制结构,它能够结束函数的执行。所以,具备多个 return 语句的函数具备多个可能的退出点,这意味着若是输出的路径不少,可能难以读取并理解函数的输出行为。

思考如下:

function foo(x) {
    if (x > 10) return x + 1;

    var y = x / 2;

    if (y > 3) {
        if (x % 2 == 0) return x;
    }

    if (y > 1) return y;

    return x;
}

突击测验:不要做弊也不要在浏览器中运行这段代码,请思考 foo(2) 返回什么? foo(4) 返回什么? foo(8)foo(12) 呢?

你对本身的回答有多少信心?你付出多少精力来得到答案?我错了两次后,我试图仔细思考而且写下来!

我认为在许多可读性的问题上,是由于咱们不只使用 return 返回不一样的值,更把它做为一个流控制结构——在某些状况下能够提早退出一个函数的执行。咱们显然有更好的方法来编写流控制( if 逻辑等),也有办法使输出路径更加明显。

注意: 突击测验的答案是:22813

思考如下版本的代码:

function foo(x) {
    var retValue;

    if (retValue == undefined && x > 10) {
        retValue = x + 1;
    }

    var y = x / 2;

    if (y > 3) {
        if (retValue == undefined && x % 2 == 0) {
            retValue = x;
        }
    }

    if (retValue == undefined && y > 1) {
        retValue = y;
    }

    if (retValue == undefined) {
        retValue = x;
    }

    return retValue;
}

这个版本毫无疑问是更冗长的。可是在逻辑上,我认为这比上面的代码更容易理解。由于在每一个 retValue 能够被设置的分支, 这里都有个守护者以确保 retValue 没有被设置过才执行。

相比在函数中提前使用 return,咱们更应该用经常使用的流控制( if 逻辑 )来控制 retValue 的赋值。到最后,咱们 return retValue

我不是说,你只能有一个 return,或你不该该提前 return,我只是认为在定义函数时,最好不要用 return 来实现流控制,这样会创造更多的隐含意义。尝试找出最明确的表达逻辑的方式,这每每是最好的办法。

return 的输出

有个技巧你可能在你的大多数代码里面使用过,而且有可能你本身并无特别意识到,那就是让一个函数经过改变函数体外的变量产出一些值。

还记得咱们以前提到的函数f(x) = 2x2 + 3吗?咱们能够在 JS 中这样定义:

var y;

function foo(x) {
    y = (2 * Math.pow( x, 2 )) + 3;
}

foo( 2 );

y;                      // 11

我知道这是一个无聊的例子。咱们彻底能够用 return 来返回,而不是赋值给 y

function foo(x) {
    return (2 * Math.pow( x, 2 )) + 3;
}

var y = foo( 2 );

y;                      // 11

这两个函数完成相同的任务。咱们有什么理由要从中挑一个吗?是的,绝对有。

解释这二者不一样的一种方法是,后一个版本中的 return 表示一个显式输出,而前者的 y 赋值是一个隐式输出。在这种状况下,你可能已经猜到了:一般,开发人员喜欢显式模式而不是隐式模式。

可是,改变一个外部做用域的变量,就像咱们在 foo(..) 中所作的赋值 y 同样,只是实现隐式输出的一种方式。一个更微妙的例子是经过引用对非局部值进行更改。

思考:

function sum(list) {
    var total = 0;
    for (let i = 0; i < list.length; i++) {
        if (!list[i]) list[i] = 0;

        total = total + list[i];
    }

    return total;
}

var nums = [ 1, 3, 9, 27, , 84 ];

sum( nums );            // 124

很明显,这个函数输出为 124,咱们也很是明确地 return 了。但你是否发现其余的输出?查看代码,并检查 nums 数组。你发现区别了吗?

为了填补 4 位置的空值 undefined,这里使用了 0 代替。尽管咱们在局部操做 list 参数变量,但咱们仍然影响了外部的数组。

为何?由于 list 使用了 nums 的引用,不是对 [1,3,9,..] 的值复制,而是引用复制。由于 JS 对数组、对象和函数都使用引用和引用复制,咱们能够很容易地从函数中建立输出,即便是无意的。

这个隐式函数输出在函数式编程中有一个特殊的名称:反作用。固然,没有反作用的函数也有一个特殊的名称:纯函数。咱们将在之后的章节讨论这些,但关键是咱们应该喜欢纯函数,而且要尽量地避免反作用。

函数功能

函数是能够接受而且返回任何类型的值。一个函数若是能够接受或返回一个甚至多个函数,它被叫作高阶函数。

思考:

function forEach(list,fn) {
    for (let i = 0; i < list.length; i++) {
        fn( list[i] );
    }
}

forEach( [1,2,3,4,5], function each(val){
    console.log( val );
} );
// 1 2 3 4 5

forEach(..) 就是一个高阶函数,由于它能够接受一个函数做为参数。

一个高阶函数一样能够把一个函数做为输出,像这样:

function foo() {
    var fn = function inner(msg){
        console.log( msg );
    };

    return fn;
}

var f = foo();

f( "Hello!" );          // Hello!

return 不是“输出”函数的惟一办法。

function foo() {
    var fn = function inner(msg){
        console.log( msg );
    };

    bar( fn );
}

function bar(func) {
    func( "Hello!" );
}

foo();                  // Hello!

将其余函数视为值的函数是高阶函数的定义。函数式编程者们应该学会这样写!

保持做用域

在全部编程,尤为是函数式编程中,最强大的就是:当一个函数内部存在另外一个函数的做用域时,对当前函数进行操做。当内部函数从外部函数引用变量,这被称做闭包。

实际上,闭包是它能够记录而且访问它做用域外的变量,甚至当这个函数在不一样的做用域被执行。

思考:

function foo(msg) {
    var fn = function inner(){
        console.log( msg );
    };

    return fn;
}

var helloFn = foo( "Hello!" );

helloFn();              // Hello!

处于 foo(..) 函数做用域中的 msg 参数变量是能够在内部函数中被引用的。当 foo(..) 执行时,而且内部函数被建立,函数能够获取 msg 变量,即便 return 后仍可被访问。

虽然咱们有函数内部引用 helloFn,如今 foo(..) 执行后,做用域应该回收,这也意味着 msg 也不存在了。不过这个状况并不会发生,函数内部会由于闭包的关系,将 msg 保留下来。只要内部函数(如今被处在不一样做用域的 helloFn 引用)存在, msg 就会一直被保留。

让咱们看看闭包做用的一些例子:

function person(id) {
    var randNumber = Math.random();

    return function identify(){
        console.log( "I am " + id + ": " + randNumber );
    };
}

var fred = person( "Fred" );
var susan = person( "Susan" );

fred();                 // I am Fred: 0.8331252801601532
susan();                // I am Susan: 0.3940753308893741

identify() 函数内部有两个闭包变量,参数 idrandNumber

闭包不只限于获取变量的原始值:它不只仅是快照,而是直接连接。你能够更新该值,并在下次访问时获取更新后的值。

function runningCounter(start) {
    var val = start;

    return function current(increment = 1){
        val = val + increment;
        return val;
    };
}

var score = runningCounter( 0 );

score();                // 1
score();                // 2
score( 13 );            // 15

警告: 咱们将在以后的段落中介绍更多。不过在这个例子中,你须要尽量避免使用闭包来记录状态更改(val)。

若是你须要设置两个输入,一个你已经知道,另外一个还须要后面才能知道,你可使用闭包来记录第一个输入值:

function makeAdder(x) {
    return function sum(y){
        return x + y;
    };
}

//咱们已经分别知道做为第一个输入的 10 和 37
var addTo10 = makeAdder( 10 );
var addTo37 = makeAdder( 37 );

// 紧接着,咱们指定第二个参数
addTo10( 3 );           // 13
addTo10( 90 );          // 100

addTo37( 13 );          // 50

一般, sum(..) 函数会一块儿接收 xy 并相加。可是在这个例子中,咱们接收而且首先记录(经过闭包) x 的值,而后等待 y 被指定。

注意: 在连续函数调用中指定输入,这种技巧在函数式编程中很是广泛,而且有两种形式:偏函数应用和柯里化。咱们稍后会在文中深刻讨论。

固然,由于函数若是只是 JS 中的值,咱们能够经过闭包来记住函数值。

function formatter(formatFn) {
    return function inner(str){
        return formatFn( str );
    };
}

var lower = formatter( function formatting(v){
    return v.toLowerCase();
} );

var upperFirst = formatter( function formatting(v){
    return v[0].toUpperCase() + v.substr( 1 ).toLowerCase();
} );

lower( "WOW" );             // wow
upperFirst( "hello" );      // Hello

函数式编程并非在咱们的代码中分配或重复 toUpperCase()toLowerCase() 逻辑,而是鼓励咱们用优雅的封装方式来建立简单的函数。

具体来讲,咱们建立两个简单的一元函数 lower(..)upperFirst(..),由于这些函数在咱们程序中,更容易与其余函数配合使用。

提示: 你知道如何让 upperFirst(..) 使用 lower(..) 吗?

咱们将在本书的后续中大量使用闭包。若是抛开整个编程来讲,它多是全部函数式编程中最重要的基础。但愿你能用得舒服!

句法

在咱们函数入门开始以前,让咱们花点时间来讨论它的语法。

不一样于本书中的许多其余部分,本节中的讨论主要是意见和偏好,不管你是否赞成这里提出的观点或采起相反的观点。这些想法是很是主观的,尽管许多人彷佛对此很是执着。不过最终,都由你决定。

什么是名称?

在语法上,函数声明须要包含一个名称:

function helloMyNameIs() {
    // ..
}

可是函数表达式能够命名或者匿名:

foo( function namedFunctionExpr(){
    // ..
} );

bar( function(){    // <-- 这就是匿名的!
    // ..
} );

顺便说一句,匿名的意思是什么?具体来讲,函数具备一个 name 的属性,用于保存函数在语法上设定名称的字符串值,例如 "helloMyNameIs""FunctionExpr"。 这个name 属性特别用于 JS 环境的控制台或开发工具。当咱们在堆栈轨迹中追踪(一般来自异常)时,这个属性能够列出该函数。

而匿名函数一般显示为:(anonymous function)

若是你曾经试着在一个异常的堆栈轨迹中调试一个 JS 程序,你可能已经发现痛苦了:看到 (anonymous function) 出现。这个列表条目不给开发人员任何关于异常来源路径的线索。它没有给咱们开发者提供任何帮助。

若是你命名了你的函数表达式,名称将会一直被使用。因此若是你使用了一个良好的名称 handleProfileClicks 来取代 foo,你将会在堆栈轨迹中得到更多的信息。

在 ES6 中,匿名表达式能够经过名称引用来得到名称。思考:

var x = function(){};

x.name;         // x

若是解析器可以猜到你可能但愿函数采用什么名称,那么它将会继续下去。

但请注意,并非全部的句法形式均可以用名称引用。最多见的地方是函数表达式是函数调用的参数:

function foo(fn) {
    console.log( fn.name );
}

var x = function(){};

foo( x );               // x
foo( function(){} );    //

当名称不能直接从周围的语法中被推断时,它仍会是一个空字符串。这样的函数将在堆栈轨迹中的被报告为一个 (anonymous function)

除了调试问题以外,函数被命名还有一个其余好处。首先,句法名称(又称词汇名)是能够被函数内部的自引用。自引用是递归(同步和异步)所必需的,也有助于事件处理。

思考这些不一样的状况:

// 同步状况:
function findPropIn(propName,obj) {
    if (obj == undefined || typeof obj != "object") return;

    if (propName in obj) {
        return obj[propName];
    }
    else {
        let props = Object.keys( obj );
        for (let i = 0; i < props.length; i++) {
            let ret = findPropIn( propName, obj[props[i]] );
            if (ret !== undefined) {
                return ret;
            }
        }
    }
}
// 异步状况:
setTimeout( function waitForIt(){
    // it 存在了吗?
    if (!o.it) {
        // 再试一次
        setTimeout( waitForIt, 100 );
    }
}, 100 );
// 事件处理未绑定
document.getElementById( "onceBtn" )
    .addEventListener( "click", function handleClick(evt){
        // 未绑定的 event
        evt.target.removeEventListener( "click", handleClick, false );

        // ..
    }, false );

在这些状况下,使用命名函数的函数名引用,是一种有用和可靠的在自身内部自引用的方式。

此外,即便在单行函数的简单状况下,命名它们每每会使代码更加明了,从而让之前没有阅读过的人更容易阅读:

people.map( function getPreferredName(person){
    return person.nicknames[0] || person.firstName;
} )
// ..

光看函数 getPreferredName(..) 的代码,并不能很明确告诉咱们这里的操做是什么意图。但有名称就能够增长代码可读性。

常用匿名函数表达式的另外一个地方是 IIFE (当即执行函数表达式):

(function(){

    // 我是 IIFE!

})();

你几乎从没看到为 IIFE 函数来命名,但他们应该命名。为何?咱们刚刚提到过的缘由:堆栈轨迹调试,可靠的自我引用和可读性。若是你想不出你的 IIFE 应该叫什么,请至少使用 IIFE:

(function IIFE(){

    // 如今你真的知道我叫 IIFE!

})();

我有许多个理由能够解释命名函数比匿名函数更可取。事实上,我甚至认为匿名函数都是不可取的。相比命名函数,他们没有任何优点。

写匿名功能很是容易,由于咱们彻底不用在想名称这件事上费神费力。

诚实来说,我也像你们同样在这个地方犯错。我不喜欢在起名称这件事上浪费时间。我能想到命名一个函数的前 3 或 4 个名字一般是很差的。我必须反复思考这个命名。这个时候,我宁愿只是用一个匿名函数表达。

可是,咱们把易写性拿来与易读性作交换,这不是一个好选择。由于懒而不想为你的函数命名,这是常见的使用匿名功能的借口。

命名全部单个函数。若是你对着你写的函数,想不出一个好名称,我明确告诉你,那是你并无彻底理解这个函数的目的——或者来讲它的目的太普遍或太抽象。你须要从新设计功能,直到它更清楚。从这个角度说,一个名称会更明白清晰。

从我本身的经验中证实,在思考名称的过程当中,我会更好地了解它,甚至重构其设计,以提升可读性和可维护性。这些时间的投入是值得的。

没有 function 的函数

到目前为止,咱们一直在使用完整的规范语法功能。可是相信你也对新的 ES6 => 箭头函数语法有所耳闻。

比较:

people.map( function getPreferredName(person){
    return person.nicknames[0] || person.firstName;
} )
// ..

people.map( person => person.nicknames[0] || person.firstName );

哇!

关键字 function 没了,return() 括号,{} 花括号和 ; 分号也是这样。全部这一切,都是咱们与一个胖箭头作了交易: =>

但还有另外一件事咱们忽略了。 你发现了吗?getPreferredName 函数名也没了。

那就对了。 => 箭头函数是词法匿名的。没有办法合理地为它提供一个名字。他们的名字能够像常规函数同样被推断,可是,最多见的函数表达式值做为参数的状况将不会起任何做用了。

假设 person.nicknames 由于一些缘由没有被定义,一个异常将会被抛出,意味着这个 (anonymous function) 将会在追踪堆栈的最上层。啊!

=> 箭头函数的匿名性是 => 的阿喀琉斯之踵。这让我不能遵照刚刚所说的命名原则了:阅读困难,调试困难,没法自我引用。

可是,这还不够糟糕,要面对的另外一个问题是,若是你的函数定义有不一样的场景,那么你必需要一大堆细微差异的语句来实现。我不会在这里详细介绍全部,但会简要地说:

people.map( person => person.nicknames[0] || person.firstName );

// 多个参数? 须要 ( )
people.map( (person,idx) => person.nicknames[0] || person.firstName );

// 解构参数? 须要 ( )
people.map( ({ person }) => person.nicknames[0] || person.firstName );

// 默认参数? 须要 ( )
people.map( (person = {}) => person.nicknames[0] || person.firstName );

// 返回对象? 须要 ( )
people.map( person =>
    ({ preferredName: person.nicknames[0] || person.firstName })
);

在函数式编程中, => 使人兴奋的地方在于它几乎彻底遵循函数的数学符号,特别是像 Haskell 这样的函数式编程语言。=> 箭头函数语法甚至能够用于数学交流。

咱们进一步地来深挖,我建议使用 => 的论点是,经过使用更轻量级的语法,能够减小函数之间的视觉边界,也让咱们使用偷懒的方式来使用它,这也是函数式编程者的另外一个爱好。

我认为大多数的函数式编程者都会对此睁只眼闭只眼。他们喜欢匿名函数,喜欢简洁语法。可是像我以前说过的那样:这都由你决定。

注意: 虽然我不喜欢在个人应用程序中使用 =>,但咱们将在本书的其他部分屡次使用它,特别是当咱们介绍典型的函数式编程实战时,它能简化、优化代码片断中的空间。不过,加强或减弱代码的可读性也取决你本身作的决定。

来讲说 This ?

若是您不熟悉 JavaScript 中的 this 绑定规则,我建议去看我写的《You Don't Know JS: this & Object Prototypes》。 出于这章的须要,我会假定你知道在一个函数调用(四种方式之一)中 this 是什么。可是若是你依然对 this 感到迷惑,告诉你个好消息,接下来咱们会总结在函数式编程中你不该当使用 this

JavaScript 的 function 有一个 this 关键字,每一个函数调用都会自动绑定。this 关键字有许多不一样的方式描述,但我更喜欢说它提供了一个对象上下文来使该函数运行。

this 是函数的一个隐式的输入参数。

思考:

function sum() {
    return this.x + this.y;
}

var context = {
    x: 1,
    y: 2
};

sum.call( context );        // 3

context.sum = sum;
context.sum();              // 3

var s = sum.bind( context );
s();                        // 3

固然,若是 this 可以隐式地输入到一个函数当中去,一样的,对象也能够做为显式参数传入:

function sum(ctx) {
    return ctx.x + ctx.y;
}

var context = {
    x: 1,
    y: 2
};

sum( context );

这样的代码更简单,在函数式编程中也更容易处理:当显性输入值时,咱们很容易将多个函数组合在一块儿, 或者使用下一章输入适配技巧。然而当咱们作一样的事使用隐性输入时,根据不一样的场景,有时候会难处理,有时候甚至不可能作到。

还有一些技巧,是基于 this 完成的,例如原型受权(在《this & Object Prototypes》一书中也详细介绍):

var Auth = {
    authorize() {
        var credentials = this.username + ":" + this.password;
        this.send( credentials, resp => {
            if (resp.error) this.displayError( resp.error );
            else this.displaySuccess();
        } );
    },
    send(/* .. */) {
        // ..
    }
};

var Login = Object.assign( Object.create( Auth ), {
    doLogin(user,pw) {
        this.username = user;
        this.password = pw;
        this.authorize();
    },
    displayError(err) {
        // ..
    },
    displaySuccess() {
        // ..
    }
} );

Login.doLogin( "fred", "123456" );

注意: Object.assign(..) 是一个 ES6+ 的实用工具,它用来将属性从一个或者多个源对象浅拷贝到目标对象: Object.assign( target, source1, ... )

这段代码的做用是:如今咱们有两个独立的对象 LoginAuth,其中 Login 执行原型受权给 Auth。经过委托和隐式的 this 共享上下文对象,这两个对象在 this.authorize() 函数调用期间其实是组合的,因此这个 this 上的属性或方法能够与 Auth.authorize(..) 动态共享 this

this 由于各类缘由,不符合函数式编程的原则。其中一个明显的问题是隐式 this 共享。但咱们能够更加显式地,更靠向函数式编程的方向:

// ..

authorize(ctx) {
    var credentials = ctx.username + ":" + ctx.password;
    Auth.send( credentials, function onResp(resp){
        if (resp.error) ctx.displayError( resp.error );
        else ctx.displaySuccess();
    } );
}

// ..

doLogin(user,pw) {
    Auth.authorize( {
        username: user,
        password: pw
    } );
}

// ..

从个人角度来看,问题不在于使用对象来进行操做,而是咱们试图使用隐式输入取代显式输入。当我戴上名为函数式编程的帽子时,我应该把 this 放回衣架上。

总结

函数是强大的。

如今,让咱们清楚地理解什么是函数:它不只仅是一个语句或者操做的集合,并且须要一个或多个输入(理想状况下只需一个!)和一个输出。

函数内部的函数能够取到闭包外部变量,并记住它们以备往后使用。这是全部程序设计中最重要的概念之一,也是函数式编程的基础。

要警戒匿名函数,特别是 => 箭头函数。虽然在编程时用起来很方便,可是会对增长代码阅读的负担。咱们学习函数式编程的所有理由是为了书写更具可读性的代码,因此不要赶时髦去用匿名函数。

别用 this 敏感的函数。这不须要理由。

【上一章】翻译连载 |《JavaScript 轻量级函数式编程》- 第 1 章:为何使用函数式编程?

【下一章】翻译连载 |《JavaScript 轻量级函数式编程》- 第3章:管理函数的输入

iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。

沪江Web前端上海团队招聘【Web前端架构师】,有意者简历至:zhouyao@hujiang.com

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息