利用 CoffeeScript 和 Node 进行函数式 JavaScript 编程

函数式脚本编写掌控了 Web 应用程序的复杂性

CoffeeScript 在填补 JavaScript 不完善之处方面声名卓著,但它还有其他一些值得探索的优势。在本文中,Andrew Glover 将向您展示 CoffeeScript 较为整洁的语法如何使您能够更轻松地利用 JavaScript 库中的函数式结构,特别是用于 Node.js 中的服务器端编程。作者最后使用了一系列简短的演示,展示如何使用 JavaScript 的实用工具库 Underscore.js 来处理 CoffeeScript 和 Node 中的集合。

Andrew Glover, CTO, App47

/developerworks/i/p-anglover.jpgAndrew Glover 是一位具有行为驱动开发、持续集成和敏捷软件开发激情的开发人员、作家、演说家和企业家。他是 easyb 行为驱动开发 (Behavior-Driven Development, BDD) 框架的创建者和以下三本书的合著者:持续集成Groovy 在行动Java 测试模式



2012 年 3 月 19 日

CoffeeScript 是一种相对较新的语言,为开发人员提供了不再有 JavaScript 缺陷的令人期待的方案。利用 CoffeeScript,开发人员即可使用一种轻量级、直观的语言完成编码工作,这种语言就像是 Ruby 和 Python 的混合体。对于兼容浏览器的 Web 应用程序,CoffeeScript 将编译为 JavaScript;对于服务器端应用程序来说,它还能与 Node.js 无缝地协同工作。本文的核心是使用 CoffeeScript 的第三项收益,也就是处理 JavaScript 的函数 方面的功能。CoffeeScript 拥有整洁、现代化的语法,释放了 JavaScript 库中潜藏的函数式编程世界。

主流编程语言中的函数式编程

尽管没有任何一种主流编程语言(例如 Java™ 语言、C++ 和 C#)明确作为函数式编程语言,但这些语言中的附加库和框架实现了各种级别的函数式编程。更重要的是,像 Clojure、F# 和 Erlang 这样的语言日益趋向主流,因为函数式编程所产生的 bug 更少,而且能提高复杂应用程序的生产力。

与 JavaScript 相似,函数式编程同样非常有用,但也是一段时间非常不受欢迎。JavaScript 最初被视为一种玩具式的语言,而函数式编程则因超高的复杂度而闻名。但随着对高度并发式应用程序的需求增加,人们急需找到一种替代方法来取代现有编程风格。事实证明,函数式编程并不存在传闻中的不必要的复杂性,它是一款出色的工具,能够整理某些类型的应用程序中固有的复杂性。

在这篇文章中,我们将探讨如何利用名为 Underscore 的 JavaScript 库在 CoffeeScript 和 Node 中进行函数式脚本编程。将这三项技术结合,就会构成一种强大的技术体系,使您能利用 JavaScript,开发出运用函数式编程的服务器端和基于浏览器的应用程序。

请注意,这篇文章是在我之前的两篇文章 “Java 开发 2.0:面向 Java 开发人员的 JavaScript” 和 “面向 Java 开发人员的 Node.js” 的基础之上编写的。我假设您的开发环境中包含 Node.js,而且您已经熟悉了 Node 中的基本编程。

设置 CoffeeScript 和 Node

如果您的开发环境中已经安装了 Node.js,那么您可以直接使用它的包管理器 (NPM) 来安装 CoffeeScript。以下命令将告知 NPM 在全局安装包:

 $> npm install -g coffee-script

使用 CoffeeScript 时,您的大部分时间将花费在编写程序、将其保存为 .coffee 文件、然后将结果编译为 JavaScript 方面。CoffeeScript 的语法与 JavaScript 语法极为接近,因此大多数开发人员都能轻松上手;举例来说,清单 1 中的 CoffeeScript 脚本与 JavaScript 极其相似,只是没有 JavaScript 中常见的那种混乱的括号和分号:

清单 1. 典型的 CoffeeScript
 $> coffee -bpe "console.log 'hello coffee'"    console.log('hello coffee');

coffee 命令是执行某些管理任务的捷径。它能够将 CoffeeScript 文件编译为 JavaScript、运行 CoffeeScript 文件,甚至可以作为一种交互式环境或者 REPL(类似于 Ruby 的 irb)。

下面,我将我的脚本存到一个文件中:

 console.log "hello coffee"

随后我将这个文件编译(或转换)为 JavaScript:

 $> coffee -c hello.coffee

结果获得了一个名为 hello.js 的文件。由于所得到的 JavaScript 脚本对于 Node 同样有效,因此我可以直接在我的 Node 环境中运行它:

清单 2. 在 Node 中运行 JavaScript
 $> node hello.js  hello coffee!

此外,我还可以使用 coffee 命令来运行原始的 .coffee 文件,如清单 3 所示:

清单 3. 在 Node 中运行 CoffeeScript
 $> coffee hello.coffee  hello coffee!

注意观察监控器工具 - watchr

开放源码社区制作了大量便捷的文件监控器实用工具,能够完成运行测试、编译代码等任务。这些工具通常是通过命令行工作的,属于极为轻量级的工具。我们将配置监控器工具,用它来监控我们的开发环境中的所有 .coffee 文件,并在保存时将其编译为 .js 文件。

在实现这个目标时,我喜欢使用的实用工具是 watchr,这是一个 Ruby 库。为了使用 watchr,您的开发环境中需要安装 RubyRubyGems。在安装完成之后,即可运行以下命令,将 watchr 安装为全局 Ruby 库(包括相应的实用工具):

 $> gem install watchr

watchr 中,您使用正则表达式定义要监视的文件,以及应该对其执行的操作。以下命令将 watchr 配置为编译在 src 目录中找到的全部 .coffee 文件:

 watch('src\/.*\.coffee') {|match| system "coffee --compile --output js/ src/"}

请注意,本例中的 coffee 命令会将所得到的 .js 文件置于一个 js 目录内。

我可以在一个终端窗口中触发这项操作,例如:

 $> watchr project.watchr

现在,只要我对 src 目录中的任何 .coffee 文件作出修改,watchr 都能确保创建一个新的 .js 文件,并将其放置在我的 js 目录中。


CoffeeScript 概览

CoffeeScript 引入了多种极有价值的特性,因此使用起来比 JavaScript 更容易。CoffeeScript 大体上消除了使用花括号、分号和 var 关键字、function 关键字的需要。实际上,我最喜爱的 CoffeeScript 特性之一就是它的函数 定义,如清单 4 所示:

清单 4. CoffeeScript 函数非常简单!
 capitalize = (word) -> 
  word.charAt(0).toUpperCase() + word.slice 1

console.log capitalize "andy" //prints Andy

这里,我在 CoffeeScript 中声明了一个简单的函数,将某个词的首字母大写。在 CoffeeScript 中,函数定义的语法紧接一个箭头之后。主体部分也是使用空格分隔的,因此 CoffeeScript 没有花括号。另外还要注意这里没有使用圆括号。CoffeeScript 的 word.slice 1 将编译为 JavaScript 的 word.slice(1)。同样,请注意函数的主题部分也是使用空格分隔的:函数定义行下的所有代码均缩排。下方未缩排的 console.log 表示方法的定义已完整。(CoffeeScript 的这两项特性分别借鉴自 Ruby 和 Python。)

您可能希望了解对应的 JavaScript 函数是怎样的,清单 5 就给出了对应的 JavaScript 代码:

清单 5. 即便是 JavaScript 的单行代码也是非常复杂的
var capitalize = function(word) {
  return word.charAt(0).toUpperCase() + word.slice(1);
};

console.log(capitalize("andy"));

变量

CoffeeScript 能自动在您定义的任何变量之前添加 JavaScript 形式的var。因此,在 CoffeeScript 中编写代码时,您不需要牢记 var。(JavaScript 中的 var 关键字是可选的。如果没有这个关键字,您的变量将成为全局变量,而这种做法在绝大多数情况下都是不合理的做法。)

CoffeeScript 还允许您为参数定义默认值,如清单 6 所示:

清单 6. 默认参数值!
greeting = (recipient = "world") -> 
  "Hello #{recipient}"

console.log greeting "Andy" //prints Hello Andy
console.log greeting()      //prints Hello world

清单 7 展示了对应的 JavaScript 脚本对这种默认参数值的处理方法:

清单 7. 杂乱的 JavaScript
 var greeting;

greeting = function(recipient) {
 if (recipient == null) recipient = "world";
 return "Hello " + recipient;
};

条件

CoffeeScript 可通过引入 andornot 等关键字处理条件,如清单 8 所示:

清单 8. CoffeeScript 条件
 capitalize = (word) -> 
	if word? and typeof(word) is 'string'
		word.charAt(0).toUpperCase() + word.slice 1
	else
		word

console.log capitalize "andy"   //prints Andy
console.log capitalize null     //prints null
console.log capitalize 2        //prints 2
console.log capitalize "betty"  //prints Betty

在清单 8 中,我利用了 ? 操作符来测试条件的存在与否。在尝试将一个词的首字母转为大写之前,这段脚本将确保参数 word 不是 null,同时保证它确属 string 类型。CoffeeScript 的出色之处在于允许您使用 is 来取代 ==

函数式编程的类定义

JavaScript 并不直接支持类;它是一种面向原型的语言。对于那些仍然沉浸在面向对象编程中的人来说,这可能让人感到迷惑不解 — 我们想要自己的类!为了满足这种要求,CoffeeScript 提供了一种 class 语法,在编译为标准 JavaScript 时,能获得函数内定义的一系列函数。

在清单 9 中,我使用 class 关键字定义了一个名为 Message 的类:

清单 9. CoffeeScript 确实支持类
class Message
	constructor: (@to, @from, @message) ->
	
	asJSON:  ->
		JSON.stringify({to: @to, from: @from, message: @message})

mess = new Message "Andy", "Joe", "Go to the party!"
console.log mess.asJSON()

清单 9 中,我使用 constructor 关键字定义了一个构造函数。随后,我输入了一个名称,后接一个函数,我用这种方式定义了一个方法 (asJSON)。


CoffeeScript 与 Node

CoffeeScript 脚本将编译为 JavaScript 脚本,因此 CoffeeScript 是在 Node 中进行编程的理想选择,在简化 Node 原本已经非常整洁的代码方面也是非常有帮助的。CoffeeScript 极其擅长简化 Node 的多种回调,通过一个简单的代码对比即可看出这一点。在清单 10 中,我使用纯 JavaScript 方法定义了一个简单的 Node Web 应用程序:

清单 10. 使用 JavaScript 编写的一个 Node.js web 应用程序
var express = require('express');

var app = express.createServer(express.logger());

app.put('/', function(req, res) {
  res.send(JSON.stringify({ status: "success" }));
});

var port = process.env.PORT || 3000;

app.listen(port, function() {
  console.log("Listening on " + port);
});

在 CoffeeScript 中重新编写相同的 Web 应用程序,消除 Node 回调的复杂语法,如清单 11 所示:

清单 11. CoffeeScript 简化了 Node.js
express = require 'express'

app = express.createServer express.logger()

app.put '/', (req, res) ->
  res.send JSON.stringify { status: "success" }

port = process.env.PORT or 3000

app.listen port, ->
  console.log "Listening on " + port

清单 11 中,我添加了一个 or 操作符,取代了 JavaScript ||。此外,我还发现,使用箭头来表示 app.listen 中的匿名函数比直接键入 function() 更容易。

CoffeeScript 就像日常语言

现在,您很可能已经认识到,CoffeeScript 倾向于使用抽象符号的日常英语表述形式。在 CoffeeScript 中,我们不是键入 !==,而是可以使用更加直观的 isnt;同样,=== 也变为了 is

如果您对这个文件执行 coffee -c,那么就会看到 CoffeeScript 生成了与 清单 10 所示几乎完全相同的 JavaScript 脚本。CoffeeScript 中 100% 有效的 JavaScript 脚本可以配合任何 JavaScript 库一起使用。


通过 Underscore 实现函数式集合

作为 JavaScript 编程的函数式实用工具,Underscore.js 是一个能够简化 JavaScript 开发的函数库。除了其他功能之外,Underscore 还提供了一组丰富的面向集合的函数,非常适合处理特殊任务。

举例来说,假设您需要找到一个数字集合内的所有奇数,该数字集合包含从 0 到 10(不含 10)的数字。尽管您能解决这个问题,但结合使用 CoffeeScript 和 Underscore 能使您节约大量键入时间,或许还能减少一些 bug。在清单 12 中,我提供了基本算法,而 Underscore 提供了聚合函数,即本例中的 filter

清单 12. Underscore 的 filter 函数
 _ = require 'underscore'

numbers = _.range(10)

odds = _(numbers).filter (x) -> 
	x % 2 isnt 0

console.log odds

首先,由于 _(也就是 underscore)是一个有效的变量名,因此我将其设置为引用 Underscore 库。接下来,我将一个匿名函数附加到了测试奇数的 filter 函数。请注意,我使用了 CoffeeScript isnt 关键字,而非 JavaScript 的 !== 关键字。随后我使用 range 函数指定我希望排序数字 0 至 9,此外,我还为我的范围指定了一个步进计数(即按 2 计数),并从任何数字开始。

filter 函数返回一个数组,这是传递给该函数的数组经过过滤之后的版本,在本例中,返回的数组是 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]。因此运行 清单 12 中的代码将得到 [ 1, 3, 5, 7, 9 ]

map 函数是另外一个我最常应用于 JavaScript 中的集合的函数,如清单 13 所示:

清单 13. Underscore 的 map 函数
 oneUp = _(numbers).map (x) ->
	x + 1
	
console.log oneUp

在这里,输出结果应该是 [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]。通常,Underscore 会将 numbers 范围内的各值递增 1,因此我不必手动遍历每一个整数。

如果您需要测试一个集合的多个方面,Underscore 能帮助您简化一切!只需创建一个类似于清单 14 所示的函数即可,这个函数用于测试偶数:

清单 14. Underscore 的 even 函数
 even = (x) ->
	x % 2 is 0
	
console.log _(numbers).all(even)
console.log _(numbers).any(even)

定义了 even 函数之后,即可轻松将其连接到 Underscore 函数,如 allany。在本例中,all 将我的 even 函数应用到 numbers 范围中的每一个值。随后返回一个布尔值,指示是否所有 值均为偶数 (false)。类似地,如果有任何 值是偶数 (true),则 any 函数将返回布尔值 true。

利用 Underscore 完成更多任务

本文仅能简单介绍 Underscore 的部分概况。Underscore 的其他特点还包括函数绑定、JavaScript 模板编写和深度相等性的测试。(请参见 参考资料 部分。)

如果您不需要对一个值集合应用任何此类函数,而是需要执行其他一些操作,那么又该怎样做?完全没有问题!利用 Underscore 的 each 函数即可。each 函数作为一个易用的迭代器(也就是说,它能处理场景背后的循环逻辑,在每次迭代时传入指定的函数)。如果您使用过 Ruby 或者 Groovy,那么应该对这种函数感到非常熟悉。

清单 15. Underscore 的 each 函数
_.each numbers, (x) ->
	console.log(x)

在清单 15 中,each 函数获取一个集合(我的 numbers 范围)和一个需要应用于迭代数组中各值的函数。在本例中,我使用 each 将当前迭代的值输出到控制台。对于我来说,需要做的事情就像将数据保存到数据、将结果返回给用户那样简单。


结束语

CoffeeScript 给 JavaScript 编程注入了新鲜感,也简化了 JavaScript 编程,因此任何用户都能够轻松上手,尤其是熟悉 Ruby 或 Python 的用户。在本文中,我展示了 CoffeeScript 如何通过借鉴这些语言,使 JavaScript 风格的代码更易于阅读,同时还能显著加快编写过程。正如我所演示的那样,将 CoffeeScript、Node 与 Underscore 相结合,即可得到超轻量级的有趣开发堆栈 (development stack),该堆栈适用于基本函数式编程场景。经过一段时间的练习,您就可以将本文所学知识作为基础,深入研究依靠动态 Web 和移动交互的更为复杂的业务应用程序。

参考资料

学习

获得产品和技术

讨论

  • 加入 developerWorks 中文社区。与其他 developerWorks 用户联系,浏览开发人员推动的博客、论坛、小组和 wiki。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


这是您第一次登陆到 developerWorks,已经自动为您创建了您的概要文件。 选择您概要文件中可以公开的信息的信息(如姓名、国家/地区,以及公司),这些信息同时也会与您所发布的内容相关联。 您可以随时更新您的 IBM 账号。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=806111
ArticleTitle=利用 CoffeeScript 和 Node 进行函数式 JavaScript 编程
publish-date=03192012