WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
46 stars 10 forks source link

JavaScript 函数式 #122

Open WangShuXian6 opened 3 years ago

WangShuXian6 commented 3 years ago

JavaScript 函数式编程(FP)

函数式编程有许多不同的定义。

lisp程序员的定义与haskell的定义大不相同。 OCaml(关于OCaml,最早称为Objective Caml,是Caml编程语言的主要实现,开发工具包含交互式顶层解释器,字节码编译器以及最优本地代码编译器)的函数编程定义与erlang(Erlang是一种通用的面向并发的编程语言)中的范例几乎没有相似之处。甚至可以在javascript中找到几个相互竞争的定义。

然而,有一种联系——一些模糊的“当我看到它时就知道”的定义,很像说瞎话(事实上,有些人确实发现函数式编程在敷衍!)

在某些圈子里,最终的结果可能不被认为是惯用的,但这里获得的知识直接适用于任何形式的函数式编程。

函数式编程的核心是在代码中使用众所周知的、可理解的,也已被证明可以避免使代码更难理解的错误的模式。

将这些原则应用到代码的更多部分越多,代码就会越好

WangShuXian6 commented 3 years ago

为什么要函数式编程?

代码不只是计算机的一组指令

代码的更重要的作用是作为与其他人交流的一种手段。

花在“编码”上的大量时间实际上是花在阅读现有代码上。 很少有人享有这样的特权:把全部或大部分时间都花在简单地敲出所有新代码上,从不处理别人(或我们过去的自己)写的代码上。

据广泛估计,开发人员将70%的代码维护时间花在阅读上以理解它。 程序员每天编写的代码行数的平均值大约是10行。我们每天花7个小时来阅读代码,去理解这10行怎么运行!

例如,一旦您学习了“map(…)”的功能,当您在任何程序中看到它时,您几乎可以立即发现并理解它。

但是每次你看到一个“for”循环,你就必须阅读整个循环才能理解它。

“for”循环的语法可能是熟悉的,但实际上它所做的并不是;你每次都必须读才能理解。

通过拥有看一眼就能识别的代码的能力,从而减少时间去了解代码在做什么,我们的注意力被释放出来,去思考更高层次的程序逻辑;那些都是最需要我们关注的重要内容。

命令式代码描述我们大多数人已经自然编写的代码;它专注于精确指导计算机“如何”做某事。

声明式代码遵循函数式编程原则是更专注于描述结果输出的代码。

函数式编程是一种非常不同的方式来考虑代码应该如何构造,从而使数据流更加明显

传统方式

命令式的,几乎完全集中于“如何”完成任务;它充斥着“if”语句、“for”循环、临时变量、重新分配、值突变、带有副作用的函数调用以及函数之间的隐式数据流。 当然,你“可以”通过它的逻辑来查看数字是如何流动和更改到最终状态的,但它一点也不清楚或直接。


var numbers = [4,10,0,27,42,17,15,-6,58];
var faves = [];
var magicNumber = 0;

pickFavoriteNumbers(); calculateMagicNumber(); outputMsg(); // The magic number is: 42

// ***

function calculateMagicNumber() { for (let fave of faves) { magicNumber = magicNumber + fave; } }

function pickFavoriteNumbers() { for (let num of numbers) { if (num >= 10 && num <= 20) { faves.push( num ); } } }

function outputMsg() { var msg = The magic number is: ${magicNumber}; console.log( msg ); }


## 函数式
>更具声明性一些;它消除了前面提到的大多数命令式技术。
>注意没有显式的条件、循环、副作用、重新分配或突变;
>相反,它使用我们所说的函数式编程和可信的模式,如过滤、还原、转换和组合。
>注重从低级别的“如何”转移到高级的“结果”。

>我们没有使用“if”语句来测试一个数字,而是将其委托给一个函数式编程里的实用程序,如“gte(..)”(大于或等于)去操作,然后将重点放在更重要的任务上,即将该过滤器与另一个过滤器和求和函数组合起来,得到我们想要的结果。

>数据流是明确的:
```js
var sumOnlyFavorites = FP.compose( [
    FP.filterReducer( FP.gte( 10 ) ),
    FP.filterReducer( FP.lte( 20 ) )
] )( sum );

var printMagicNumber = FP.pipe( [
    FP.reduce( sumOnlyFavorites, 0 ),
    constructMsg,
    console.log
] );

var numbers = [4,10,0,27,42,17,15,-6,58];

printMagicNumber( numbers );        // The magic number is: 42

// ***************

function sum(x,y) { return x + y; }
function constructMsg(v) { return `The magic number is: ${v}`; }

首先创建一个函数 sumOnlyFavorites(..) 这是其他三个函数的组合。 结合了两个过滤器, 一个检查值是否大于或等于10,一个检查值是否小于或等于20. 然后使用 sum(..) 减少数据传输. 结果函数 sumOnlyFavorites(..) 作为缩减作用,用于检查一个值是否通过两个过滤器,如果通过,则将该值添加到累加器值中。

然后使用定义好的函数 sumOnlyFavorites(..) 它可以首先减少一个数字列表, 然后使用另一个函数 printMagicNumber(..) 打印产生通过“sumOnlyFavorites”计算出数字的总和. 函数 printMagicNumber(..) 把最后的总数再输送到 constructMsg(..), 进入 console.log(..)打印创建一个字符串值结果.

WangShuXian6 commented 3 years ago

函数的本质

函数式编程不仅仅是用“function”关键字进行定义来编程。

从表面上看:函数是可以执行一次或多次的代码集合。

从代数中观察一些关于函数和图的基本知识 f(x) y=f(x)

一个方程定义: f(x) = 2x2 + 3 image 对于x的任何值,比如2,如果你把它插入方程,你得到11。11是什么?它是f(x)函数的返回值,前面我们说它代表y值。

可以选择将输入和输出值解释为图中曲线上(2,11)处的点。 对于插入的每一个'x'值,我们得到另一个'y'值,作为一个点的坐标与它配对。 另一个是(0,3),另一个是(-1,5)。 把所有这些点放在一起,就得到了抛物线图

在数学中,函数总是有输入并有输出。 在函数式编程中经常听到的一个术语是’态射‘(morphism); 两个数学结构之间保持结构的一种过程抽象方法,例如与该函数的输出对应另一函数的输入。

在代数数学中,这些输入和输出通常被解释为要绘制图形的坐标的组成部分。 然而,在我们的程序中,虽然很少被解释为图形上的可视绘制曲线,但是我们可以定义具有各种输入和输出的函数。

本质上,函数式编程就是在数学意义上接受使用函数方法作为特定程序。

进行函数编程,应该尽可能多地使用函数,并尽可能避免使用过程。 所有的“函数”都应该接受输入并返回输出。

函数的输入

函数必须有输入

参数是您传入的值,因素是接收传入值的函数内的命名变量。

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

var a = 3;

foo( a, a * 2 );

`“a”和“a2”是函数“foo(…)”调用的参数, “x”和“y”是接收参数值的参数(分别为“3”和“6”(“a2”的结果))。

默认参数

从ES6开始,参数可以声明默认值。 如果没有传递该参数的参数,或者传递了值“undefined”,则将替换默认的赋值表达式。


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

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

>考虑有助于函数可用性的默认情况是一个很好的实践。
>然而,在读取和理解函数如何被调用的变化方面,默认参数可能会导致更复杂的问题。
>在多大程度上依赖此功能方面要谨慎。

### 计数输入
>一个参数的函数也称为“一元”函数,二个参数的函数也称为“二元”函数,n个参数或更高的函数称为“n元”函数。

>可以通过函数引用的“length”属性来确定其参数个数
```js
function foo(x,y,z) {
    // ..
}

foo.length;             // 3

需要注意,某些类型的参数列表可能会使函数的“length”属性与您可能期望的不同:


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

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

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

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


>可以检查“arguments”的“length”属性,以确定实际传递了多少个:
```js
function foo(x,y,z) {
    console.log( arguments.length );
}

foo( 3, 4 );    // 2

从ES5(特别是严格模式)开始,“arguments”被一些人认为是不赞成使用的;许多人会避免使用它。

但是,建议“arguments.length”,仅用于可以需要传递的参数数量的情况

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

现在,“args”将是参数的完整数组,不管它们是什么,您可以使用“args.length”来确切知道传入了多少个参数。

参数数组

将一个数组作为参数传递给函数调用

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

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

foo( ...arr );                      // 4

多个值和…扩展参数可以交错

var arr = [ 2 ];

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

都会使处理参数数组更加容易。“slice(…)”、“concat(…)”和“apply(…)”的日子已经变得没有用了,这些方法都需要数组参数值。

参数的解构

ES6 解构是一种为您希望看到的结构类型(对象、数组等)声明模式的方法,以及如何处理其各个部分的分解(分配)

数组参数解构

给传入数组中前两个值中的每一个都提供一个参数名 [ .. ]参数列表的括号,这称为数组参数解构。 在这个例子中,解构函数告诉引擎在这个分配位置需要一个数组。 该模式表示取数组的第一个值并将其赋给名为“x”的局部参数变量,将第二个值赋给“y”,剩下的值将赋给“args”。


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

foo( [1,2,3] );


### 声明风格的重要性
>思考下我们刚才被解构的“foo(…)”,我们可以手动处理参数:
```js
function foo(params) {
    var x = params[0];
    var y = params[1];
    var args = params.slice( 2 );

    // ..
}

声明性代码比命令式代码更有效地通信。

声明性代码(例如,前一个“foo(…)”代码段中的解构函数,或“…”运算符用法)将重点放在代码的结果上。

命令式代码(如后一段中的手动处理的函数)更关注如何获得结果。如果你以后读到这样的命令式代码,你必须关注执行所有的代码,以理解期望的结果。变得在那里被“编码”,很容易被细节所模糊。

前面的“foo(…)”被认为更具可读性,因为解构的方法隐藏了不必要的参数输入的细节;读者可以自由地只关注处理这些参数。这显然是最重要的关注点,所以读者应该集中精力来最全面地理解代码。

无论我们使用的语言和库/框架的允许程度怎样,只要可能,我们都应该努力实现声明性、自解释的代码。

命名参数

正如我们可以解构数组参数一样,我们也可以解构对象参数:


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

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

>将一个对象作为单个参数传入,它被分解为两个单独的参数变量“x”和“y”,它们从传入的对象中分配相应属性名的值。
>“x”属性不在对象上并不重要;它只是像您所期望的那样,以一个带有“undefined”的变量结束。
>使用对象析构函数传递潜在多个参数的方法对函数式编程的好处是,只接受一个参数的函数更容易与另一个函数的单个输出组合。

#### 无序参数
>命名参数由于被指定为对象属性,所以没有从根本上进行排序。这意味着我们可以按照我们想要的任何顺序输入:

>只是简单地省略了'x'参数。当然我们也可以指定一个'x'参数,可以放在'y'的后面

>从可读性的角度来看,命名参数更灵活,更具吸引力,尤其是当相关函数可以接受三个、四个或更多输入时。

### 函数的输出
>在JavaScript中,函数总是返回一个值。
>这三个函数都具有相同的“返回”行为:
>如果没有“return”,或者只有空的“return;”,则“undefined”值是隐式的“return”。
```js
function foo() {}

function bar() {
    return;
}

function baz() {
    return undefined;
}

它们应该显式地“返回”一个值,而不是返回隐式的返回“undefined”。

“return”语句只能返回单个值。 因此,如果函数需要返回多个值,唯一可行的选择是将它们收集到数组或对象这样的复合值中:

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

然后,我们从“foo()”返回的两个对应项分配给“x”和“y”:

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

将多个值收集到数组(或对象)中以返回,然后将这些值解构回不同的赋值,是透明地表示函数的多个输出的一种方法。

提前返回

“return”语句不仅返回函数的值。 它也是一个流控制结构;它在该点结束了函数的执行。 因此,具有多个“return”语句的函数具有多个可能的退出点,这意味着如果有多条路径可以生成该输出,则可能难以读取函数以了解其输出行为。

函数的功能

函数可以接收和返回任何类型的值

高阶函数

接收或返回一个或多个其他函数值的函数


function forEach(list,fn) {
for (let v of list) {
fn( v );
}
}

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


#### 作用域的保持
>一个函数在另一个函数的作用域中时的行为。

##### 闭包
>当内部函数引用外部函数中的变量时,这称为闭包。

>闭包是指当一个函数从它自己的作用域之外记住和访问变量时,即使这个函数是在另一个作用域中执行的。

## 语法

### 命名
>函数有一个“name”属性,它保存函数语法上给定的名称的字符串值

>名称标签有助于代码更易于阅读。

>堆栈跟踪调试、可靠的自引用和可读性

#### 立即调用的函数表达式(IIFES)
```js
(function IIFE(){

    // 你已经知道我是立即调用函数

})();

不用function定义的函数

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

// vs.

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

建议支持' => '的论据是,通过使用更轻量级的语法,我们减少了函数之间的视觉边界,这使得我们可以使用简单的函数表达式

不要使用“this”感知函数

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