javascript与函数式编程

chenxj      2022-02-08     365

关键词:

原文:https://bethallchurch.github.io/JavaScript-and-Functional-Programming/

译文:http://www.zcfy.cc/article/1013

译者注:推荐一篇译文,《函数式编程术语解析》。

本文是我在 2016 年 7 月 29 号听 Kyle Simpson 精彩的课程《Functional-Light JavaScript》时所做的笔记(外加个人的深入研究)(幻灯片在这)。

  长久以来,面向对象在 JavaScript 编程范式中占据着主导地位。不过,最近人们对函数式编程的兴趣正在增长。函数式编程是一种编程风格,它强调将程序状态变化(即副作用[side effect])的次数减到最小。因此,函数式编程鼓励使用不可变数据(immutable data)和纯函数(pure functions)(“纯”意味着没有副作用的)。它也更倾向于使用声明式的风格,鼓励使用命名良好的函数,这样就能使用在我们视线之外的那些打包好的细节实现,通过描述希望发生什么以进行编码。

  尽管面向对象编程与函数式编程之间有些矛盾,它们却并非互斥的关系。JavaScript 所拥有的工具,能支持这两种方式。甚至可以说,就算不把它孤立地当作函数式语言使用,还是有不少来自函数式方法的概念和最佳实践可以帮助我们,让代码更干净,可读性更强,推理起来更简单。

副作用最小化

  所谓副作用,指的是函数内部产生了超出函数之外的变化。函数可能会做一些事,如操作 DOM、修改更高层作用域中的变量值,或者将数据写入数据库。这些带来的就是副作用。

// 有副作用的函数:修改更高层作用域中的变量值
var x = 10;

const myFunc = function ( y ) {
  x = x + y;
};

myFunc( 3 );
console.log( x ); // 13

myFunc( 3 );
console.log( x ); // 16

  副作用并非天生邪恶。不产生任何副作用的程序也无法影响世界,因此也没有任何意义(除非是作为理论兴趣进行研究)。不过,副作用确实是危险的,应当尽量避免使用,除非绝对必要。

  当函数产生副作用的时候,仅凭借输入输出的内容,不足以明确函数究竟做了什么工作。必须了解上下文环境、程序状态的历史,这让函数更难理解。在不可预测的交互方式下,副作用可能带来一些 bug,且函数因上述依赖,测试起来也更困难。

  副作用最小化是函数式编程中最基础的原则,接下来的多数小节都可以当作是避免副作用的一些办法概要。

视数据为不可变动的(Immutable)

  变动(mutation)指的是值在原位置上的变化(an in-place change to a value)。不可变值意味着,一旦创建出来,永远都不会变化。在 JavaScript 中,简单值如数字、字符串、布尔值这些是不可变的。不过,像对象、数组这样的数据结构都是可变的。

// push 方法改变了数组
const x = [1, 2];
console.log( x ); // [1, 2]

x.push( 3 );
console.log( x ); // [1, 2, 3]

为什么要避免变动数据呢?

  变动是一种副作用。程序中变化的东西越少,需要跟踪记录的也就越少,程序也就越简单。

  JavaScript 中维持对象、数组等数据结构不可变性的可用工具很有限。通过 Object.freeze 可以强制实现对象的不可变,但作用深度只有一层:

const frozenObject = Object.freeze( { valueOne : 1, valueTwo : { nestedValue : 1 } } );
frozenObject.valueOne = 2; // 不允许
frozenObject.valueTwo.nestedValue = 2; // 竟然允许了!

  不过,还是有一些很棒的工具库解决了这些问题,其中最著名的要数 Immutable 了。

  对多数应用来说,使用工具库来保证不可变性有些矫枉过正。很多情况下,简单地将数据当作是不可变的,就能让我们受益良多。

避免变动:数组

  JavaScript 数组方法可以被概括为变动方法 (mutator methods) 和非变动方法。应当尽可能避免变动方法。

  举例来说,concat 方法可以用来替代 push 方法。push 改变了原数组;concat 返回由原数组和作为参数的数组组成的新数组,而原来的数组还是完整的。

// push 改变了数组
const arrayOne = [1, 2, 3];
arrayOne.push( 4 );

console.log( arrayOne ); // [1, 2, 3, 4]

// concat 生成了新数组,原数组保持不变
const arrayTwo = [1, 2, 3];
const arrayThree = arrayTwo.concat([ 4 ]);

console.log( arrayTwo ); // [1, 2, 3]
console.log( arrayThree ); // [1, 2, 3, 4]

  还有一些非变动方法,包括 map、filter、reduce 等。

避免变动:对象

  可以使用 Object.assign 方法,而非直接编辑对象。该方法将源对象的属性复制到目标对象中,并将目标对象返回。如果总是用一个空对象作为目标对象,就能通过 Object.assign 避免直接编辑对象。

const objectOne = { valueOne : 1 };
const objectTwo = { valueTwo : 2 };

const objectThree = Object.assign( {}, objectOne, objectTwo );

console.log( objectThree ); // { valueOne : 1, valueTwo : 2 }

关于 const

const 很有用,却不会使数据不可变。它只能防止变量被重新赋值。这不能混为一谈。

const x = 1;
x = 2; // 不允许

const myArray = [1, 2, 3];
myArray = [0, 2, 3]; // 不允许

myArray[0] = 0; // 允许了!

书写纯函数

  纯函数 不会改变程序的状态,也不会产生可感知的副作用。纯函数的输出,仅仅取决于输入值。无论何时何地被调用,只要输入值相同,返回值也就一样。

  纯函数是最小化副作用的重要工具。另外,与上下文无关的特点,也让它们有了高可测试性和可复用性。

  前面讲副作用的小节中的代码里, myFunc 函数就是非纯函数,注意两次调用时输入相同但每次返回结果却不同。不过,它也能改写成纯函数:

// 将全局变量变为局部变量
const myFunc = function ( y ) {
  const x = 10;
  return x + y;
}

console.log(myFunc( 3 )); // 13
console.log(myFunc( 3 )); // 13
// 将 x 作为参数传递
const x = 10;

const myFunc = function ( x, y ) {
  return x + y;
}

console.log(myFunc( x, 3 )); // 13
console.log(myFunc( x, 3 )); // 13

你的程序最终肯定还是会产生一些副作用。当副作用产生的时候,小心应对,尽可能地约束、限制它们的影响。

书写返回函数的函数(Function-Generating Functions)

找一些程经验的人,让他们猜猜下面的代码做了什么:

例1

const numbers = [1, 2, 3];

for ( let i = 0; i < numbers.length; i++ ) {
  console.log( numbers[i] );
}

例2

const numbers = [1, 2, 3];

const print = function ( input ) {
  console.log( input );
};

numbers.forEach( print );

  我测试过的所有人在例 2 上运气更好。例 1 展示的是命令式方法,将一列数字打印出来。例 2 展示的是声明式方法。将循环遍历数组、在控制台打印数字这些细节各种包装成 forEach 和 print 函数,无需知道如何做,就可以表达我们需要程序做什么。这让代码可读性更高。例 2 的最后一行看起来,很接近英语句子

  采用这种方法,涉及到编写大量函数。利用现有函数编写生成新函数的函数,可以让这个过程中的重复(DRY-er)更少。

  特别地,JavaScript 的两个特性让这种形式的函数生成变得可能。第一个是闭包。函数能够访问包含作用域中的变量,就算该作用域已不复存在,这就是闭包。第二个特性是,JavaScript 将函数当作值来对待。这使书写高阶函数成为可能,高阶函数可以接收函数作为参数,并/或返回函数。

  这些特性组合在一起,我们就可以编写返回函数的函数了。返回的函数能“记住”传给生成函数的参数,并在程序的其他地方使用这些参数。

函数组合

  通过函数组合,可能将函数组合在一起形成新的函数。一起来看例子:

// 通过 add 和 square 函数组合生成 addThenSquare
const add = function ( x, y ) {
  return x + y;
};

const square = function ( x ) {
  return x * x;
};

const addThenSquare = function ( x, y ) {
  return square(add( x, y ));
};

  你可能会发现一直在重复这种利用更小的函数生成一个更复杂的函数的形式。通常编写一个组合函数会更有效率:

const add = function ( x, y ) {
  return x + y;
};

const square = function ( x ) {
  return x * x;
};

const composeTwo = function ( f, g ) {
  return function ( x, y ) {
    return g( f ( x, y ) );
  };
};

const addThenSquare = composeTwo( add, square );

  还可以走得更远,编写一个更一般化的组合函数:

// 这个版本的 composeTwo 的初始化函数可以接收任意数量的参数
const composeTwo = function ( f, g ) {
  return function ( ...args ) {
    return g( f( ...args ) );
  };
};

// composeMany 可以接收任意数量的函数
// 其初始化函数能接收任意数量的参数
const composeMany = function ( ...args ) {
  const funcs = args;
  return function ( ...args ) {
    funcs.forEach(( func ) => {
      args = [func.apply( this, args )]; 
    });
    return args[0];
  };
};

  组合函数的最终形式取决于你所需的通用性水平,以及偏好的 API 类型。

偏函数(Partial Application)

  偏函数 指定一个或多个参数,然后返回另一个函数,该函数稍后会被完整调用。

  在下面的例子中,double、triple 和 quadruple 都是 multiply 函数的偏函数。

const multiply = function ( x, y ) {
  return x * y;
};

const partApply = function ( fn, x ) {
  return function ( y ) {
    fn( x, y );
  };
};

const double = partApply( multiply, 2 );
const triple = partApply( multiply, 3 );
const quadruple = partApply( multiply, 4 );

柯里化

  柯里化是将接收多个参数的函数转换为一系列只接收一个参数的函数的过程。

const multiply = function ( x, y ) {
  return x * y;
};

const curry = function ( fn ) {
  return function ( x ) {
    return function ( y ) {
      return fn( x, y );
    };
  };
};

const curriedMultiply = curry( multiply );

const double = curriedMultiply( 2 );
const triple = curriedMultiply( 3 );
const quadruple = curriedMultiply( 4 );

console.log(triple( 6 )); // 18

  柯里化和偏函数在概念上很相似(可能不会两个都需要使用),但仍然有所不同。主要区别在于,柯里化总是生成函数套链,每次只接收一个参数,而偏函数返回的函数可以一次接收多个参数。 比较它们作用于最少接收三个参数的函数时,这种差别就更明晰了:

const multiply = function ( x, y, z ) {
  return x * y * z;
};

const curry = function ( fn ) {
  return function ( x ) {
    return function ( y ) {
      return function ( z ) {
        return fn( x, y, z );
      };
    };
  };
};

const partApply = function ( fn, x ) {
  return function ( y, z ) {
    return fn( x, y, z );
  };
};

const curriedMultiply = curry( multiply );
const partiallyAppliedMultiply = partApply( multiply, 10 );

console.log(curriedMultiply( 10 )( 5 )( 2 )); // 100
console.log(partiallyAppliedMultiply( 5, 2 )); // 100

递归

  递归函数是这样一种函数,它会一直调用自身,直至满足基本条件。递归函数是高度声明式的。它们也很优雅,写起来很爽!

  下面是计算递归计算阶乘的例子:

const factorial = function ( n ) {
  if ( n === 0 ) {
    return 1;
  }
  return n * factorial( n - 1 );
};

console.log(factorial( 10 )); // 3628800

  在 JavaScript 中使用递归函数需要细心一些。每次函数调用都会向调用栈(call stack)中加入新的调用帧(call frame),当函数返回的时候,该调用帧从调用栈中弹出。递归函数调用在返回之前调用自身,所以很容易就会超出调用栈的限制,导致程序崩溃。

  不过,这可以通过尾调用优化来避免。

尾调用优化

  尾调用指的是,某个函数的最后一步动作是调用函数。尾调用优化指的是,当语言编译器识别到尾调用的时候,会对其复用相同的调用帧。这意味着,在编写尾调用的递归函数时,调用帧的限制永远不会被超出,因为调用帧会被反复使用。

  下面是将前面的递归函数采用尾递归优化重写之后的例子:

const factorial = function ( n, base ) {
  if ( n === 0 ) {
    return base;
  }
  base *= n;
  return factorial( n - 1, base );
};

console.log(factorial( 10, 1 )); // 3628800

  ES2015 语言规范中已包含了适当的尾部调用的支持,但目前在大部分环境中尚未得到支持。可以在这里查看你能否使用。

小结

  函数式编程容纳了许多思想,借助它们可以优化代码。纯函数和不可变数据将副作用的危害最小化,声明式编程让代码可读性大大提高。在与复杂性的斗争中,这些是我们应当拥抱的重要工具。

 

javascript-underscore与函数式编程

《Javascript函数式编程 PDF》#csdn下载地址http://download.csdn.net/detail/tssxm/9713727 Underscore #githubhttps://github.com/jashkenas/underscore#中文官方网站http://www.css88.com/doc/underscore/#CDN<s 查看详情

javascript函数式编程

第1章JavaScript函数式编程简介11.1JavaScript案例11.2开始函数式编程41.2.1为什么函数式编程很重要41.2.2以函数为抽象单元71.2.3封装和隐藏91.2.4以函数为行为单位101.2.5数据抽象141.2.6函数式JavaScript初试171.2.7加速191.3Underscore示例221.4总结2... 查看详情

玩转javascript面试:何为函数式编程?(代码片段)

函数式编程在JavaScript领域着实已经成为一个热门话题。就在几年前,很多JavaScript程序员甚至都不知道啥是函数式编程,但是就在近三年里我看到过的每一个大型应用的代码库中都包含了函数式编程思想的大规模使用。函数式编... 查看详情

javascript函数式编程

编程范式编程范式是一个由思考问题以及实现问题愿景的工具组成的框架。很多现代语言都是聚范式(或者说多重范式):他们支持很多不同的编程范式,比如面向对象,元程序设计,泛函,面向过程,等等。函数式编程范式函... 查看详情

《javascript函数式编程思想》——列表

第8章 列表函数式编程与列表处理有很深的渊源。列表是最基础,也是使用最普遍的复合数据类型。作为最早出现的函数式编程语言之一,Lisp【注:它的名称就来源于“列表处理器”(LIStProcessor)】用函数参... 查看详情

函数与函数式编程

函数与函数式编程 纵观JavaScript中所有必须需要掌握的重点知识中,函数是我们在初学的时候最容易忽视的一个知识点。在学习的过程中,可能会有很多人、很多文章告诉你面向对象很重要,原型很重要,可是却很少有人告... 查看详情

javascript中的函数式编程

本文和大家分享的主要是javascript中函数式编程相关内容,一起来看看吧,希望对大家学习javascript有所帮助。 函数式编程(functionalprogramming)或称函数程序设计,又称泛函编程,是一种编程范型,比起命令式编程,函数式编程... 查看详情

javascript系列:函数式编程(开篇)

...数以及函数柯里化等高级函数应用,同时,因为正在学习JavaScript·函数式编程,想整理一下函数式编程中,对于我们日常比较有用的部分。 为什么函数式编程很重要?   学习过C++,java这些面向对象编程语言,我... 查看详情

javascript函数式编程基础(代码片段)

javascript函数式编程基础函数调用引用做返回值/*javascript函数式编程基础*/functionsayHello()return"helloworld";letresult=sayHello();//函数调用letfn=sayHello;//函数引用console.log(fn());//helloworld//函数做返回值functionee( 查看详情

Javascript 是函数式编程语言吗?

】Javascript是函数式编程语言吗?【英文标题】:IsJavascriptaFunctionalProgrammingLanguage?【发布时间】:2011-04-2703:08:08【问题描述】:仅仅因为函数是一等对象,有闭包和更高阶的函数,Javascript是否应该被称为函数式编程语言?我认为... 查看详情

函数式编程

和Lisp、Haskell不同,javascript并非函数式编程语言,但在javascript中可以操控对象一样操控函数,也就是说可以在javascript中应用函数式编程技术。ES5中的数组方法(如map()和reduce())就可以非常适合用于函数式编程风格。本文将详细介... 查看详情

函数式编程-函数的合成与柯里化

...是g和f的合成函数g·f。下面就是代码实现了,我使用的是JavaScript语言。注意,本文所有示例代码都是简化过的,完整的D 查看详情

《javascript函数式编程思想》——从面向对象到函数式编程

...假如本书的写作时间倒退回十年前,书名可能会变成JavaScript面向对象编程思想。自上世纪90年代兴起的面向对象编程思想随Java的繁荣达于顶点,在JavaScript从一门只被用来编写零星的简单的表单验证代码的玩具语言变成日... 查看详情

javascript函数式编程(代码片段)

JavaScript函数式编程(一) JavaScript函数式编程(二)在第二篇文章里,我们介绍了 Maybe、Either、IO 等几种常见的Functor,或许很多看完第二篇文章的人都会有疑惑:『这些东西有什么卵用?』事实上,如果只是为了学... 查看详情

《javascript函数式编程思想》——副作用和不变性

第6章 副作用和不变性6.1 副作用6.2 纯函数6.2.1 外部变量6.2.2 实现6.2.3 函数内部的副作用6.2.4 闭包6.3 不变性6.3.1 哲学上的不变性与身份6.3.2 简单类型和复合类型6.3.3 值类型和引用类型6.3.4 可变类型和不可变类型6.3.5 可变... 查看详情

javascript函数式编程-包含闭包链式优化及柯里化

本文着重介绍个人理解的函数式编程。函数式编程个人理解为:以函数为主要载体的编程方式。好处:语义更加清晰可复用性高可维护性好作用域局限、副作用少基本函数式编程://实现数组中每个单词首字母大写//一般写法const... 查看详情

《javascript函数式编程思想》——类型系统

第2章 类型系统为什么在许多编程语言中整数和浮点数是两种类型?结构体、数组、列表、映射……这些类型有什么关系?用户自定义的各种类型与它们又有什么关系?函数也是类型吗?强类型和弱类型意味着什... 查看详情

《javascript函数式编程思想》

自序伴随着Web技术的普及,JavaScript已成为应用最广泛的编程语言之一。由于其在Web前端编程中的统治地位、语言本身的表现力、灵活性、开源的本质和ECMAScript标准近年来的快速发展,JavaScript向各个领域渗透的势头仍然... 查看详情