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 相关文章推荐
jquery Moblie入门—hello world的示例代码学习
Jan 08 Javascript
JQUERY表单暂存功能插件分享
Feb 23 Javascript
JavaScript作用域示例详解
Jul 07 Javascript
jQuery获取选中单选按钮radio的值
Dec 27 Javascript
使用JavaScript触发过渡效果的方法
Jan 19 Javascript
JS实现加载和读取XML文件的方法详解
Apr 24 Javascript
Vue.js通用应用框架-Nuxt.js的上手教程
Dec 25 Javascript
浅谈AngularJS中$http服务的简单用法
May 15 Javascript
vue.js实现带日期星期的数字时钟功能示例
Aug 28 Javascript
p5.js临摹动态图形实现方法详解
Oct 23 Javascript
微信小程序按顺序同步执行的两种方式
Dec 20 Javascript
three.js 将图片马赛克化的示例代码
Jul 31 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
一个数据采集类
2007/02/14 PHP
php fckeditor 调用的函数
2009/06/21 PHP
php json与xml序列化/反序列化
2013/10/28 PHP
php中simplexml_load_file函数用法实例
2014/11/12 PHP
PHP使用strtotime计算两个给定日期之间天数的方法
2015/03/18 PHP
php preg_match的匹配不同国家语言实例
2016/12/29 PHP
Yii1.1中通过Sql查询进行的分页操作方法
2017/03/16 PHP
ThinkPHP框架获取最后一次执行SQL语句及变量调试简单操作示例
2018/06/13 PHP
PHP设计模式之迭代器模式Iterator实例分析【对象行为型】
2020/04/26 PHP
php7连接MySQL实现简易查询程序的方法
2020/10/13 PHP
JavaScript中将一个值转换为字符串的方法分析[译]
2012/09/21 Javascript
JavaScript对象反射用法实例
2015/04/17 Javascript
jquery衣服颜色选取插件效果代码分享
2015/08/28 Javascript
javascript:void(0)点击登录没反应怎么解决
2015/11/13 Javascript
基于jquery实现轮播特效
2016/04/22 Javascript
详解Angular2中的编程对象Observable
2016/09/17 Javascript
for循环 + setTimeout 结合一些示例(前端面试题)
2017/08/30 Javascript
js代码规范之Eslint安装与配置详解
2018/09/08 Javascript
Vue中的情侣属性$dispatch和$broadcast详解
2019/03/07 Javascript
详解vue-cli3多环境打包配置
2019/03/28 Javascript
JS操作json对象key、value的常用方法分析
2019/10/29 Javascript
Python获取当前路径实现代码
2017/05/08 Python
深入浅出学习python装饰器
2017/09/29 Python
python logging日志模块以及多进程日志详解
2018/04/18 Python
解决python升级引起的pip执行错误的问题
2018/06/12 Python
python2和python3的输入和输出区别介绍
2018/11/20 Python
如何给HTML标签中的文本设置修饰线
2019/11/18 HTML / CSS
微软英国官方网站:Microsoft英国
2016/10/15 全球购物
anello泰国官方网站:日本流行包包品牌
2019/08/08 全球购物
《小蝌蚪找妈妈》教学反思
2014/02/21 职场文书
诚实守信道德模范事迹材料
2014/08/15 职场文书
2014年国庆节演讲稿
2014/09/02 职场文书
超级礼物观后感
2015/06/15 职场文书
企业管理不到位检讨书
2019/06/27 职场文书
元素水平垂直居中的方式
2021/03/31 HTML / CSS
Python音乐爬虫完美绕过反爬
2021/08/30 Python