JavaScript的函数式编程基础指南


Posted in Javascript onMarch 19, 2016

引言

JavaScript是一种强大的,却被误解的编程语言。一些人喜欢说它是一个面向对象的编程语言,或者它是一个函数式编程语言。另外一些人喜欢说,它不是一个面向对象的编程语言,或者它不是一个函数式编程语言。还有人认为它兼具面向对象语言和函数式语言的特点,或者,认为它既不是面向对象的也不是函数式的,好吧,让我们先搁置那些争论。

让我们假设我们共有这样的一个使命:在JavaScript语言所允许的范围内,尽可能多的使用函数式编程的原则来编写程序。

首先,我们需要清理下脑子里那些关于函数式编程的错误观念。

在JS界被(重度)误解的函数式编程

显然有相当一批开发者一天到晚的以函数式范式的方式使用JavaScript。我还是要说有更大量的JavaScript开发者,并不真正理解那佯做的真正意义。

我确信,导致这种局面是因为很多用于服务端的web开发语言都源自C语言,而C语言,很显然不是一种函数式编程语言。

似乎有两个层级的混乱,第一个层级的混乱我们用下面这个在jQuery中经常会用到的例子来说明:

$(".signup").click(function(event){
  $("#signupModal").show();
  event.preventDefault();
});

嘿,仔细看。我传递一个匿名函数作为参数,这在JavaScript世界里被称作众所周知“CallBack”(回调)函数。

真有人会认为这就是函数式编程吗?根本不是!

这个例子展示了一个函数式语言的关键特性:函数作为参数。另一方面,这个3行代码的例子也违背了几乎所有其他的函数式编程范式。

第二个层级的混乱有点微妙。读到这里,一些追求潮流的JS开发者在暗自思考。

好吧,废话!但是我已经知道了所有关于函数式编程的知识与技能。我在我所有的项目上使用Underscore.js。

Underscore.js 是一个广受欢迎的JavaScript库,到处都在使用。举个例子,我有一组单词,我需要获得一个集合,集合里的每个元素是各个单词的头两个字母。用Underscore.js实现这个相当简单:

var firstTwoLetters = function(words){
  return _.map(words,function(word){
    return _.first(word,2);
  });
};

看!看JavaScript巫术。我正在使用这些高级的函数式应用函数,像_.map 和 _.first。你还有什么要说的,利兰(译注:作者Leland)?

尽管underscore 和 像_.map这样的函数是非常有价值的函数式范式,但是像这个例子中所采用的组织代码的方法看起来…冗长而且对于我来说太难于理解。我们真的需要这样做吗?

如果开始思考的时候多一点“函数式”的思维,可能我们能够把上面的例子改成这样:

// ...一点魔法
var firstTwoLetters = map(first(2));

仔细想想,在1行代码中包含了和上面5行代码同样的信息。words 和word 仅仅是参数/占位符。这个方法的核心是用一种更明显的方式组合map函数,first函数,和常量2。

JavaScript是函数式编程语言吗?

没有神奇的公式能够判定一种语言是不是“函数式”语言。有些语言很明显就是函数式的,就像另外一些语言很明显不是函数式的,但是有大量语言的是模棱两可的中间派。

于是这里给出一些常用的、重要的函数式语言的“配料”(JavaScript能实现用粗体标志)

  • 函数是“第一等公民”
  • 函数能够返回函数
  • 词法上支持闭包
  • 函数要“纯粹”
  • 可靠递归
  • 没有变异状态

这决不是一个排它的列表,但是我们至少要逐个讨论Javascript中最重要的三个特性,它们支撑我们可以用函数式的方式来编写程序。

让我们逐个详细的了解下:

函数是“第一等公民”

这条可能是在所有的配料中最明显的,并且可能是在很多现代编程语言中最常见到的。

在JavaScript局部变量是通过var关键字来定义的。

var foo = "bar";

JavaScript中把函数以局部变量的方式定义是非常容易做到的。

var add = function (a, b) { return a + b; };
var even = function (a) { return a % 2 === 0; };

这些都是事实,变量:变量add和变量even通过被赋值的方式,与函数定义建立引用关系,这种引用关系是在任何时候如果需要是可以被改变的。

// capture the old version of the function
var old_even = even; 
 
// assign variable `even` to a new, different function
even = function (a) { return a & 1 === 0; };

当然,这没有什么特别的。但是成为“第一等公民”这个重要的特性使得我们能够把函数以参数的方式传递给另一个函数。举个例子:

var binaryCall = function (f, a, b) { return f(a, b); };

这是一个函数,他接受了一个二元函数f,和两个参数a,b,然后调用这个二元函数f,该二元函数f以a、b为输入参数。

add(1,2) === binaryCall(add, 1, 2); 
// true

这样做看起来有点笨拙,但是当把接下来的函数式编程“配料”合并考虑的时候,牛叉之处就显而易见了…

函数能返回函数(换个说法“高阶函数”)

事情开始变的酷起来。尽管开始比较简单。函数最终以新的函数作为返回值。举个例子:

var applyFirst = function (f, a) { 
 return function (b) { return f(a, b); };
};

这个函数(applyFirst)接受一个二元函数作为其中一个参数,可以把第一个参数(即二元函数)看作是这个applyFirst函数的“部分操作”,然后返回一个一元(一个参数)函数,该一元函数被调用的时候返回外部函数的第一个参数(f)的二元函数f(a, b)。返回两个参数的二元函数。

让我们再谈谈一些函数,例如mult(乘法)函数:

var mult = function(a, b) { return a * b; };

依循mult(乘法)函数的逻辑,我们可以写一个新的函数double(乘方):

var double = applyFirst(mult, 2);
 
double(32); 
// 64
double(7.5); 
// 15

这就是偏函数,在FP中经常会用到。(译注:FP全名为 Functional Programming 函数式程序设计 )

我们当然可以像applyFirst那样定义函数:

var curry2 = function (f) {
 return function (a) {
  return function (b) {
   return f(a, b);
  };
 };
};

现在,我想要一个double(乘方)函数,我们换种方式做:

var double = curry2(mult)(2);

这种方式被称作“函数柯里化”。有点类似partial application(偏函数应用),但是更强大一点。

准确的说,函数式编程之所以强大,大部分因于此。简单和易理解的函数成为我们构筑软件的基础构件。当拥有高水平的组织能力、很少重用的逻辑的时候,函数能够被组合和混合在一起用来表达出更复杂的行为。

高阶函数可以得到的乐趣更多。让我们看两个例子:

1.翻转二元函数参数顺序

// flip the argument order of a function
var flip = function (f) {
 return function (a, b) { return f(b, a); };
};
 
divide(10, 5) === flip(divide)(5, 10); 
// true

2.创建一个组合了其他函数的函数

// return a function that's the composition of two functions...
// compose (f, g)(x) -> f(g(x))
var compose = function (f1, f2) {
 return function (x) {
  return f1(f2(x));
 };
};
 
// abs(x) = Sqrt(x^2)
var abs = compose(sqrt, square);
 
abs(-2); 
// 2

这个例子创建了一个实用的函数,我们可以使用它来记录下每次函数调用。

var logWrapper = function (f) {
 return function (a) {
  console.log('calling "' + f.name + '" with argument "' + a);
  return f(a);
 };
};

var app_init = function(config) { 
/* ... */
 };
 
if(DEBUG) {
 
// log the init function if in debug mode
 app_init = logWrapper(app_init);
}
 
// logs to the console if in debug mode
app_init({
 
/* ... */
});

词法闭包+作用域

我深信理解如何有效利用闭包和作用域是成为一个伟大JavaScript开发者的关键。
那么…什么是闭包?

简单的说,闭包就是内部函数一直拥有父函数作用域的访问权限,即使父函数已经返回。<译注4>
可能需要个例子。

var createCounter = function () {
 var count = 0;
 return function () {
  return ++count;
 };
};
 
var counter1 = createCounter();
 
counter1(); 
// 1
counter1(); 
// 2
 
var counter2 = createCounter();
 
counter2(); 
// 1
counter1(); 
// 3

一旦createCounter函数被调用,变量count就被分配一个新的内存区域。然后,返回一个函数,这个函数持有对变量count的引用,并且每次调用的时候执行count加1操作。

注意从createCounter函数的作用域之外,我们是没有办法直接操作count的值。Counter1和Counter2函数可以操作各自的count变量的副本,但是只有在这种非

常具体的方式操作count(自增1)才是被支持的。

在JavaScript,作用域的边界检查只在函数被声明的时候。逐个函数,并且仅仅逐个函数,拥有它们各自的作用域表。(注:在ECMAScript 6中不再是这样,因为let的引入)

一些进一步的例子来证明这论点:

// global scope
var scope = "global";
 
var foo = function(){
 
// inner scope 1
 var scope = "inner";
 var myscope = function(){
  
// inner scope 2
  return scope;
 };
 return myscope;
};
 
console.log(foo()()); 
// "inner"
 
console.log(scope); 
// "global"

关于作用域还有一些重要的事情需要考虑。例如,我们需要创建一个函数,接受一个数字(0-9),返回该数字相应的英文名称。

简单点,有人会这样写:

// global scope...
var names = ['zero','one','two','three','four','five','six','seven','eight','nine'];
var digit_name1 = function(n){
 return names[n];
};

但是缺点是,names定义在了全局作用域,可能会意外的被修改,这样可能致使digit_name1函数所返回的结果不正确。
那么,这样写:

var digit_name2 = function(n){
 var names = ['zero','one','two','three','four','five','six','seven','eight','nine'];
 return names[n];
};

这次把names数组定义成函数digit_name2局部变量.这个函数远离了意外风险,但是带来了性能损失,由于每次digit_name2被调用的时候,都将重新为names数组定义和分配空间。换个例子如果names是个非常大的数组,或者可能digit_name2函数在一个循环中被调用多次,这时候性能影响将非常明显。

// "An inner function enjoys that context even after the parent functions have returned."
var digit_name3 = (function(){
 var names = ['zero','one','two','three','four','five','six','seven','eight','nine'];
 return function(n){
  return names[n];
 };
})();

这时候我们面临第三个选择。这里我们实现立即调用的函数表达式,仅仅实例化names变量一次,然后返回digit_name3函数,在 IIFE (Immediately-Invoked-Function-Expression 立即执行表达式)的闭包函数持有names变量的引用。
这个方案兼具前两个的优点,回避了缺点。搞定!这是一个常用的模式用来创建一个不可被外部环境修改“private”(私有)状态。

Javascript 相关文章推荐
Javascript 对象的解释
Nov 24 Javascript
基于jQuery的左右滚动实现代码
Dec 03 Javascript
javascript 在firebug调试时用console.log的方法
May 10 Javascript
javascript中scrollTop详解
Apr 13 Javascript
Bootstrap树形控件使用方法详解
Jan 27 Javascript
AngularJS中的API(接口)简单实现
Jul 28 Javascript
react.js 获取真实的DOM节点实例(必看)
Apr 17 Javascript
addEventListener()与removeEventListener()解析
Apr 20 Javascript
新版vue-cli模板下本地开发环境使用node服务器跨域的方法
Apr 03 Javascript
angularjs结合html5实现拖拽功能
Jun 25 Javascript
vue实现将数据存入vuex中以及从vuex中取出数据
Nov 08 Javascript
nuxt 服务器渲染动态设置 title和seo关键字的操作
Nov 05 Javascript
深入解析JavaScript中函数的Currying柯里化
Mar 19 #Javascript
Linux下为Node.js程序配置MySQL或Oracle数据库的方法
Mar 19 #Javascript
分享js粘帖屏幕截图到web页面插件screenshot-paste
Aug 21 #Javascript
JQuery用户名校验的具体实现
Mar 18 #Javascript
基于javascript实现页面加载loading效果
Sep 15 #Javascript
JQuery fileupload插件实现文件上传功能
Mar 18 #Javascript
javascript移动开发中touch触摸事件详解
Mar 18 #Javascript
You might like
用PHP实现小型站点广告管理(修正版)
2006/10/09 PHP
php 特殊字符处理函数
2008/09/05 PHP
比较discuz和ecshop的截取字符串函数php版
2012/09/03 PHP
深入解析PHP 5.3.x 的strtotime() 时区设定 警告信息修复
2013/08/05 PHP
PHP基于MySQL数据库实现对象持久层的方法
2015/06/17 PHP
利用PHP内置SERVER开启web服务(本地开发使用)
2020/01/22 PHP
基于jQuery的日期选择控件
2009/10/27 Javascript
jquery中的$(document).ready()与window.onload的区别
2009/11/18 Javascript
JavaScript中获取未知对象属性的代码
2011/04/27 Javascript
如何实现textarea里的不同文本显示不同颜色
2014/01/20 Javascript
js实现简单随机抽奖的方法
2015/01/27 Javascript
jQuery插件kinMaxShow扩展效果用法实例
2015/05/04 Javascript
JavaScript更改字符串的大小写
2015/05/07 Javascript
jQuery实现的简单百分比进度条效果示例
2016/08/01 Javascript
巧用数组制作图片切换js代码
2016/11/29 Javascript
jstree的简单实例
2016/12/01 Javascript
js中开关变量使用实例
2017/02/24 Javascript
es6数组includes()用法实例分析
2020/04/18 Javascript
[46:14]VGJ.T vs Liquid 2018国际邀请赛小组赛BO2 第一场 8.19
2018/08/21 DOTA
pytorch 输出中间层特征的实例
2019/08/17 Python
python3排序的实例方法
2020/10/20 Python
python爬取代理ip的示例
2020/12/18 Python
Css3实现无缝滚动防抖
2020/09/14 HTML / CSS
英国手机零售商:Carphone Warehouse
2018/06/06 全球购物
英国顶级珠宝品牌之家:John Greed
2018/06/09 全球购物
会计师事务所审计实习自我鉴定
2013/09/20 职场文书
实习生的自我鉴定范文欣赏
2013/11/20 职场文书
单身联谊活动方案
2014/01/29 职场文书
高考寄语大全
2014/04/08 职场文书
毕业论文指导教师评语
2014/12/30 职场文书
经典搞笑版检讨书
2015/02/19 职场文书
男方家长婚礼答谢词
2015/09/29 职场文书
浅谈Nginx 中的两种限流方式
2021/03/31 Servers
python删除csv文件的行列
2021/04/06 Python
试了下Golang实现try catch的方法
2021/07/01 Golang
Redis特殊数据类型HyperLogLog基数统计算法讲解
2022/06/01 Redis