akira_cn 发布于 06月26, 2017

漫谈 JS 函数式编程(一)

原文:https://www.h5jun.com/post/js-functional-1.html

这可能是最简单易懂的函数式编程介(扯)绍(淡)了

目前前端界(以及其他一些领域)对函数式编程大体上两种态度,一些人是觉得函数式编程特牛逼,尤其是现在许多新生的框架和库都在标榜自己的函数式特征。而另一些人,又觉得函数式编程学起来很难,而且似乎也没有什么卵用,理由是在自己经历的项目里面很难看到具体的函数式编程应用场景,甚至其中许多人认同一个观点,觉得函数式编程只适合于学术研究,很难在工程项目中实际使用。

不管你在阅读本文之前属于哪一种人,又或者你是刚接触函数式编程的新人,都没有关系。本文不是研究函数式编程范式的学术研究,而函数式编程作为一个可以说是程序设计理论中最古老的编程范式,在它几十年上百年的发展历史中,已经积累了大量的资料和素材,对于想要在学术领域里完全弄明白它的同学,完全可以在网上、书店里找到各种资料。本文的重点不在于概念,而在于实战。因此,你不会听到太多各种函数式编程的名词讨论,比如诸如 Curry、Mond 之类的专业术语。相反,我们主要来讨论函数式编程在前端领域内使用的一些实际例子,了解为什么前端需要学习函数式编程,使用函数式编程写代码能给我们带来什么。如果弄明白了这些,那么关于函数式编程不实用的谣言也就不攻自破了。


数据抽象或过程抽象

为什么我们接受面向过程或面向对象思想很容易,而我们要完全接受函数式编程却感觉难得多?

我认为这个问题大体上可以这么解释:

人脑本能地容易理解“看得见“、“摸得着”的物体,对于“运动”和“变化”一类不着形的东西,人脑理解起来要略微地费劲一些。而人类要做好一件复杂的事情,大脑有两种抽象方向,一种是对实体进行抽象,另一种是对过程进行抽象:

简答来说,即在软件设计的过程中,如果要保证软件产品的功能稳定可用,同时要保证它的灵活性和可扩展性,那么系统就要有变化的部分和不变的部分。哪些部分应当设计成“不变”,哪些部分应当设计成“可变”,在这个取舍过程中,FP(函数式编程)和 OOP(面向对象编程)正是走了两条不同的路线。

面向对象对数据进行抽象,将行为以对象方法的方式封装到数据实体内部,从而降低系统的耦合度。而函数式编程,选择对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。两者虽是截然不同,然而在系统设计的目标上可以说是殊途同归的。

面向对象思想和函数式编程思想也是不矛盾的,因为一个庞大的系统,可能既要对数据进行抽象,又要对过程进行抽象,或者一个局部适合进行数据抽象,另一个局部适合进行过程抽象,这都是可能的。数据抽象不一定以对象实体为形式,同样过程抽象也不是说形式上必然是 functional 的,比如流式对象(InputStream、OutputStream)、Express 的 middleware,就带有明显的过程抽象的特征。但是在通常情况下,OOP更适合用来做数据抽象,FP更适合用来做过程抽象。

纯函数

再具体深入下去之前,我们先来解答一个问题,那就是为什么用 FP 或过程抽象能够降低系统的耦合度。这里我们要先理解一个概念,这个概念叫“纯函数”。

根据定义,如果一个函数符合两个条件,它被称为纯函数:

  • 此函数在相同的输入值时,总是产生相同的输出。函数的输出和当前运行环境的上下文状态无关。
  • 此函数运行过程不影响运行环境,比如不会触发事件、更改环境中的对象、终端输出值等。

简单来说,也就是当一个函数的输出不受外部环境影响,同时也不影响外部环境时,该函数就是纯函数。

JavaScript 内置函数中有不少纯函数,也有不少非纯函数。

比如以下函数是纯函数:

  • String.prototype.toUpperCase
  • Array.prototype.map
  • Function.prototype.bind

以下函数不是纯函数:

  • Math.random
  • Date.now
  • document.body.appendChild
  • Array.prototype.sort

为什么要区分纯函数和非纯函数呢?因为在系统里,纯函数与非纯函数相比,在可测试性、可维护性、可移植性、并行计算和可扩展性方面都有着巨大的优势。

在这里我用可测试性来举例:

对于纯函数,因为是无状态的,测试的时候不需要构建运行时环境,也不需要用特定的顺序进行测试:

test(t => {
    t.is(add(10, 20), 30); //add(x,y) 是个纯函数,不需要为它构建测试环境
    ...
});

对于非纯函数,就比较复杂:

test.before(t => {
    let list = document.createElement('ul');
    list.id = 'xxxxxx';
    ...
});

test(t => {
    let list = document.getElementById('xxxxxx');
    t.is(sortList(list).innerHTML, `<ul>
        ...
    </ul>`);
});

test.after(t => {
    ...
    document.removeChild(list);
});

函数式编程能够减少系统中的非纯函数

首先我们看一个例子:

JS Bin on jsbin.com

//two impure functions

function setColor(el, color){
  el.style.color = color;
}

function setColors(els, color){
  els.forEach(el => setColor(el, color));
}

let items1 = document.querySelectorAll('ul > li:nth-child(2n + 1)');
let items2 = document.querySelectorAll('ul > li:nth-child(3n + 1)');

setColors(items2, 'green');
setColors(items1, 'red');

在这里我们有两个彼此依赖的非纯函数,setColor(el, color) 和 setColors(els, color)。在测试的时候,我们需要构建环境来测试两个函数。

现在,我们用函数式编程思想来改造这个系统:

JS Bin on jsbin.com

//only one impure function

function batch(fn){
  return function(target, ...args){
    if(target.length >= 0){
      return Array.from(target).map(item => fn.apply(this, [item, ...args]));
    }else{
      return fn.apply(this, [target, ...args]);
    }
  }
}

function setColor(el, color){
  el.style.color = color;
}

let setColors = batch(setColor);

let items1 = document.querySelectorAll('ul > li:nth-child(2n + 1)');
let items2 = document.querySelectorAll('ul > li:nth-child(3n + 1)');

setColors(items2, 'green');
setColors(items1, 'red');

在这里,我们建立一个过程抽象的高阶函数 batch(fn),这个函数的作用是,对它的输入函数返回一个新的函数,这个函数与输入函数的区别是,如果调用的第一个实参是一个数组,那么将这个数组展开,用每一个值依次调用输入函数,返回一个数组,包活每次调用返回的结果。

batch(fn) 本身虽然看似复杂,但是有意思的事,这个函数无疑是纯函数,所以 batch(fn) 自身的测试是非常简单的:

test(t => {
  let add = (x, y) => x + y;
  let listAdd = batch(add);

  t.deepEqual(listAdd([1,2,3], 1), [2,3,4]);
});

由于我们上面举的例子 setColor 和 setColors 虽然不是纯函数,但是却非常简单,因此似乎设计 batch(fn) 的意义不大,有把系统变得更复杂的嫌疑。然而,对于有许多操作 DOM 的函数的框架或库,有了 batch(fn),我们就可以实现很简单的接口(对单一元素操作),然后利用 batch(fn) 获得更复杂接口(对元素进行批量操作),从而大大降低系统本身的复杂的,提升可维护性。

注意一点,batch(fn) 输出的函数有副作用,然而 batch(fn) 用闭包将输出的函数的副作用限制在了 batch(fn) 的作用域内。

Ramda.js 的 lift 方法

Ramda.js 的 lift 方法和 batch 有一点点类似,不过功能更强大。让我们来用它实现一个有一点点“烧脑”的效果,来作为这篇文章的结尾吧:

JS Bin on jsbin.com

async function reducer(promise, action){
  let res = await promise;
  return action(res);
}

function continuous(...functors){
  return async function(input){
    return await functors.reduce(reducer, input)
  }
}

function sleep(ms){
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function setColor(item, color){
  await sleep(500);
  item.style.color = color;
}

let comb = R.lift((el, color) => {
  return [el, color];
});

let changeColorTo = (args) => R.partial(setColor, args);

let items = Array.from(list.children);

let task = R.map(changeColorTo, comb(
  items,
  ['red', 'orange', 'yellow']
));

continuous(...task)(0);

-- 期待下一篇吧 --

阅读全文 »

akira_cn 发布于 03月08, 2017

使用 webpack2 和 NPM Scripts 进行 JavaScript 组件开发

原文:https://www.h5jun.com/post/using-webpack2-and-npm-scripts.html

最近 webpack 成为非常流行的打包工具,很多项目都在使用它。在我们进行 JavaScript 独立组件开发的时候,如果我们想要使用语言新特性,又想发布的时候产出兼容性好的代码,那么使用 webpack 就能够很大程度上帮助我们实现这一目标。

现在让我们来看看究竟该怎么做吧。

阅读全文 »

akira_cn 发布于 03月07, 2017

用60行代码实现JavaScript动画序列播放

原文:https://www.h5jun.com/post/sixty-lines-of-code-animation.html

最近在给学生上课,上周六的第一堂课是关于 JavaScript 动画的内容,其中包括一些简单的动画,比如匀速或者匀加/减速的运动,也包括复杂一些的组合动画。而动画的基本原理,在我之前的文章已经有了详细的介绍。在这里,我想谈一谈的是,我们可以如何针对现代浏览器设计更加简单的 API,来实现动画的序列播放。

阅读全文 »

akira_cn 发布于 02月23, 2017

轻松管理你的 Node 版本

原文:https://www.h5jun.com/post/manage_node_with_n.html

玩 Node.js 的小伙伴们都知道,现在 Node 的版本更新很快,目前最新稳定版已经更新到 v7.6.0 了,而生产环境一般选择使用 LTS(Long-term Support)版本,目前最新的是 v6.10.0。

新版的 Node 7.x.x 有非常有用的更新,那就是支持了 --harmony-async-await。这样就不用依赖 babel 来使用 async/await 特性了。

但是,如何让 7.x.x 和 LTS 的 6.x.x 并存呢?就需要用 Node 版本管理工具了。

阅读全文 »

akira_cn 发布于 01月17, 2017

5分钟现场撸代码——谈总结会抽奖程序

原文:https://www.h5jun.com/post/luckey-draw-in-5-minutes.html

昨天给奇舞团小伙伴们开年度总结会。JK大大为我们捐了10个小水滴摄像机,在开会前5分钟,裕波临时说要写一个抽奖程序,现场抽10名中奖的小伙伴,于是这个抽奖任务就理(莫)所(名)当(其)然(妙)地落到了我这个团长的头上。

闲话少说,那么如何在开会前现场写一个抽奖程序,满足这一要求呢?

阅读全文 »

akira_cn 发布于 11月01, 2016

值得订阅的 12 份优质前端期刊

原文:https://www.h5jun.com/post/the-12-best-weekly.html

值得订阅的 12 份优质前端期刊

作为前端从业人员,能够及时接收行业最新最前沿技术资讯,是个人成长的一项必备技能。大家都知道前端发展迅速,每周都有新东西出现。那么行业里面究竟有哪些值得关注和订阅的前端资讯类期刊呢?小编将多年整理和收集的 12 份优质期刊分享给大家(排名不分先后)

阅读全文 »

akira_cn 发布于 10月19, 2016

【译】前端是 ? 而 JavaScript 是 ?

原文:http://www.zcfy.cc/article/1446

前端将要统治世界,让 JavaScript 君临天下,而码农们则在争论着我们的技术究竟变得有多复杂。

现代前端技术饱受争议,但是我们只顾争吵,似乎忘记了我们所得到的回报。我们,web 王国的劳动人民、web 设计师、前 Wordpress 工程师和吟唱代码的诗人,将要统治数字世界,能够建造各种神迹。谁会想到这一切都是我们的老朋友 JavaScript 所带来的。这个当年的 ? 语言只能用来做些许有趣的 DOM 操作。谁又想到它竟然有这样的超能力?

阅读全文 »