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,开发出运用函数式编程的服务器端和基于浏览器的应用程序。
请注意,这篇文章是我假设您的开发环境中包含 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()
更容易。
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
函数,如 all 和 any。在本例中,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 和移动交互的更为复杂的业务应用程序。 |