Skip to content

第2章: 一等公民函数(First-Class Functions)

快速回顾

当提及'一等公民函数'时,我们指它们与普通对象具有完全相同的特性。具体来说,我们可以像处理其他数据类型一样处理函数:存储于数组中、作为参数传递、赋值给变量等操作均不受限制。因为函数本质上并没有特殊之处——它们可以存储在数组里,作为函数参数传递,赋值给变量等操作

这虽然是JavaScript基础知识,但在Github进行代码搜索时会发现,开发者普遍存在规避使用或缺乏理解的现象。让我们通过构造假设示例来验证这一现象

js
const hi = name => `Hi ${name}`;
const greeting = name => hi(name);

示例中greetinghi的函数包裹完全是冗余的。原因在于JavaScript函数具有可调用特性:当hi()时执行函数并返回值,不加括号则直接返回变量存储的函数。请自行验证以下代码:

js
hi; // name => `Hi ${name}`
hi("jonas"); // "Hi jonas"

由于greeting仅传递相同参数调用hi,可简写为:

js
const greeting = hi;
greeting("times"); // "Hi times"

既然hi本身就是接收单个参数的函数,为何要再包裹另一层函数用他娘的一模一样的参数来调用它?这就像在七月的酷暑中裹上最厚重的羽绒服,只为了开空调吃冰棒一样荒谬

这种纯粹为了延迟执行而包裹函数的行为不仅冗长可憎,更是糟糕实践(稍后将解释其与代码维护的关系)

深入理解本概念至关重要,现在让我们分析npm包库中的典型案例

js
// ignorant
const getServerStuff = callback => ajaxCall(json => callback(json));

// enlightened
const getServerStuff = ajaxCall;

以下ajax代码在现实中随处可见。两者的等价性原因如下:

js
// this line
ajaxCall(json => callback(json));

// is the same as this line
ajaxCall(callback);

// so refactor getServerStuff
const getServerStuff = callback => ajaxCall(callback);

// ...which is equivalent to this
const getServerStuff = ajaxCall; // <-- look mum, no ()'s

这就是标准的实现方式。再次强调以便理解为何要如此锲而不舍

js
const BlogController = {
  index(posts) { return Views.index(posts); },
  show(post) { return Views.show(post); },
  create(attrs) { return Db.create(attrs); },
  update(post, attrs) { return Db.update(post, attrs); },
  destroy(post) { return Db.destroy(post); },
};

该控制器99%都是冗余代码。重构方案可以是:

js
const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
};

或完全删除该控制器,因其仅起到组合Views和Db的简单作用

为何推崇一等公民函数?

通过getServerStuffBlogController示例可见,冗余抽象层会徒增维护成本却无实质价值。这正是我们推崇一等公民函数的主要原因

此外,如需修改被包裹函数,必须同步修改包装函数

js
httpGet('/post/2', json => renderPost(json));

假设httpGet需要支持可能传递的err参数,我们就必须回头修改对应的'胶水代码':

js
// go back to every httpGet call in the application and explicitly pass err along.
httpGet('/post/2', (json, err) => renderPost(json, err));

而采用一等公民函数写法,变更范围将大幅缩减:

js
// renderPost is called from within httpGet with however many arguments it wants
httpGet('/post/2', renderPost);

除去除冗余函数外,参数命名问题也需重视。项目演化过程中,命名不准确是常见的混乱源——特别是随着需求变更

同一概念使用多个名称会引发混淆。以这两个功能相同但通用性天差地别的函数为例:

js
// specific to our current blog
const validArticles = articles =>
  articles.filter(article => article !== null && article !== undefined),

// vastly more relevant for future projects
const compact = xs => xs.filter(x => x !== null && x !== undefined);

特定命名将代码与数据强耦合(如articles),这种情况常导致重复造轮子

需要特别提醒:类似面向对象编程,必须警惕this带来的致命问题。若底层函数使用this而以一等公民函数形式调用,我们将面临抽象泄漏的灾难

js
const fs = require('fs');

// scary
fs.readFile('freaky_friday.txt', Db.save);

// less so
fs.readFile('freaky_friday.txt', Db.save.bind(Db));

通过自我绑定,Db可以随意访问其原型链上的垃圾代码。我尽量像躲避瘟疫般回避使用this——编写函数式代码时根本不需它。但在集成第三方库时,可能不得不向周遭的混乱现实妥协

部分观点主张this对性能优化有必要。若您执着于微观优化,请合上本书;若不可退款,或许可以置换本晦涩难懂之书

至此,我们准备进入下一章

第3章: 纯函数带来的愉悦体验