来自dz210 的answer 非常棒:易读、简单、直截了当且易于操作。
我将建议一些更难做的事情。我建议你为自己构建一个可重用函数的小工具包,它可以帮助你更简单地解决这些简单的问题。
构建小函数
首先,您的最终数据结构如下所示:
var friends = {
bill: {
firstName: "Bill",
lastName: "Gates",
number: "(206) 555-5555",
address: ['One Microsoft Way','Redmond','WA','98052']
},
steve: {
firstName: "Steve",
lastName: "Jobs",
number: "(408) 555-5555",
address: ['1 Infinite Loop','Cupertino','CA','95014']
}
}
您需要提取其中存储的两个值,分别表示为“bill”和“steve”。这是提取对象值的函数的第一次传递,如下所示:
var values = obj => {
var results = [];
Object.keys(obj).forEach(key => results.push(obj[key]));
return results;
}
请注意,这使用内置功能来提取对象的键。然后对于每一个,您都需要获取lastName 属性。这是一个简单的函数:
var getLastName = obj => obj['lastName'];
(我知道我可以使用点符号。请耐心等待。)
虽然这适用于这种用法,但它显然非常具体。 firstName 的一个看起来像这样:
var getFirstName = obj => obj['firstName'];
等等。但是如果我们想让这个可重用,我们可能会创建一个函数,给定一个属性名称,返回一个函数,该函数接受一个对象并检索该对象的命名属性,如下所示:
var prop = name => obj => obj[name];
那么我们有
var getLastName = prop('lastName');
现在,对于我们的例子,我们想要获取列表中每个元素的姓氏。通过对每个数据元素应用一个函数将一个数据结构转换为另一个相同形状的数据结构称为mapping,我们可以编写一个简单的map 函数:
var map = fn => list => list.map(fn);
(注意there are reasons 是为了避免这种风格的实现,但一开始也不错。)
这让我们可以做例如,
map(n => n * n)([1, 2, 3, 4, 5]); //=> [1, 4, 9, 16, 25]
或
map(str => str.toUpperCase())(['foo', 'bar', 'baz']); //=> ['FOO', 'BAR', "BAZ']
所以现在要收集列表中每个朋友的姓氏,我们可以将map(getLastName) 应用于values(friends) 的结果:
map(getLastName)(values(friends)); //=> ['Gates', 'Jobs']
当然我们可以到此为止,但仍有一些有用的清理工作要做。姓氏没有什么特别之处。我们可能经常想从列表的每个元素中获取命名属性的值,给它一个有用的名称会很好。在许多地方,它被称为“pluck”,表示从每个元素中提取命名属性。这是一个基本的实现:
var pluck = name => map(prop(name));
允许
var getLastNames = people => pluck('lastName')(values(people))
getLastNames(friends); //=> ['Gates', 'Jobs']
但是一旦我们有了map,我们也可以简单地实现values:
var values = obj => map(key => obj[key])(Object.keys(obj));
pluck 和getLastNames 之间也有一些共同点。在每种情况下,我们调用一个函数,并将其结果传递给另一个函数。在数学中,这称为composition,我们可以编写另一个函数来表示它。这个函数是我们在这里开发的样式代码的核心,将两个函数合二为一以实现可重用性:
var compose = (f, g) => x => f(g(x));
使用它,我们可以重写我们的两个函数:
var pluck = compose(map, prop);
var getLastNames = compose(pluck('lastName'), values);
结果
// library functions
var map = fn => list => list.map(fn);
var compose = (f, g) => x => f(g(x));
var prop = name => obj => obj[name];
var pluck = compose(map, prop);
var values = obj => map(key => obj[key])(Object.keys(obj));
// application code
var getLastNames = compose(pluck('lastName'), values);
这里有两点需要注意:
- 此仍然的最终输出不会将您的输出记录到控制台。那是问题中的要求,不设法做到这一点似乎很荒谬。我将展示如何做到这一点,但请注意,这可能不是请求的真正目的。通常登录控制台没有什么重要的目的。我们用它来表明我们有我们想要的数据,但是我们以不同的方式使用它。到目前为止,这些函数都是纯函数,它们仅根据输入计算输出,而不是任何外部信息源,并且不会产生副作用,例如更新 DOM 或记录到安慰。在您的应用程序中的某个时刻,您可能需要产生副作用。这里展示的编程风格建议尽可能将这些与您的其余代码隔离开来。那些让人很难理解正在发生的事情。纯函数易于理解;这些不是。因此,如果您的大部分代码是纯的,并且您隔离了不纯的东西,它将使测试和调试变得非常容易。但是如果我们想最终记录我们的输出,我们可以这样写:
var log = obj => console.log(obj);
var lastNames = getLastNames(friends); //=> ['Gates', 'Jobs']
lastNames.forEach(log); // logs 'Gates' and then 'Jobs' to the console
- 尽管其余功能相当简单,
values 却相当复杂。这是因为我们需要在第一个调用的函数中使用obj 参数,然后在使用该结果的函数中再次使用。当然有一些技术可以帮助我们做到这一点,但它们比这里所做的任何事情都复杂。
为什么要打扰?
为什么有人会写这样的代码来解决这么简单的问题?
他们可能不会。
但是小问题会成倍增加并变成更大的问题。有很多方法可以处理一个人的需求增长。我在这里建议的是一种帮助管理这种复杂性的技术。通过为许多此类需求构建一个包含简单可重用函数的工具包,人们可以在安全的基础上构建更大更好的代码。
此外,所讨论的两个函数map 和compose 最终成为函数式编程中最突出的两个函数。如果你继续使用这种技术,你会一次又一次地看到它们。其他的都是可重用和合理的,除了一个专门为此需求编写的函数。但是让我们再看一遍:
var getLastNames = compose(pluck('lastName'), values);
一旦我们习惯了构图的概念,这是一本非常优雅的读物。我们可以理解为getLastNames是values和pluck('lastName')这两个函数的组合。前者是明确的,后者应该是直截了当的。我们不必阅读任何描述如何 提取名称或如何组合这两个函数的代码。这感觉更接近于声明我们希望程序做什么,而不是一组关于如何做的明确说明。这就是重点。我们越能描述什么我们想要而不是如何去做,我们越是要求计算机理解我们而不是试图理解计算机。
我不能使用图书馆吗?
当然可以。围绕其中一些原则设计了许多库。 Underscore 旨在为 Javascript 带来一些功能,Lodash 提供了非常相似的功能集。这是两个最流行的 JS 工具。但是它们都坚持一些本机方法的设计,而牺牲了可能更干净的功能设计。其他库,例如 fn.js、fnuc、FKit 和我最喜欢的 Ramda(披露:我是作者之一),在它们的前景中更直接地起作用。
例如,使用 Ramda,代码将是
var getLastNames = R.compose(R.pluck('lastName'), R.values);
但是在这里使用库的危险在于,您依赖它而牺牲了对正在发生的事情的理解。如果你对compose、map、prop 和pluck 和values 做了什么有很好的理解,那么请使用一个库,它可能具有这些函数的更好测试、久经考验的版本很有用。但如果你不这样做,它可能会阻碍你的学习。从自己尝试这些事情开始,然后慢慢转向图书馆,这永远不会有什么坏处。
这种风格叫什么?
这种从普通数据结构上的简单函数构建一切的技术,使用可以接受函数和/或返回新函数的函数,是所谓的函数式编程的标志之一.还有许多其他相当常见的功能,我们将只讨论其中两个。其中最重要的一项是引用透明度,其中任何表达式都可以用它的值替换,而不会改变程序的行为。通过主要使用 pure 函数,我们朝着这一目标迈出了一大步。另一种是不可变数据结构。虽然 Javascript 不支持此功能,但我们可以拒绝修改传递给我们的数据并始终返回新结构。您可能会注意到,这就是我们在这里所做的。 (尽管请注意,在原始问题中的 friends.bill = ... 和 friends.steve = ... 行中,您没有使用不可变的数据结构。)
网络上有很多很棒的函数式编程资源,最近,其中一些资源针对的是 Javascript。现在是使用这种材料的好时机。