CoffeeScript 是一种相对较新的语言,为开发人员提供了不再有 JavaScript 缺陷的令人期待的方案。利用 CoffeeScript,开发人员即可使用一种轻量级、直观的语言完成编码工作,这种语言就像是 Ruby 和 Python 的混合体。对于兼容浏览器的 Web 应用程序,CoffeeScript 将编译为 JavaScript;对于服务器端应用程序来说,它还能与 Node.js 无缝地协同工作。本文的核心是使用 CoffeeScript 的第三项收益,也就是处理 JavaScript 的函数 方面的功能。CoffeeScript 拥有整洁、现代化的语法,释放了 JavaScript 库中潜藏的函数式编程世界。
与 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
,您的开发环境中需要安装 Ruby 和RubyGems。在安装完成之后,即可运行以下命令,将 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 可通过引入 and
、or
和 not
等关键字处理条件,如清单 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()
更容易。
如果您对这个文件执行 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 函数,如 all
和 any
。在本例中,all
将我的 even
函数应用到 numbers
范围中的每一个值。随后返回一个布尔值,指示是否所有 值均为偶数 (false)。类似地,如果有任何 值是偶数 (true),则 any
函数将返回布尔值 true。
如果您不需要对一个值集合应用任何此类函数,而是需要执行其他一些操作,那么又该怎样做?完全没有问题!利用 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 和移动交互的更为复杂的业务应用程序。