JS 高阶函数

最近在学习函数式编程,整个 team 都在啃一本叫《Mostly adequate guide》的函数式编程教材,难度确实挺大的,不过新意满满。今天就讲讲 FP 基础中的基础——高阶函数。

Function Object

什么是函数?在大多数编程语言中,函数是一段独立的代码块,用来处理某些通用功能的方法;主要操作是给函数传入特定对象(参数),并在方法调用结束后获得一个新的对象(返回值)。

function greeting(name) {
  return `Hello ${name}`;
}

console.log( greeting('Onion') ); // Hello Onion

但是在 Javascript、Haskell、Clojure 这类语言中,函数是另一种更高级的存在,俗称一等公民;它除了是代码块以外,它还是一种特殊类型的对象——Function Object。

为什么说 Fuction 也是对象呢?还是看上面的示例函数——greeting,我们事实上是可以打印出它的固有属性(properties)的:

console.log(greeting.length, greeting.name);  // 1 'greeting'

这里length是参数列表长度,name就是它定义的名字了。是不是和对象很接近了?我们甚至可以给它添加新的属性和方法:

greeting.displayName = 'Garlic';
greeting.innerName = () => 'Ginger';

console.log(greeting.displayName); // Garlic
console.log(greeting.innerName()); // Ginger

是吧?这么看,函数已经包含了几乎所有的 Object 功能了。当然,生产中尽量不要给函数添加随机属性,毕竟代码是给人阅读的,不要随便增加团队的认知成本。

high order function

上面提到了函数是一种特殊的对象,因此在 js 语言中,函数也可以像普通 object 一样成为其他函数里的参数或是返回值。我们将参数或是返回值为函数的函数称为高阶函数

Higher-Order function is a function that receives a function as an argument or returns the function as output

Function 参数

先看一下函数参数的用法,最经典的案例就是 Array#map。给个例子,实现一个让数组所有元素+1 的操作,传统的做法如下所示:

const arr1 = [1, 2, 3];
const arr2 = [];

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

如果使用高阶函数 map:

const arr1 = [1, 2, 3];

const arr3 = arr1.map( function callback(element, index, array) {
  return element+1;
});

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

map 是 Array.prototype 的原生方法,它的第一个参数是一个 callback 函数,第二个参数是用来绑定 callback 的 this。这里,callback 的作用是迭代调用数组里的元素,并将返回值组装成一个新的数组。这个 map 的函数参数本身还有三个参数:element,index 和 array,分别表示迭代时的元素,索引,以及原始数组。

上面的代码使用 es6 的箭头函数,可以写得更简洁一点:

const arr1 = [1, 2, 3];

const arr3 = arr1.map(e => e+1);

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

讲真,我们经常用到高阶函数,Array 里还有好多类似的函数,如 fliter、reduce 等等。这类高阶函数可以明显的改善代码质量,并切能确保不会对原始数组产生副作用。

Fucntion 返回值

返回值是函数的函数,我们也经常使用,最著名的就是 Function#bind。

给个案例,如下函数 greeting 会打印出thisname,但是 greeting 并不是一个纯函数,因为它的 this 绑定不明确,可能会在不同的运行上下文中会返回不同的结果。

function greeting() {
  return `Hello ${this.name}`;
}

如果想明确它的结果该怎么办呢?嗯,为 greeting 绑定一个 object。这个 helloOnion 就是greeting.bind后返回的新函数。

let helloOnoin = greeting.bind({name: 'Onion'});

console.log(helloOnoin()); // Hello Onion

bind方法创建一个新的函数,在bind被调用时,这个新函数的thisbind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。我们可以试着写一个乞丐版的 myBind 方法(bind 还能绑定参数,这个先略过了),这样可以更清晰地看到什么是返回函数的高阶函数了。

Function.prototype.myBind = function(context) {
  let func = this; // method is attached to the prototype, so just refer to it as this.
  return function newFn() {
    return func.apply(context, arguments);
  }
}

这里给 Function 的原型链加了一个新的函数 myBind,并用到了闭包(在内存里保留了原始函数和目标this);之后,调用 myBind 返回一个新的函数,并且在该函数运行时调用原始函数,最后apply执行时绑定目标 this。看一下效果:

let helloOnoin = greeting.myBind({name: 'Onion'});

console.log(helloOnoin()); // Hello Onoin

我这里再写一个健壮一点的 bind 实现,大家自己体会一下,bind 是如何将前几个参数也绑定了的:

Function.prototype.bind = function(context, ...args) {
  let func = this;
  return function () {
    return func.call(context, ...args, ...arguments);
  }
}

函数柯里化

高阶函数还在一种叫柯里化的方法里大显身手。

在数学和计算机科学中,柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

柯里化,通俗点说就是先给原始函数传入几个参数,它会生成一个新的函数,然后让新的函数去处理接下来的参数。我们先不去管 curry 的实现,看看柯里函数的用法。比如,实现一个 add 函数——简单的两数相加,常规手断就是直接加两参数运行——add(1,2)。但是这里我们先给它做个柯里化处理,并产生了一个新的函数——curryingAdd。

function curry(fn) { ... }

function add(a, b) { return a+b; }

const curryingAdd = curry(add);

柯里化后的 curryingAdd,从普通函数变成了高阶函数:它支持一次传入一个参数(比如 10)并返回一个新的函数——addTen。我们运行addTen(1),它会记录之前已经传入的 10,并把 10 和 1 相加得到 11。是不是觉得很没用?哈,这说明你 FP 学的不够深,在FP里所有的函数都是柯里话了的,所有函数都是可以延迟计算的。

const addTen = curryAdd(10);

console.log(addTen(1)); // 11
console.log(addTen(100)); // 110

柯里化的作用就是将普通函数转变成高阶函数,实现动态创建函数、延迟计算、参数复用等等作用。篇幅有限,我不做深入讲解了。实现上,就是返回一个高阶函数,通过闭包把传入的参数保存起来。当传入的参数数量不足时,递归调用 bind 方法;数量足够时则立即执行函数。学习一下 javascript 的高阶用法还是有意义的。

function curry(fn) {
  const arity = fn.length;

  return function $curry(...args) {
    if( args.length < arity ) {
      return $curry.bind(null, ...args);
    }
    return fn.apply(null, args);
  }
}

compose

compose 也是一个高阶函数里重要的一课。compose 就是组合函数,将子函数串联起来执行,一个函数的输出结果是另一个函数的输入参数,一旦第一个函数开始执行,会像多米诺骨牌一样推导执行后续函数。还是举个例子:我实现了一个带 Hello 的greeting函数,并希望在greeting调用结束后把返回值都显示成大写状态。

const greeting = name => `Hello ${name}`;
const toUpper = str => str.toUpperCase();

toUpper(greeting('Onion')); // HELLO ONION

传统的手段就是嵌套两个函数使用——toUpper(greeting('Onion')),但是有时候这种嵌套可能会很多,比如下面这个态势:

f(g(h(i(j(k('Onion'))))))

再看看 compose 的用法:

const composedFn = compose(f, g, h, i, j, k)
console.log( composedFn('Onion') )

是不是这一个 composedFn 函数比那种一层层的嵌套要美观得多?OK,怎么实现 compose 函数呢?把源码贴在这里了。如果你觉得写(...fns) => (...args) => ..这类代码不可思议的话,建议啃一下上面提到的教材《Mostly adequate guide》,啃完你就发现再正常不过了。

// compose: ( (a->b), (b->c), ..., (y->z) ) -> a -> z
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.apply(null, res)], args)[0];

小结

这一期快速科普了 JS 高阶函数,现实开发中很多人都觉得没啥用,但是面试官很喜欢问这类问题。倒不是说面试官懂很多,大概率他也只是看题库问问题罢了。我是觉得学习这类方法的意义还是在于思维训练——为 FP 编程打好基础;相传,FP 开发人员的收入是普通的三倍。为了成为一个更“有钱”的开发人员,共勉。

相关

文章同步发布于an-Onion 的 Github。码字不易,欢迎点赞。

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

推荐阅读更多精彩内容