[译]A Gentle Introdunction to Functional Javascript part 2

本人声明

  1. 本栏仅为归档自己看到的优秀文章; 
  2. 文章版权归原作者所有; 
  3. 因为个人水平有限,翻译难免有错误,请多多包涵。
  4. 第一部分已被翻译,详见Functional Javascript教程(一),全系列共四篇。

原文地址

https://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-arrays/ Written by James Sinclair

背景

    很久没更博了在2018最后一天更博也是希望2019年能把写博客这件事坚持下去,熟悉我的人都知道我语文不咋地,所以在写自己的博客之前我想先尝试翻译几篇优秀的文章,了解他们的写作思路。这个系列是关于函数式编程的文章。函数式编程获得了越来越多的关注,python,JavaScript都对函数式提供了很强的支持,阅读本文也许会对python,JavaScript中的reduce, map函数有新的理解。阅读本文最好有JavaScript语法知识。

文章正文

    这是JavaScript函数式编程入门四部分的第二篇。在上一篇文章中,我们了解到函数如何使特定代码抽象更简单。在这篇文章中我们将对列表应用这些技巧。

Arrays和Lists

    回忆上篇文章,我们讨论了DRY(don't repeat youself)。我们发现函数对于封装一系列会重复使用的代码很有用。但是如果我们正大量重复使用相同的函数怎么办。举个例子:

function addColour(colour){
    var rainbowEl = document.getElementById('rainbow');
    var div = document.createElement('div');
    div.style.paddingTop = '10px';
    div.style.backgroundColour = colour;
    rainbowEl.appendChild(div);
}

addColour('red');
addColour('orange');
addColour('yellow');
addColour('green');
addColour('blue');
addColour('purple');

    其中函数addColour被调用了很多次。我们希望能够避免重复使用相同的代码。一个重构的方法是把这些颜色(red,orange...)放到一个数组中,然后使用循环调用addColour。

var colours = [
    'red', 'orange', 'yellow',
    'green', 'blue', 'purple'
];

for(var i = 0;i < colours.length;i = i+1){
    addColour(colours[i]);
}

    这份代码相当不错,它完成了任务,也比上一份代码减少了重复。但它表达的却不够清晰。我们必须向计算机写出特定的代码:定义一个引索变量(var i=0)并增加它(i++)还要负责检查是否是时候停止循环(i<colours.length)。如果我们把所有的这些循环相关封装到一个函数中会怎样?

For-Each

    因为javaScript允许我们把函数作为一个参数传递给另一个函数,所以写出forEach函数非常直接。

function forEach(callback, array){
    for(var i = 0;i < array.length;i=i+1){
        callback(array[i]);
    }
}

    这个函数以另一个函数callback,和数组(array)为参数并对数组(array)中的每个元素调用callback。

    现在,我们想对数组(colours)中的每个元素运行addColour函数,利用上面的forEach函数,我们用一行代码就可以完成这个目标:

forEach(addColour, colours);

    因为对数组上的每个元素分别调用函数非常有用所以现代JavaScript都把它作为数组的内置方法。所以除了使用上述的forEach函数,我们也像如下使用内置方法:

var colours = [
    'red', 'orange', 'yellow',
    'green', 'blue', 'purple'
];

colours.forEach(addColour);

    可以在此了解更多关于内置forEach方法 MDN JavaScript参考指南

Map

    现在, forEach函数很方便,但是用处很有限。如果我们的传入的回调函数(callback,上文中的addColour函数)返回了一个值,forEach就会直接忽略它。一点小调整,我们可以改动forEach函数返回所有回调函数返回的值,然后我们就会得到一个新的数组,它的值都和原数组中的值一一对应。

    我们来看个例子。假设我们有一组ID,现在想得到这组ID中每个ID相关的dom元素。我们用一种程序化的方法使用for循环来解决:

var ids = ['unicorn', 'fairy', 'kitten'];
var elements = [];
for(var i = 0;i < ids.length;i = i+1){
    elements[i] = document.getElementById(ids[i]);
}
//elements now contains the elements we are after

    再次的我们必须向电脑拼写出关于如何定义一个引索变量以及自增它这些我们本不需要去考虑的细节。让我们像之前forEach函数那样来把这里面的for循环提取出来放到叫做map的函数中(map是映射的意思)。

var map = function(callback, array){
    var newArray = [];
    for(var i = 0;i < array.length;i=i+1){
        newArray[i] = callback(array[i]);
    }
    return newArray;
}

    现在有了我们耀眼的新map函数,我们可以这样使用它:

var getElement = function(id){
    return document.getElementById(id);
};

var elements = map(getElement, ids);

    map函数读入简单琐碎的函数并把他们变成超级有用的函数。map仅仅通过一次简单的调用大大提高了这些函数的用处。

    和forEach一样,map是如此方便,所以现代javascript也把它放进了数组对象的内置方法中。你也可以调用这个内置方法如下所示。

var ids = ['unicorn', 'fairy', 'kitten'];
var getElement = function(id){
    return document.getElementById(id);
};

var elements = ids.map(getElement);

    可以在此了解更多关于内置map方法 MDN JavaScript参考指南

Reduce

    尽管现在map非常的方便,但是我们定义一个更有用的函数,如果我们读入一整个数组只返回一个值(reduce就是这个意思减少返回的维度)。这听上去可能有点反直觉,怎么可能一个返回一个值的函数比返回很多值的函数更有用?为了了解原因,我们首先看一看这个函数是怎样运行的。

    我们来考虑两个相识的问题:

1.给定一组数值,求和;

2.给定一组单词,通过在每个单词间放空格把这组单词连在一起。

    这些例子看上去有点愚蠢,琐碎,确实是这样。但是请和我一起忍受,一旦我们了解了reduce函数的原理,我们就可以在更有趣的地方使用它。

    所以再一次的,使用for循环的程序化的解决办法是:

//Given an array of new numbers, calculate the sum
var numbers = [1, 3, 5, 7, 9];
var total = 0;
for(i = 0;i < numbers.length;i = i+1){
    total = total + numbers[i];
}
//total is 25

//Given an array of words, join them together with a space
var words = ['sparkle', 'fairies', 'are', 'amazing'];
var sentence = '';
for(i = 0;i < words.length;i++){
    sentence = sentence + ' ' + words[i];
}
//'sparkle fairies are amazing'

    这两个解决方法有很多共同点。他们分别使用for循环遍历数组;他们都有一个工作变量(total和sentence);并且他们都设置了它的初始值。

    我们来重构每个循环的内部,把它变成一个函数:

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

var numbers = [1, 3, 5, 7, 9];
var total = 0;
for(i = 0;i < numbers.length;i++){
    total = add(total, numbers[i]);
}

function joinWord(sentence, word){
    return sentence + ' ' + word;
}

var words = ['sparkle', 'fairies', 'are', 'amazing'];
var sentence = '';
for(i = 0;i < words.length;i++){
    sentence = joinWord(sentence ,words[i]);
}

    这似乎没有更简洁,但是模式变得更加清晰了。两个内部函数(add, joinWord)都读入一个工作变量作为他们的第一个参数,读入当前数组元素作为第二个参数。现在这个模式更加清晰了,我们可以移动这些不整洁的for循环到一个函数里:

var reduce = function(callback, initialValue, array){
    var working = initialValue;
    for(var i = 0;i < array.length;i++){
        working = callback(working, array[i]);
    }
    return working;
}

    现在我们得到了一个新的reduce函数,来试一试:

var total = reduce(add, 0, numbers);
var sentence = reduce(joinWord, '', words);

    和forEach,map一样,reduce也内置在标准JavaScript数组对象里。我们也可以这样使用:

var total = numbers.reduce(add, 0);
var sentence = words.reduce(joinWord, '');

    可以在此了解更多关于内置reduce方法 MDN JavaScript参考指南

把他们放在一起

    正如之前提到的那样,这些都是写琐碎的例子——add和joinWord这些函数都是相当简单——这很重要。越小越简单的函数更容易想明白也更容易测试。甚至当我们将两个小又易的函数合并(举个例子,类似add和reduce),它的结果依然比一个很长很复杂的函数容易理解。而且我们可以做更多有趣的事情不仅仅是把数字相加。

    让我们来完成一点复杂的事情。我们将以一些不方便的格式化数据开始,并使用我们的map和reduce函数把它转化成HTML列表,这是我们的数据:

var ponies = [
    [
        ['name', 'Fluttershy'],
        ['image', 'http://tinyurl.com/gpbnlf6'],
        ['description', 'Fluttershy is a female Pegasus pony and one of the main characters of My Little Pony Friendship is Magic.']
    ],
    [
        ['name', 'Applejack'],
        ['image', 'http://tinyurl.com/gkur8a6'],
        ['description', 'Applejack is a female Earth pony and one of the main characters of My Little Pony Friendship is Magic.']
    ],
    [
        ['name', 'Twilight Sparkle'],
        ['image', 'http://tinyurl.com/hj877vs'],
        ['description', 'Twilight Sparkle is the primary main character of My Little Pony Friendship is Magic.']
    ]
];

这些数据并不是很糟糕。如果这些内部数组是格式良好的object对象的话这份数据会更简洁。上一节,我们使用reduce函数计算像数值和字符串这样的基本对象,但是没人规定reduce返回值必须是基本对象。我们可以返回object, array,甚至DOM元素。我们来实现一个函数,它读入上述数据中的其中一个内部数组(比如['name','Fluttershy'])并且把键值对增加到一个对象中(object)。

var addToObject = function(obj, arr){
    obj[arr[0]] = arr[1];
    return obj;
};

利用这个addToObject函数,我们可以把每个'pony'数组转化一个object:

var ponyArrayToObject = function(ponyArray){
    return reduce(addToObject, {}, ponyArray);
};

然后如果我们使用map函数就可以把这整个数组变得更加简洁。

var tidyPonies = map(ponyArrayToObject, poines);

现在我们有一组pony对象了(object),借助Thoms Fuchs' 推特大小的模板引擎,我们可以再一次使用reduce把它转化成嗯HTML片段。这个模板函数读入一个模板字符串和一个object对象,就会在任何它发现大括号包裹的单词上,用来自object对象上的相关词进行转义,举个例子:

var data = {name:"Fluttershy"};
t("Hello {name}!", data);
//"Hello Fluttershy!"

data = {who:"Fluttershy", time:Date.now()};
t("Hello {name}!It's {time} ms since epoch.", data);
//"Hello Fluttershy! It's 1454135887369 ms since epoch."

    所以,如果我们想把pony对象(objec)转化成list元素(dom li),我们可以这样做:

var ponyToListItem = function(pony) {
    var template = '<li><img src="{image}" alt="{name}"/>' +
                   '<div><h3>{name}</h3><p>{description}</p>' +
                   '</div></li>';
    return t(template, pony);
};

这个函数可以帮助我们把每一个pony对象(object)转化成HTML,但是转化整个数组我们还需要用到reduce和joinWord函数:

var ponyList = map(ponyToListItem, tidyPonies);
var html = '<ul>' + reduce(joinWord, '', ponyList)+'</ul>';

    你可以在http://jsbin.com/wuzini/edit?html,js,output完成整个实验。

    一旦理解了map和reduce的适合的使用模式,你可能就会发现自己不再需要写老式的for循环。事实上,在你的下一个项目中完全避免使用for循环是一个很有用的挑战。一旦用上几次map和reduce几次之后,你就会发现有更多的模式可以把他们抽象出来。常见还的包括从数组中过滤(filtering)和采取目标(plucking)值。因为这些模式经常出现,人们把他们一起放到一个函数式编程库中以便大家在这些具有公共模式的地方使用。其中较为出名的库包括

    现在大家知道了将函数作为变量可以有多么的方便,特别是处理列表的时候(数组和链表),你的“工具箱”中也多了一套新的设备。所以现在你可以选择放弃阅读接下来的几章,没有人会轻视你,你仍然可以继续成为一个有创造力的成功的程序员,不用为偏函数应用(partial application),柯里化(currying)和组合(composition)而烦恼。这些问题并不适合每个人。

    但是,如果你愿意迎难而上,你可以接着阅读下面的章节,看看兔子洞能有多深(还未翻)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值