ES6中的高阶函数:如同 a => b => c 一样简单

作者:Sequoia McDowell

2016年01月16日

ES6来啦!随着越来越多的代码库和思潮引领者开始在他们的代码中使用ES6,以往被认为是“仅需了解”的ES6特性变成了必需的代码常识。这不仅仅是新的语法学习 - 在许多范例中, ES6中新的语言特性可以让在ES5中写起来非常麻烦的表达变得更加简单,进而鼓励了新表达方式的使用。下面我们将关注一个这样简洁表达的使用范例:ES6中的箭头函数如何使高阶函数的书写更加简便。

高阶函数是至少具有以下两种功能之一的函数:

  1. 使用一个或多个函数作为实参
  2. 返回一个函数作为结果

本文的目的并不是说服你立即采用这种新方式,尽管笔者非常鼓励你尝试使用!本文旨在让你熟悉这种表达方式,这样当你在遇到其他人基于ES6写的代码库时,不会如笔者当初一样看着这些陌生代码挠头不解。如果你在此之前需要先复习一下箭头语法的知识,请先查阅这篇文章

希望你熟悉有返回值的箭头函数:

const square = x => x * x;

但是下面这两行代码是什么意思?

const has = p => o => o.hasOwnProperty(p);
const sortBy = p => (a, b) => a[p] > b[p];

“p 返回 o 返回 o.hasOwnProperty…”这句代码是什么意思?我们能这样用吗?

理解语法

为了说明带箭头的高阶函数的书写规则,让我们一起来看一个经典示例:加法函数。在ES5中会这样写:

function add(x){
  return function(y){
     return y + x;
  };
}
var addTwo = add(2);
addTwo(3);          // => 5
add(10)(11);        // => 21 

我们的加法函数输入x,返回了一个输入y返回值是y + x的函数。那我们应该如何用箭头函数表达这个函数呢?已知...

  1. 箭头函数定义是一个表达式,并且
  2. 箭头函数隐式返回单一表达式结果

那么我们所要做的就是让另一个箭头函数作为我们箭头函数的函数体,因此表达方式如下:

const add = x => y => y + x;
// outer function: x => [inner function, uses x]
// inner function: y => y + x;

现在我们就可以通过一个与变量x相关的返回值创建内部函数:

const add2 = add(2);// returns [inner function] where x = 2
add2(4);            // returns 6: exec inner with y = 4, x = 2
add(8)(7);          // 15 

我们所写的加法函数并不是超级有用,但是它可以说明一个外函数如何输入一个以x为变量的参数函数,并在它返回的函数中引用此函数。

用户分类

假如你在github上看到了一个ES6代码库,并且遇到了下面这样的代码:

const has = p => o => o.hasOwnProperty(p);
const sortBy = p => (a, b) => a[p] > b[p];
 
let result;
let users = [
  { name: 'Qian', age: 27, pets : ['Bao'], title : 'Consultant' },
  { name: 'Zeynep', age: 19, pets : ['Civelek', 'Muazzam'] },
  { name: 'Yael', age: 52, title : 'VP of Engineering'}
];
 
result = users
  .filter(has('pets'))
  .sort(sortBy('age')); 

这些代码又是什么意思?我们叫它箭头原型的排列和滤波方法,每一种方法都使用一个单功能参数,但是我们会调用返回函数的函数来筛选和排序,而不是编写表达式去做这些。

让我们来看一看,在下列每种情况下返回函数的表达方式。

无高阶函数:

result = users
  .filter(x => x.hasOwnProperty('pets')) //pass Function to filter
  .sort((a, b) => a.age > b.age);        //pass Function to sort

有高阶函数:

result = users
  .filter(has('pets'))  //pass Function to filter
  .sort(sortBy('age')); //pass Function to sort

在每个实例中,过滤过程都是通过检查对象是否含有名为“pets”的属性值的函数执行的。

为什么它是有用的?

它的有效性出于以下几个原因:

  • 它减少了重复代码
  • 它简化了代码的可重用性
  • 它提升了代码含义的清晰度

想象一下我们只想过滤出有宠物和有职位的用户,我们可以在其中添加另一个函数:

result = users
  .filter(x => x.hasOwnProperty('pets'))
  .filter(x => x.hasOwnProperty('title'))
  ...

这里的重复只是徒增杂乱:它的简明度没有提升,只是写了更多代码妨碍视线。下面是可实现同样功能的使用了has函数的代码:

result = users
  .filter(has('pets'))
  .filter(has('title'))
  ...

这段代码更加短小精悍且易于书写,还可以减少打错字的情况。笔者同时认为这段代码大大增加了阅读清晰度,一眼就能读出代码的含义。
至于函数重用性,如果需要在很多地方过滤出有宠物的用户或者有职位的用户,你可以创建如下函数,并按需重用:

const hasPets = has('pets');
const isEmployed = has('title');
const byAge = sortBy('age');
 
let workers = users.filter(isEmployed);
let petOwningWorkers = workers.filter(hasPets);
let workersByAge = workers.sort(byAge);

我们也可以使用已给出的函数来获得单返回值,并不仅仅是用于过滤数组:

let user = {name: 'Assata', age: 68, title: 'VP of Operations'};
if(isEmployed(user)){   // true
  //do employee action
}
hasPets(user);          // false
has('age')(user);       //true

更进一步

让我们来写一个能检查对象拥有一个有固定主键的过滤函数。has函数可检查主键,然而我们需要知道两项值(主键和键值),而不是一项,来同时检查值。下面请看一种方法:

//[p]roperty, [v]alue, [o]bject:
const is = p => v => o => o.hasOwnProperty(p) && o[p] == v;
 
// broken down:
// outer:  p => [inner1 function, uses p]
// inner1: v => [inner2 function, uses p and v]
// inner2: o => o.hasOwnProperty(p) && o[p] = v;

此处,叫做“is”的函数可执行以下三件事:

  1. 取一个属性名并且返回一个函数,该函数……
  2. 取一个函数值返回一个函数,该函数……
  3. 取一个对象,并检测该对象是否有特定函数值的特定属性,最终返回一个布尔值。

下面是一个使用is函数来过滤用户的示例:

const titleIs = is('title');
// titleIs == v => o => o.hasOwnProperty('title') && o['title'] == v;
 
const isContractor = titleIs('Contractor');
// isContractor == o => o.hasOwnProperty('contractor') && o['title'] == 'Contractor';
 
let contractors = users.filter(isContractor);
let developers  = users.filter(titleIs('Developer'));
 
let user = {name: 'Viola', age: 50, title: 'Actress', pets: ['Zak']};
isEmployed(user);   // true
isContractor(user); // false 

书写风格说明

看一眼下面这个函数,记下你弄清楚函数意义的时间:

const i = x => y => z => h(x)(y) && y[x] == z;

现在请再看一下同功能仅在书写风格中略微不同的函数:

const is = prop => val => obj => has(prop)(obj) && obj[prop] == val;

可见,书写一行越简洁越好的函数的编码趋势是以牺牲可读性为代价的。请克制自己这样做的冲动!简短却无意义的变量名的确看起来很漂亮,但是让人难以理解函数的功能。起一个包含多个词的有意义的变量名和函数名,其实是在帮你自己和同事理解函数功能。

还有一件事...

如果你想以年龄降序排序而不是升序排列,该怎么做呢?或者说查找不是雇员的用户该怎么做呢?我们需要去写一个新的functionssortByDesc 或者notHas程序吗?当然不用!我们可以将已有的返回布尔值的函数封装起来,用一个反转其布尔值的函数来实现上述功能,反之亦然。

//take args, pass them thru to function x, invert the result of x
const invert = x => (...args) => !x(...args);
const noPets = invert(hasPets);
 
let petlessUsersOldestFirst = users
  .filter(noPets)
  .sort(invert(sortBy('age')));

总结

函数式编程在编程界的势头越来越强劲,而ES6使得在JavaScript中采用这种编程思想更加容易。如果你在JavaScript编程过程中还没遇到过函数式编程风格的代码,你很可能在接下来的几个月里就能遇到。这意味着即便你不喜欢这种风格,理解它的基础知识也是非常重要的,有的知识在这里已经提到过了。希望这篇文章提出的概念能够帮你在实际遇到ES6代码时准备好前提知识,更希望它能鼓励你尝试这种编程风格!

OneAPM 助您轻松锁定 .NET 应用性能瓶颈,通过强大的 Trace 记录逐层分析,直至锁定行级问题代码。以用户角度展示系统响应速度,以地域和浏览器维度统计用户使用情况。想阅读更多技术文章,请访问 OneAPM 官方博客
本文转自 OneAPM 官方博客
原文链接:https://strongloop.com/strongblog/higher-order-functions-in-es6easy-as-a-b-c/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容