第2章: 一等公民函数(First-Class Functions)
快速回顾
当提及'一等公民函数'时,我们指它们与普通对象具有完全相同的特性。具体来说,我们可以像处理其他数据类型一样处理函数:存储于数组中、作为参数传递、赋值给变量等操作均不受限制。因为函数本质上并没有特殊之处——它们可以存储在数组里,作为函数参数传递,赋值给变量等操作
这虽然是JavaScript基础知识,但在Github进行代码搜索时会发现,开发者普遍存在规避使用或缺乏理解的现象。让我们通过构造假设示例来验证这一现象
const hi = name => `Hi ${name}`;
const greeting = name => hi(name);示例中greeting对hi的函数包裹完全是冗余的。原因在于JavaScript函数具有可调用特性:当hi带()时执行函数并返回值,不加括号则直接返回变量存储的函数。请自行验证以下代码:
hi; // name => `Hi ${name}`
hi("jonas"); // "Hi jonas"由于greeting仅传递相同参数调用hi,可简写为:
const greeting = hi;
greeting("times"); // "Hi times"既然hi本身就是接收单个参数的函数,为何要再包裹另一层函数用他娘的一模一样的参数来调用它?这就像在七月的酷暑中裹上最厚重的羽绒服,只为了开空调吃冰棒一样荒谬
这种纯粹为了延迟执行而包裹函数的行为不仅冗长可憎,更是糟糕实践(稍后将解释其与代码维护的关系)
深入理解本概念至关重要,现在让我们分析npm包库中的典型案例
// ignorant
const getServerStuff = callback => ajaxCall(json => callback(json));
// enlightened
const getServerStuff = ajaxCall;以下ajax代码在现实中随处可见。两者的等价性原因如下:
// 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这就是标准的实现方式。再次强调以便理解为何要如此锲而不舍
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%都是冗余代码。重构方案可以是:
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
};或完全删除该控制器,因其仅起到组合Views和Db的简单作用
为何推崇一等公民函数?
通过getServerStuff和BlogController示例可见,冗余抽象层会徒增维护成本却无实质价值。这正是我们推崇一等公民函数的主要原因
此外,如需修改被包裹函数,必须同步修改包装函数
httpGet('/post/2', json => renderPost(json));假设httpGet需要支持可能传递的err参数,我们就必须回头修改对应的'胶水代码':
// go back to every httpGet call in the application and explicitly pass err along.
httpGet('/post/2', (json, err) => renderPost(json, err));而采用一等公民函数写法,变更范围将大幅缩减:
// renderPost is called from within httpGet with however many arguments it wants
httpGet('/post/2', renderPost);除去除冗余函数外,参数命名问题也需重视。项目演化过程中,命名不准确是常见的混乱源——特别是随着需求变更
同一概念使用多个名称会引发混淆。以这两个功能相同但通用性天差地别的函数为例:
// 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而以一等公民函数形式调用,我们将面临抽象泄漏的灾难
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对性能优化有必要。若您执着于微观优化,请合上本书;若不可退款,或许可以置换本晦涩难懂之书
至此,我们准备进入下一章