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语句可以不以;结尾的烦恼
Mar 08 Javascript
jQuery EasyUI API 中文文档 - ComboBox组合框
Oct 07 Javascript
给jqGrid数据行添加修改和删除操作链接(之一)
Nov 04 Javascript
jQuery模仿京东/天猫商品左侧分类导航菜单效果
Jun 29 Javascript
浅谈JS中的!=、== 、!==、===的用法和区别
Sep 24 Javascript
jquery 删除节点 添加节点 找兄弟节点的简单实现
Dec 07 Javascript
BootStrap 下拉菜单点击之后不会出现下拉菜单(下拉菜单不弹出)的解决方案
Dec 14 Javascript
微信小程序实现动态改变view标签宽度和高度的方法【附demo源码下载】
Dec 05 Javascript
如何用JavaScript实现功能齐全的单链表详解
Feb 11 Javascript
Vue实现点击按钮复制文本内容的例子
Nov 09 Javascript
uin-app+mockjs实现本地数据模拟
Aug 26 Javascript
openlayers实现地图弹窗
Sep 25 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
php5中date()得出的时间为什么不是当前时间的解决方法
2008/06/30 PHP
PHP提示Warning:phpinfo() has been disabled函数禁用的解决方法
2014/12/17 PHP
PHP统计当前在线用户数实例讲解
2015/10/21 PHP
PHP中phar包的使用教程
2017/06/14 PHP
php之header的不同用法总结(实例讲解)
2017/11/28 PHP
thinkPHP5框架设置404、403等http状态页面的方法
2018/06/05 PHP
JavaScript 继承详解 第一篇
2009/08/30 Javascript
js判断为空Null与字符串为空简写方法
2014/02/24 Javascript
JavaScript实现网页加载进度条代码超简单
2015/09/21 Javascript
使用ReactJS实现tab页切换、菜单栏切换、手风琴切换和进度条效果
2016/10/17 Javascript
深入理解Vue nextTick 机制
2018/04/28 Javascript
深入理解vue中的slot与slot-scope
2019/04/22 Javascript
vue router 用户登陆功能的实例代码
2019/04/24 Javascript
layui实现form表单同时提交数据和文件的代码
2019/10/25 Javascript
[05:08]DOTA2-DPC中国联赛3月6日Recap集锦
2021/03/11 DOTA
python和bash统计CPU利用率的方法
2015/07/10 Python
Python实现模拟分割大文件及多线程处理的方法
2017/10/10 Python
python实现指定文件夹下的指定文件移动到指定位置
2018/09/17 Python
对python中类的继承与方法重写介绍
2019/01/20 Python
Python实现的企业粉丝抽奖功能示例
2019/07/26 Python
Laravel框架表单验证格式化输出的方法
2019/09/25 Python
Python3 Tkinkter + SQLite实现登录和注册界面
2019/11/19 Python
HTML5打开本地app应用的方法
2016/03/31 HTML / CSS
HTML5触摸事件演化tap事件介绍
2016/03/25 HTML / CSS
HTML5超炫酷粒子效果的进度条的实现示例
2019/08/23 HTML / CSS
adidas澳大利亚官方网站:adidas Australia
2018/04/15 全球购物
英国一家集合了众多有才华设计师品牌的奢侈店:Wolf & Badger
2018/04/18 全球购物
VLAN和VPN有什么区别?分别实现在OSI的第几层?
2014/12/23 面试题
公司活动策划方案
2014/01/13 职场文书
给校长的建议书600字
2014/05/15 职场文书
师德模范事迹材料
2014/06/03 职场文书
2014年党建工作汇报材料
2014/11/02 职场文书
委托培训协议书
2014/11/17 职场文书
应届毕业生求职信范文
2015/03/19 职场文书
休假证明书
2015/06/24 职场文书
北京大学中文系教授推荐的10本小说
2019/08/08 职场文书