前端必刷手写题系列 [9]

401 阅读6分钟

这是我参与更文挑战的第 8 天,活动详情查看 更文挑战

这个系列也没啥花头,就是来整平时面试的一些手写函数,考这些简单实现的好处是能看出基本编码水平,且占用时间不长,更全面地看出你的代码实力如何。一般不会出有很多边界条件的问题,那样面试时间不够用,考察不全面。

平时被考到的 api 如果不知道或不清楚,直接问面试官就行, api 怎么用这些 Google 下谁都能马上了解的知识也看不出水平。关键是在实现过程,和你的编码状态习惯思路清晰程度等。

注意是简单实现,不是完整实现,重要的是概念清晰实现思路清晰,建议先解释清除概念 => 写用例 => 写伪代码 => 再实现具体功能,再优化,一步步来。

18. compose (函数组合)

是什么

这是函数式编程中的一个概念。

简单来说,这就是组合(compose):

var compose = function(f, g) {
  return function(x) {
    return f(g(x));
  };
};

f 和 g 都是函数x 是在它们之间通过“管道”传输的值

组合看起来像是在饲养函数。你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合产下一个崭新的函数。组合的用法如下:

// 就是转大写
let toUpperCase = function(x) { 
    return x.toUpperCase(); 
};
// 惊讶函数, 就是加感叹和语气
let exclaim = function(x) { 
    return x + ' ao ao ao!!!'; 
};

// 组合函数,上面两个结合产生的新函数
let shout = compose(exclaim, toUpperCase);

console.log(shout("hello, my friend"))
// HELLO, MY FRIEND ao ao ao!!!

注意,这里的 compose 函数式要你自己实现的(或者找库函数),这只是个用例,之后我们会手写一个, 写完你可以测试下这些用例。

在 compose 的定义中,g先于 f 执行,因此就创建了一个从右到左的数据流。这样做的可读性远远高于嵌套一大堆的函数调用,如果不用组合,shout 函数将会是这样的:

let shout = function(x) {
    // 函数嵌套 
    return exclaim(toUpperCase(x));
};
// 上面这个可能不够明显看出区别, 来稍微复杂点
let complexFn = function(x) {
    return gn(fn(fn1(exclaim(toUpperCase(x)))));
};

注意代码从右向左运行,而不是由内而外运行, 注意函数的执行顺序

let toUpperCase = function(x) { 
    return x.toUpperCase(); 
};
// 惊讶函数, 就是加感叹和语气
let exclaim = function(x) { 
    return x + ' ao ao ao!!!'; 
};

// 这次我们先执行右边的 exclaim, 再执行转大写,所以语气词也变大写了
let shoutRe = compose(toUpperCase, exclaim);

console.log(shoutRe("hello, my friend"))
HELLO, MY FRIEND AO AO AO!!!

思考下为什么从右向左? 尽管我们可以定义一个从左向右的版本,但是从右向左执行更加能够反映数学上的含义——是的,组合的概念直接来自于数学课本。实际上,现在是时候去看看所有的组合都有的一个特性——结合律(associativity)

const associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
// true

这个特性就是结合律,符合结合律意味着不管你是把 g 和 h 分到一组,还是把 f 和 g 分到一组都不重要。(但注意f,g,h顺序不能改变) {(f, g), h} === {f, (g, h)}

let toUpperCase = (x) => x.toUpperCase();
let exclaim = (x) => x + ' ao ao ao!!!';
// 我们加一个在开头加名字的函数
let nameHead = (x) => 'neverMore ' + x;
// 结合率
let associative1 = compose(toUpperCase, compose(exclaim, nameHead));
let associative2 = compose(compose(toUpperCase, exclaim), nameHead);
console.log(associative1('hello')) // NEVERMORE HELLO AO AO AO!!!
console.log(associative2('hello')) // NEVERMORE HELLO AO AO AO!!!

因为如何为 compose 的调用分组不重要,所以结果都是一样的。这也让我们有能力写一个可变的组合(variadic compose)

前面的例子中我们必须要写两个组合才行,但既然组合是符合结合律的,我们就可以只写一个,而且想传给它多少个函数就传给它多少个,然后让它自己决定如何分组。

let associative = compose(toUpperCase, exclaim, nameHead);
console.log(associative('hello')) // NEVERMORE HELLO AO AO AO!!!

// 其实我们可以继续添加函数
let associativeN = compose(toUpperCase, exclaim, nameHead, fn4, fn5, ...);

运用结合律能为我们带来强大的灵活性,还有对执行结果不会出现意外的那种平和心态

结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起。也可以用 lodash、underscore 以及 ramda 这样的类库中找很多基础函数(进行组合变成你自己的更强大类库)

关于如何组合,并没有标准的答案——我们只是以自己喜欢的方式搭乐高积木罢了。通常来说,最佳实践是让组合可重用。如果熟悉 Fowler 的《重构》一书的话,你可能会认识到这个过程叫做 “extract method”——只不过不需要关心对象的状态。

好了,说这么多,你应该了解了这个概念的方方面面,也有了很多用例接下来是如何实现。

简单手写实现

首先 reduce,api 需要先了解下。

实现

  1. 写个测试用例先

测试用例在上面的概念中已经写了巨多了,直接可使用。

  1. 实现主逻辑

重点

  1. 我们返回的是一个崭新的函数
  2. 注意 reduce 遍历顺序是从左到右compose从右向左运行。
function compose(...fns) {
	// 前面参数特判等边界条件省略,我们主要关注重点
  return fns.reduce(
    (acc, cur) => {
    	return (...args) => acc(cur(...args))
  	}
  );
}

// 用箭头函数式写法,但本人不太喜欢这样的写法,一来一行太长,不利于阅读,而来打点麻烦,也不够语义化
// let compose = (...fns) => fns.reduce((acc, cur) => (...args) => acc(cur(...args)))


let toUpperCase = (x) => x.toUpperCase();
let exclaim = (x) => x + ' ao ao ao!!!';
let nameHead = (x) => 'neverMore ' + x;

let shout = compose(exclaim, toUpperCase);
console.log(shout("hello, my friend")) // HELLO, MY FRIEND ao ao ao!!!

let shoutRe = compose(toUpperCase, exclaim);
console.log(shoutRe("hello, my friend")) // HELLO, MY FRIEND AO AO AO!!!

// 结合率
let associative1 = compose(toUpperCase, compose(exclaim, nameHead));
console.log(associative1('hello')) // NEVERMORE HELLO AO AO AO!!!

let associative2 = compose(compose(toUpperCase, exclaim), nameHead);
console.log(associative2('hello')) // NEVERMORE HELLO AO AO AO!!!

let associative = compose(toUpperCase, exclaim, nameHead);
console.log(associative('hello')) // NEVERMORE HELLO AO AO AO!!!

如果想深入了解函数式编程推荐这本 函数式编程指北

另外向大家着重推荐下另一个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列 记得点赞哈

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友 Or 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考