CoffeeScript程序设计

978-7-115-30193-2
作者: 【美】Mark Bates
译者: Goddy Zhao
编辑: 杨海玲

图书目录:

详情

本书从运行和编译CoffeeScript的基础知识开始,介绍其语法、控件结构、函数、集合和类等,并通过比较相同页面的两种语言的代码,讲解CoffeeScript如何改善JavaScript,以及如何用它构建强大、可维护、安全的应用程序。

图书摘要

新锐编程语言集萃

Programming in CoffeeScript

CoffeeScript程序设计

【美】Mark Bates 著

Goddy Zhao 译

人民邮电出版社

北京

图书在版编目(CIP)数据

CoffeeScript程序设计/(美)贝茨(Mark Bates)著;Goddy Zhao译.--北京:人民邮电出版社,2013.1

(新锐编程语言集萃)

书名原文:Programming in CoffeeScript

ISBN 978-7-115-30193-2

Ⅰ.①C… Ⅱ.①贝…②G… Ⅲ.①JAVA语言—程序设计 Ⅳ.①TP312

中国版本图书馆CIP数据核字(2012)第284974号

内容提要

CoffeeScript是一门新的编程语言,一门会被编译为JavaScript的语言。本书从运行和编译CoffeeScript的基础知识开始,逐步介绍其语法、控件结构、函数、集合和类等内容。本书的特色是,通过对相同页面的CoffeeScript代码和JavaScript代码的直接比较,让读者能够直观地了解CoffeeScript是如何改善了JavaScript的,进而能够用它构建强大、灵活、可维护、简洁、可靠以及安全的应用程序。此外,作者还在书中给出一些非常宝贵的提示,提醒读者如何才能更好地开发应用程序。

本书是一本理论和实践相结合的CoffeeScript入门教材,更是一本能够带领初学者充分理解并快速掌握CoffeeScript的好书,非常适合中高级Web开发者阅读。

新锐编程语言集萃

CoffeeScript程序设计

◆著 [美]Mark Bates

译 Goddy Zhao

责任编辑 杨海玲

◆人民邮电出版社出版发行  北京市崇文区夕照寺街14号

邮编 100061  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

三河市海波印务有限公司印刷

◆开本:800×1000 1/16

印张:16

字数:359千字  2013年1月第1版

印数:1-3000册  2013年1月河北第1次印刷

著作权合同登记号 图字:01-2012-6498号

ISBN 978-7-115-30193-2

定价:45.00元

读者服务热线:(010)67132692 印装质量热线:(010)67129223

反盗版热线:(010)67171154

广告经营许可证:京崇工商广字第0021号

版权声明

Authorized translation from the English language edition,entitled:Programming in CoffeeScript, 978-0-321-82010-5 by Mark Bates, published by Pearson Education, Inc., publishing as Addison-Wesley Professional, Copyright © 2012 Pearson Education, Inc.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD. and POSTS & TELECOM PRESS Copyright © 2013.

本书中文简体字版由Pearson Education Asia Ltd. 授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

译者序

随着Web 2.0的流行,尤其是如今Node.js的诞生,让服务器端JavaScript成为了可能,JavaScript迎来了它生命的第二春。截止至现在,GitHub中21%[1]的开源项目都是用JavaScript编写的,占据榜首。RedMonk[2]在2012年9月12日发表的一篇关于统计最流行程序设计语言排行的分析报告中显示 [3],JavaScript是目前最为流行的编程语言。微软公司的Windows 8操作系统中,加入了支持直接用JavaScript编写本地应用的特性。种种数据和现象都说明,JavaScript又一次受到了全世界的关注。

然而,没有任何一门编程语言是完美的,JavaScript也是如此,JavaScript一直以来都因为其语言本身饱受争议:容易造成污染的全局变量、没有原生的命名空间、一大堆的假值、万恶的with和evil,等等。为此,Douglas Crockford[4]专门写了一本名为JavaScript: The Good Parts (《JavaScript语言精粹》)的书,详细介绍了JavaScript中的“糟粕”与“精华”。如今,随着JavaScript的大规模使用,人们从心底里期待一个更好的JavaScript。为此,官方的ECMAScript[5]工作小组也制定了第5版的标准,Chrome、Firefox等浏览器也开始陆续实现并支持第5版的新特性。除此之外,民间的声音也愈渐响亮,其中最有代表性的就当属CoffeeScript了。

和Ruby的到来让人眼前一亮一样,CoffeeScript也给编写JavaScript带来了快感。简洁的语法让人把更多的精力放在逻辑本身,默认生成的匿名包装器函数让人无须再为全局变量的污染提心吊胆,操作符与别名让人无须再为一大堆的假值烦恼,内置的基于类的继承机制让人无须再为了搞清楚原型那些东西而绞尽脑汁。和Ruby一样,CoffeeScript让人们更加快乐地编程。

对于JavaScript程序员来说,CoffeeScript的学习成本非常低,因为它不像Ruby那样是一门全新的语言,它更多的是对JavaScript的扩展和增强;对于非JavaScript程序员而言,CoffeeScript也很容易掌握,因为它的语法非常简洁,CoffeeScript官方网站[6]用短短一页篇幅就展示了所有的语法。更何况,还有本书这样一本理论与实践并存、宽度与广度并具的CoffeeScript教材,可以帮助程序员更好地学习CoffeeScript。

我始终坚信学习技术最好的方式就是实践,在实践的过程中遇到问题,再解决问题;在解决问题的过程中再去深入探寻其背后的理论依据,从而体系化地掌握学习的内容。本书就是这样一本理论和实践相结合的 CoffeeScript入门教材。最为重要的是,它更加侧重于实践,提供了一个非常完整的包括前后端实现的待办事宜列表应用,带领读者学习和掌握CoffeeScript。这也是我很喜欢本书的一个重要原因。除此之外,本书也不缺乏详细的理论,花了4章左右的篇幅详细介绍了 CoffeeScript 所有的语法和精髓。总的来说,这是一本能够带领初学者充分理解并快速掌握CoffeeScript的好书。

另外,我有责任提醒各位读者:CoffeeScript也有它的不足,其中最为致命的就是没有很好的调试方式。原因就在于,编写用的是CoffeeScript,而真正运行的是其编译后的JavaScript代码,因此,一旦出了错误,很难通过JavaScript错误来定位到底CoffeeScript源代码中哪里出错了。不过,好在Chrome的开发者工具以及Firefox的Firebug都在尝试添加CoffeeScript的调试功能,相信不久的将来,这个问题会得到解决。

最后,祝愿所有的读者都能够通过本书学习和掌握CoffeeScript,与此同时,也能体会到编程的乐趣。

[1].https://github.com/languages

[2].http://redmonk.com/

[3].http://redmonk.com/sogrady/2012/09/12/language-rankings-9-12/

[4].http://www.crockford.com/

[5].http://ecmascript.org

[6].http://coffeescript.org/

前言

1999年,我开始了专业的开发生涯,那年我第一次以开发者的身份领取薪水(那年是我开始对Web开发产生浓厚兴趣的时候,我并未将此前的几年开发时间计算在内。)。1999 年,Web还是个“险恶之地”,HTML中最常见到的还是font和table标签,CSS初出茅庐,JavaScript[1]也才刚刚崭露头角,并且主流浏览器之间还弥漫着对JavaScript各自不同实现的硝烟。这就意味着,编写的某段JavaScript代码可以在某个浏览器上正常工作,但却不一定能在其他浏览器上正确运行。正因如此,JavaScript在21世纪初名声并不好。

在21世纪中期发生了两件重要的事情,改变了开发者对JavaScript的看法。第一件就是AJAX[2]。它能让开发人员制作出交互性更好、速度更快的Web页面,使得用户无须手动刷新浏览器就能在后台对服务器进行远程调用。

第二件重要的事情就是JavaScript库的流行,如Prototype[3],它可以简化跨浏览器的JavaScript开发。AJAX提高了Web应用的用户体验,像Prototype这样的JavaScript库则保证了Web应用可以跨主流浏览器工作。

到了2010年,当然也包括2011年,Web应用逐步演变为“单页面”应用(single page application)。这类应用使用像Backbone.js[4]这样的JavaScript框架。这类框架用JavaScript实现了前端的MVC[5]设计模式供开发者使用。整个应用使用JavaScript进行构建,在终端用户的浏览器端进行载入和运行。这些都服务于构建高响应、富客户端应用。

然而,对开发者而言,这并非就尽善尽美。尽管框架和工具简化了这类应用的开发,但是众所周知,JavaScript语言本身有很多诟病,它是一门让人喜忧参半的语言。喜的是它非常强大,忧的是它充满了矛盾和设计陷阱,很快会让你的代码变得难以管理,而且会有隐蔽的潜在bug。

开发者该如何是好?他们想要开发此类新应用,但是唯一被接受的浏览器语言就是JavaScript。当然了,他们可以用Flash[6]来写这类应用,以避免使用JavaScript,但是这样一来就需要插件支持,并且在像iOS[7]这样的不支持Flash的平台下就无法工作。

2010年10月,我“邂逅”了CoffeeScript[8],当时它扬言要“驯服”JavaScript,将JavaScript这门怪异的语言最优秀的部分呈现出来。它拥有更为清晰的语法,比方说:CoffeeScript摒弃了大部分的标点符号[9],采用了有意义的空格,从而保护JavaScript开发者免遭JavaScript语言设计缺陷产生的陷阱,例如糟糕的作用域问题以及比较操作符的误用。其中最值得称赞的是,以上这些工作,CoffeeScript都会在将源代码编译为标准的JavaScript的过程中完成,编译出来的JavaScript可以在任意浏览器以及其他的JavaScript运行时环境中执行。

在我刚使用CoffeeScript时,尽管当时版本号已经是0.9.4了,但这门语言本身仍然“很粗糙”,还有不少问题。当时,我将其用在一个项目的客户端部分,主要是想看看它是否真的有我听说的那么好。但遗憾的是,那个时候有两个原因促使我将其搁置。第一个原因是,它还不够“成熟”,还有太多的bug并且功能尚未完善。

第二个原因是,我尝试使用CoffeeScript的应用并非是一个JavaScript很重的应用[10]。此应用中只用到了少量的校验和AJAX,大部分的工作都让Ruby on Rails[11]帮我处理掉了。

那么又是什么促使我“重拾”CoffeeScript的呢?在我首次尝试CoffeeScript之后—6个月之后的某一天,官方宣布[12]Rails 3.1将以CoffeeScript作为其默认的JavaScript引擎。和绝大多数开发者一样,我被这个消息吸引了。要知道我之前就用过CoffeeScript并且觉得它不怎么好。他们是怎么想的呢?

和其他开发同事不同的是,我又花时间看了一下CoffeeScript。6个月对于任何一个项目来说都是很长的一段开发周期,CoffeeScript 也走过了相当长的一段路,因此我决定再次尝试CoffeeScript,这次将它运用在JavaScript 代码占相当大比重的应用中。经过几天的使用,我不仅改变了对CoffeeScript的看法,甚至成为了它的拥护者。

我不打算告诉你究竟是什么改变了我,也不想告诉你我为什么喜欢它。我想让你做出自己的选择。我希望通过阅读本书不仅能够改变你,而且也能让你成为这门漂亮、小巧的语言的拥护者,而这一切,都是出于你本人的意愿。不过,我会带你初窥一下后面的内容。下面是一段摘自真实应用的CoffeeScript脚本,其后是等效的JavaScript脚本。尽情享受吧!

(源代码:sneak_peak.coffee)

@updateAvatars = ->

names = $('.avatar[data-name]').map -> $(this).data('name')

Utils.findAvatar(name) for name in $.unique(names)

例 (源代码:sneak_peak.js)

(function() {

this.updateAvatars = function() {

var name, names, _i, _len, _ref, _results;

names = $('.avatar[data-name]').map(function() {

return $(this).data('name');

});

_ref = $.unique(names);

_results = [];

for (_i = 0, _len = _ref.length; _i < _len; _i++) {

name = _ref[_i];

_results.push(Utils.findAvatar(name));

}

return _results;

};

}).call(this);

什么是CoffeeScript

CoffeeScript是一门会被编译为JavaScript的语言。我知道这个解释的内容并不丰富,但是事实确实如此。CoffeeScript和Ruby[13]以及Python[14]很像,它设计的初衷就是帮助开发者更加高效地编写JavaScript代码。通过移除没有必要的诸如括号、分号这样的标点符号,用有意义的空格取而代之,可以让开发者更多地将注意力集中在代码本身,而不必去关心大括号是否关闭这样的问题。

比方说,你可能会写下面这样的JavaScript代码。

(源代码:punctuation.js)

(function() {

if (something === something_else) {

console.log('do something');

} else {

console.log('do something else');

}

}).call(this);

那么为什么不试着把它写成下面的形式。

(源代码:punctuation.coffee)

if something is something_else

console.log 'do something'

else

console.log 'do something else'

CoffeeScript还提供了几种快捷方式来简化复杂代码块的编写。例如,下面这段代码就可以实现数组值循环,不需要关心其下标。

(源代码:array.coffee)

for name in array

console.log name

(源代码:array.js)

(function() {

var name, _i, _len;

for (_i = 0, _len = array.length; _i < _len; _i++) {

name = array[_i];

console.log(name);

}

}).call(this);

除了这些改进的语法糖之外,CoffeeScript还可以帮助写出更好的JavaScript代码。例如,帮助准确地确定变量和类的作用域,帮助确保正确使用了比较操作符,等等。这些在阅读本书时都能看到。

CoffeeScript、Ruby以及Python经常会因它们相似的优点共同受到关注。CoffeeScript直接借鉴了Ruby、Python这样的语言提供的简洁语法。正因如此,CoffeeScript比JavaScript这门更像Java[15]或者C++[16]的语言更具“现代感”。与JavaScript相同的是,CoffeeScript可以在任何编程环境中使用。不论是用Ruby、Python、PHP[17]、Java还是用.NET[18]开发的应用,都没有关系。编译后的JavaScript代码都可以很好地和它们一起工作。

因为CoffeeScript会编译为JavaScript,所以你仍然可以使用现在使用的所有JavaScript库,可以用jQuery[19]、Zepto[20]、Backbone[21]、Jasmine[22]等,并且它们都可以很好地工作。而这种情况对于一门新语言来讲并不多见。

听起来很不错吧,不过我仿佛听到了你在问:与JavaScript相比,CoffeeScript的缺点有哪些呢?这是个很好的问题。答案是:缺点肯定有,但还不算多。首先,尽管CoffeeScript是一种很好的编写JavaScript代码的方式,但是,凡是JavaScript无法实现的,使用CoffeeScript也依然无能为力。比方说,我不可能用CoffeeScript创建一个类似Ruby中最著名的method_missing[23]的JavaScript版本。另外,CoffeeScript的最大缺点就是,对于你和你的团队成员,它完全是另外一门要学习的语言。不过好在,它足够简单,你会看到,CoffeeScript非常容易学。

最后,如果出于某种原因,CoffeeScript 不适合你或者你的项目,你也可以使用生成的JavaScript。总之,说真的,你没有理由不在下一个项目甚至是当前项目中尝试 CoffeeScript(CoffeeScript和JavaScript互相配合得很好)。

本书适合什么样的读者

本书适合中高级JavaScript开发者。之所以我觉得本书不大适合对JavaScript不熟悉或者仅对其略知一二的人是有原因的。

首先,本书不会教JavaScript,这是一本介绍CoffeeScript的书。虽然在读的过程中你肯定也能学到一些零碎的JavaScript知识(CoffeeScript会让你学到更多关于JavaScript的知识),但是本书不会从头开始教你JavaScript。

这段代码做了什么?(源代码:example.js)

(function() {

var array, index, _i, _len;

array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

for (_i = 0, _len = array.length; _i < _len; _i++) {

index = array[_i];

console.log(index);

}

}).call(this);

如果你不知道上面这段示例代码是什么意思,我建议你就此打住。不用担心,我是真心想要你再回来继续阅读下去。我只是觉得,如果你能对 JavaScript 有深入的理解,就能最大限度地利用这本书。在本书的讲述过程中,我通常会介绍一些 JavaScript 的基础知识,帮助你更好地理解CoffeeScript。不过,尽管如此,在继续阅读之前,对JavaScript有一定的基础还是非常重要的。因此,请去找本介绍 JavaScript 的优秀书籍(有很多这样的书)来读,然后再和我一起踏上成为CoffeeScript高手的旅途。

对于已经是JavaScript高手的你,让我们一起踏上旅途吧!本书将教你如何使用CoffeeScript编写更简洁、更好的JavaScript代码。

如何阅读本书

这里我提供一些阅读本书的方法,希望能帮助你循序渐进地学习CoffeeScript。第一部分中的各章应该按顺序阅读,因为每一章都是建立在前一章的基础之上的,所以请不要跳着阅读。

在阅读每一章的过程中,你会注意到以下一些事情。

首先,每当我介绍一些外部库、想法和概念的时候,都会加上脚注,写上相应的网站地址,通过它你可以学到更多相关的内容。尽管我很想和你喋喋不休地讲些其他东西,像Ruby,但是本书篇幅有限。因此,如果对我提到的某些东西感兴趣,想要在继续阅读之前了解更多相关的信息,那就请你访问对应的网站,满足你对知识的渴望之后,再回来继续阅读本书。

其次,在每一章中,我有时会先介绍问题错误的解决方案。在看明白了错误的方式后,我们才能审视它、理解它,进而想出正确的解决方案。运用这种方式的一个典型例子在第1章中,我们讨论了从CoffeeScript编译为JavaScript的多种不同方式。

本书中还会出现如下内容:

提示:这里是一些有用的提示。这些是我认为或许会对你有帮助的一些提示。

最后,本书中通常都会一次出现两三段代码块。首先是 CoffeeScript 脚本,然后是对应的编译后的 JavaScript 脚本,如果示例有输出的话,最后,可能还会有示例的输出结果(如果我认为有必要展示的话),如下所示。

(源代码:example.coffee)

array = [1..10]

for index in array

console.log index

(源代码:example.js)

(function() {

var array, index, _i, _len;

array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

for (_i = 0, _len = array.length; _i < _len; _i++) {

index = array[_i];

console.log(index);

}

}).call(this);

输出 (源代码:example.coffee)

1

2

3

4

5

6

7

8

9

10

有时,还会特意展示一些错误,如下所示。

(源代码:oops.coffee)

array = [1..10]

oops! index in array

console.log index

输出 (源代码:oops.coffee)

Error: In content/preface/oops.coffee, Parse error on line 3: Unexpected 'UNARY'

at Object.parseError (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/

➥parser.js:470:11)

at Object.parse (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/

➥parser.js:546:22)

at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/coffee-script.

➥js:40:22

at Object.run (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/

➥coffee-script.js:68:34)

at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/command.js:135:29

at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/command.js:110:18

at [object Object].<anonymous> (fs.js:114:5)

at [object Object].emit (events.js:64:17)

at afterRead (fs.js:1081:12)

at Object.wrapper [as oncomplete] (fs.js:252:17)

本书的组织结构

为了让读者从本书中尽可能地获益,我将本书分为两个部分。

第一部分:核心CoffeeScript

本书第一部分自上向下地介绍了整个 CoffeeScript 语言。阅读完这部分内容之后,就完全可以应对各种CoffeeScript项目了,包括第二部分内容里提到的那些。

第1章“从这里开始”,介绍各种编译和运行CoffeeScript的方法。除此之外,这一章还会介绍CoffeeScript自带的功能强大的命令行工具和REPL。

第2章“基础知识”,介绍CoffeeScript与JavaScript的不同点。内容涵盖语法、变量、作用域,以及更多后续章节所需的基础知识。

第3章“控制结构”,介绍语言中的一个重要部分—控制结构,比如if、else。这一章还会介绍CoffeeScript中的操作符和JavaScript中的操作符的区别。

第4章“函数与参数”,详细介绍CoffeeScript中的函数。内容涵盖函数定义、函数调用以及一些额外的东西,比如默认参数和splat操作符。

第5章“集合与迭代”,从数组到对象,依次介绍在CoffeeScript如何使用、操作,以及迭代集合对象。

第6章“类”,第一部分的最后一章,介绍CoffeeScript中的类。内容涵盖类的定义、类的扩展、函数重载等。

第二部分:CoffeeScript实践

本书第二部分介绍CoffeeScript实践。通过学习CoffeeScript周边的生态系统以及构建完整的应用,让你精通CoffeeScript。

第7章“Cake与Cakefile”,介绍CoffeeScript自带的Cake工具。它用来创建构建脚本、测试脚本等。与之相关的概念都会介绍。

第8章“使用Jasmine测试”,测试是软件开发过程中非常重要的一环,这一章会带你快速一览最流行的 CoffeeScript/JavaScript 测试库之一—Jasmine。这一章还会通过一个计算器类的测试脚本来带着你体验一把流行的测试驱动开发模式。

第9章“Node.js 介绍”,简单介绍事件驱动的服务器端框架—Node.js。这一章会使用CoffeeScript 来构建一个简单的 HTTP 服务器,该服务器可以根据网页浏览器的请求自动将CoffeeScript文件编译为JavaScript文件。

第10章“示例:待办事宜列表第 1 部分(服务器端)”,将介绍构建一个待办事宜列表应用的服务器端部分。在第9章的基础上,我们用Express.js web框架以及MongoDB的ORM框架Mongoose来构建一个API。

第11章“示例:待办事宜列表第2部分(客户端,使用jQuery)”,介绍使用流行的jQuery库来为第10章中建立的待办事宜列表API构建客户端部分。

第12章“示例:待办事宜列表第3部分(客户端,使用Backbone.js)”,抛弃jQuery,采用客户端框架Backbone.js对待办事宜列表应用进行重构。

安装CoffeeScript

我不是很喜欢在书中介绍安装指南,主要是因为到了书上架开售的时候,这些安装指令也许已经过时了。不过,有的时候出版社编辑会觉得应当在书中介绍安装指南。本书便是如此。

安装 CoffeeScript 非常简单。最简单的方式就是访问 CoffeeScript 的官方网站,网址是http://www.coffeescript.org/,然后根据上面的安装指南安装即可。

我相信像CoffeeScript和Node[24]这样的项目的维护者一定会保证官方网站上的安装指南的时效性,所以,直接根据官网的安装指南安装是最好的方式。

本书撰写时,CoffeeScript的版本号是1.2.0。书中所有的例子在该版本的CoffeeScript下都可以工作。

如何运行书中示例

在https://github.com/markbates/Programming-In-CoffeeScript 可以下载本书所有示例的源代码。该站点上的内容一目了然,所有的示例都可以在对应的示例文件中找到。示例文件可以根据出现的章号在对应的章文件夹中找到。

除非有特殊说明,否则,所有的示例代码应该都能在终端以如下形式运行:

> coffee example.coffee

现在你已经知道如何运行本书示例代码了,再安装好CoffeeScript,我们就可以马上进入第1章的内容了。

[1].http://en.wikipedia.org/wiki/JavaScript

[2].http://en.wikipedia.org/wiki/Ajax_(programming)

[3].http://www.prototypejs.org/

[4].http://documentcloud.github.com/backbone/

[5].http://en.wikipedia.org/wiki/Model-view-controller

[6].http://www.adobe.com/

[7].http://www.apple.com/ios/

[8].http://www.coffeescript.org

[9].比方说,最常见的分号、大括号等。—译者注

[10].即富客户端应用。—译者注

[11].http://www.rubyonrails.org

[12].http://www.rubyinside.com/rails-3-1-adopts-coffeescript-jquery-sass-and- controversy-4669.html

[13].http://en.wikipedia.org/wiki/Ruby_(programming_language)

[14].http://en.wikipedia.org/wiki/Python_(programming_language)

[15].http://en.wikipedia.org/wiki/Java_(programming_language)

[16].http://en.wikipedia.org/wiki/C%2B%2B

[17].http://en.wikipedia.org/wiki/Php

[18].http://en.wikipedia.org/wiki/.NET_Framework

[19].http://www.jquery.com

[20].https://github.com/madrobby/zepto

[21].http://documentcloud.github.com/backbone

[22].http://pivotal.github.com/jasmine/

[23].http://ruby-doc.org/docs/ProgrammingRuby/html/ref_c_object.html#Object.method_ missing

[24].http://nodejs.org

致谢

有句话在我第一本书中已经说过,在这里我还要重复一遍:写书真的是一项艰苦的工作!我相信不会有人对此有不同的感受。如果真的有,要么他在撒谎,要么他就是斯蒂芬·金[1]。幸运的是,我介于两者之间。

写书既是一项独立的工作,也需要团队的努力。我哄孩子睡觉后,就一头扎进了书房,一连打开几瓶吉尼斯黑啤,调大音乐声,开始独自写书,一连好几个小时,一直到凌晨。每当完成一章,就发给我的编辑,他会把我的稿子发给其他一些人从不同方面对其进行改进,这些人我可能都不认识。他们简单的会纠正语法或者拼写错误,复杂的会帮助改善书的结构或者指出书中示例代码不清楚的地方。因此,说真的,写作可能是一个人独自在小黑屋中完成的,但是,最终的产品一定是一群人共同努力的结果。

在这里,我有机会对所有为你现在手中(或者下载的)这本高质量的书做出过努力的人表示感谢。下面,请允许我效仿奥斯卡颁奖礼致感谢词,对在感谢名单上遗漏的人,我深感抱歉。

首先,我最要感谢的是我美丽的太太Rachel。她是我见过的最支持我也最坚强的人之一。每一个夜晚在她身边入睡,每一个早晨从她身旁醒来。凝视着她的双眸,我能看到无私的爱,无比幸福。而且,不论我写书、创业还是做任何我觉得开心的事情,她都在背后支持我。她给了我两个帅气的儿子,反过来,我给她的则是我蹩脚的幽默和我用过的手机。我俩的婚姻中,我无疑是受益者,为此,我会永远心存感激。

接下来,我要感谢我的两个儿子Dylan和Leo。尽管他们没有对本书作出直接贡献,但是他们给了我对生活的动力和激情,这一切除了孩子无人能给。儿子,爸爸非常爱你们。

我还要感谢我的父母(特别是你,母亲!)以及其他的家庭成员,谢谢你们一直以来对我的支持,同时,还时刻让我保持自省。我爱你们。

下面,我还要感谢Debra Williams Cauley。Debra是本书的编辑、负责人,同时也是我第一本书—《Ruby分布式编程》(Distributed Programming with Ruby)的“心理医生”。我只希望其他的作者也能同样幸运,能有机会与像Debra这样好的编辑合作。她真的非常有耐心。

我希望下次再写书的话还能与Debra合作。我无法想象写书要是没有她会怎样。真的谢谢你,Debra。

在写技术书的过程中,有些人至关重要,那就是技术审校者。技术审校者的工作就是阅读每个章节,并从技术角度对内容进行评论,就好像在回答这样的问题:“这里介绍这些合适吗?”他们扮演书的读者,同时又都懂技术。因此,他们的反馈非常重要。本书有几位审校者,其中我特别想提的两位是Stuart Garner和Dan Pickett。他们对审校工作非常负责,甚至做了许多超越其职责的事情,而且对我做过的蠢事或者说过的蠢话直言不讳。他们总要被我不分昼夜的邮件和电话打扰,但却总能给我很好的反馈。要不是我想“独吞”版税,我真想把版税分给他们。(别担心,他们也会得到自己的报酬。他们每个人都得到了工作时间一小时的休息。)谢谢Dan、Stuart以及其他审校者们,谢谢你们的辛苦付出。

我要感谢许多以不同方式对本书做出贡献的朋友。有人为本书设计了封面,有人建了索引,有人实现了程序设计语言(CoffeeScript)以及其他与此相关的数之不尽的工作。下面是这些人(我知道的)的名单,排名不分先后:Jeremey Ashkenas、Trevor Burnham、Dan Fishman、Chris Zahn、Gregg Pollack、Gary Adair、Sandra Schroeder、Obie Fernandez、Kristy Hart、Andy Beaster、Barbara Hacha、Tim Wright、Debbie Williams、Brian France、Vanessa Evans、Dan Scherf、Gary Adair、Nonie Ratcliff以及Kim Boedigheimer。

我还要感谢所有在我开始写书时听我唠叨的人。我知道,这对绝大多数人而言有点儿枯燥,但是,我就是喜欢听自己的声音。感谢所有容忍我啰唆的朋友。

最后,我要感谢所有读者。感谢你们购买本书,并支持像我这样的人,纯粹只想将自己的知识分享给别人来帮助开发者们。为了你们,我投入了大量的工作时间以及精力来书写本书。我希望在你们阅读完本书时,能够对 CoffeeScript 有更深的理解,并希望它能够对你们的开发工作产生影响。祝你好运!

[1].斯蒂芬·金是一位作品多产,屡获奖项的美国畅销书作家,编写过剧本、专栏评论,曾担任电影导演、制片人以及演员。—译者注

第一部分 核心CoffeeScript

本书第一部分涵盖所有你想知道的以及需要知道的CoffeeScript 知识。读完第一部分就意味着你已经做好进入CoffeeScript实战的准备,同时也熟悉了它提供的工具,并且对CoffeeScript语言本身有了充分的理解。

我们将先从最基础的内容开始,如学习如何编译和执行CoffeeScript文件;然后逐步开始学习CoffeeScript的语法。在习惯了它的语法之后,紧接着我会介绍控制结构、函数、集合,最后介绍类。

每章都是建立在上一章的基础上的。“一路上”,你会学到所有CoffeeScript拥有的优秀的技巧,来帮助你书写出出色的基于JavaScript的应用。还等什么,内容丰富——让我们开始吧!

第1章 从这里开始

至此,你已阅读过前言部分,也安装了CoffeeScript,那我们该如何使用CoffeeScript呢?本章开始介绍几种编译和执行CoffeeScript的方式。

我打算介绍几种好的编译和执行 CoffeeScript 的方式,与此同时,也会介绍一些不好的方式。尽管本章不会介绍CoffeeScript的内部工作机制,但对于刚刚开始使用CoffeeScript的人来说,本章绝对是不容错过的。熟练掌握CoffeeScript自带的命令行工具,不仅有助于阅读此书,更有助于开发第一个CoffeeScript应用。

哪怕你已经玩转了 CoffeeScript 的命令行工具,本章还是可能有你不了解的内容,因此,别急着跳去阅读第2章,请先花几分钟阅读本章。

1.1 CoffeeScript的REPL

CoffeeScript自带了功能强大的REPL[1](Read-eval-print loop)——一个可交互的控制台,准备好了的话,你可以用它立即开始使用CoffeeScript。

使用REPL非常简单。在你喜欢的终端窗口输入如下这条简单命令即可:

> coffee

随后,应当能看到如下提示:

coffee>

如果看到了coffee的提示,我们就可以开始CoffeeScript之旅了。

下面是另一种启动REPL的方式:

> coffee -i

它是下面这种方式的缩写:

> coffee --interactive

让我们先看一个简单的例子。在控制台输入如下命令:

coffee> 2 + 2

控制台应该会显示如下答案:

coffee> 4

恭喜!你成功迈出了编写CoffeeScript代码的第一步。

好,接下来我们来点更有趣的——更具 CoffeeScript“味道”的。不用太关心下面这段代码究竟做了什么(后续我们会做详细解释),只管运行它。

(源代码:repl1.coffee)

a = [1..10]

b = (x * x for x in a)

console.log b

输出 (源代码:repl1.coffee)

[ 1, 4, 9, 16, 25, 36, 49, 64, 81, 100 ]

上述这段CoffeeScript脚本优美多了吧?简单来说,我们创建了一个新数组,并填充了从1到10的10个数字。然后,循环数组中的这些数字,将它们乘以自身的值并创建第二个数组b,b 中包含了那些新的值。神奇吧?我说过,后续我会很乐意解释上述代码的具体工作原理的。现在,让我们先享受一下可以不用写那么多 JavaScript 带来的喜悦。不过,你要是对上述代码编译后的JavaScript代码是什么样子很好奇,下面就是编译后的JavaScript代码。

(源代码:repl1.js)

(function() {

var a, b, x;

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

b = (function() {

var _i, _len, _results;

_results = [];

for (_i = 0, _len = a.length; _i < _len; _i++) {

x = a[_i];

_results.push(x * x);

}

return _results;

})();

console.log(b);

}).call(this);

如你所见,REPL 是一个很有趣的尝试,可以试验不同的想法。然而,CoffeeScript REPL并非尽善尽美。你将会看到,CoffeeScript中设计的空格是有意义的。当我们在REPL中尝试书写多行CoffeeScript的时候,这种设计就带来了一点小麻烦。解决方案就是使用\字符。

我们来试着写一个简单的add函数,函数接收两个数字,返回这两个数的和。在REPL中输入如下代码。

(源代码:repl2.coffee)

add = (x, y)->\

x + y

console.log add(1, 2)

输出 (源代码:repl2.coffee)

3

注意,我们在第一行代码末尾加了\。这就等于告诉REPL,我们想要在该表达式中添加多行代码。要记住,如果想要将一行代码添加到表达式中,就要在行末添加\,这一点很重要。首行不以\结束的代码就会被认为是表达式的结束,REPL会执行该表达式。

还有一点也很重要,要注意\之后还要进行缩进,这样CoffeeScript才可以正确地解析这行表达式并将其放在正确的位置。

最后,要退出REPL,只要按下Ctrl + C进程就会结束。

REPL 是一个功能强大的工具,同时也为尝试一些想法提供了便捷的途径,但是,正如我们所看到的,当处理一些复杂代码的时候,它就显得力不从心了。在本章后续的1.5.5节“执行CoffeeScript文件”部分,我们会讨论如何执行一个包含CoffeeScript代码的文件,这是一种比较好的执行复杂代码的方式。

1.2 浏览器端编译

在开发Web应用时,有时会想要在HTML[2]文件中直接内联CoffeeScript脚本。CoffeeScript支持这种方式,后续我会介绍如何来实现。不过,我要先给你提个醒——最好不要这么做。首先,最近诸如像非侵入式JavaScript(Unobtrusive JavaScript[3])这样的实践非常流行是有其道理的。没错,能在浏览器端执行CoffeeScript的确是挺酷的,不过,这真的不是最好的编译方式。将JavaScript从HTML层分离出来,放在几个单独的文件中,可以保持代码的整洁,并且在不支持JavaScript的环境中可以更方便地进行优雅降级。

刚开始书写非侵入式JavaScript时会有点困惑,感觉比较难,不过,一段时间之后,写出重用性好、逻辑性强的代码就会显得容易些。使用如jQuery这样的工具,你可以等待页面载入后,用JavaScript来操作页面上的对象。不过,有的时候,还得要自己亲力亲为去手动实现。通常,都是调用一个init方法,可能还会传入JSON[4]参数来达到这一目的。我鼓励你用纯JavaScript来写这段代码。不过,如果你真的很想要用CoffeeScript来实现,CoffeeScript也提供了这样一种在浏览器端编译的方式。

让我们来看一个内嵌了一小段CoffeeScript脚本的HTML文件。

(源代码:hello_world.html)

<html>

<head>

<title>Hello World</title>

<script src='http://jashkenas.github.com/coffee-script/extras/coffee-script.js'➥type='text/javascript'&gt;&lt;/script>

</head>

<body>

<script type='text/coffeescript'>

name = prompt "What is your name?"

alert "Hello, #{name}"

</script>

</body>

</html>

因为浏览器原生不支持CoffeeScript的编译,至少截止到本书撰写时还不支持,所以,需要在页面中引入CoffeeScript的编译器。幸运的是,CoffeeScript团队考虑到了这点,提供了一个可嵌入的编译器。将如下代码添加到HTML文件的head部分,就可以嵌入一个CoffeeScript编译器:

<script src='http://jashkenas.github.com/coffee-script/extras/coffee-script.js'

type='text/javascript'></script>

当然,如果你愿意的话,也可以将coffee-script.js文件下载下来,存储到本地的项目中。

此外就只需要确保设置了正确的script标签类型,让编译器能够获取到内联的CoffeeScript脚本,如下所示:

<script type='text/coffeescript'></script>

在页面载入的时候,coffee-script.js中的CoffeeScript编译器会搜索HTML文档,找到类型为 text/coffeescript 的 script 标签,读取其中的内容,将其编译成等效的JavaScript代码,然后执行编译后的代码。

1.3 警告

现在你已经知道了如何在HTML文档中编译内联的CoffeeScript脚本,接下来我要指出以下几点。首先,本书中讨论的所有与作用域、匿名函数包装器等相关的内容,在编译CoffeeScript的时候也同样适用。因此,在书写类似代码的时候,要将这一点铭记于心,这非常重要。

其次,或许也是最有用的一点,就是这并不是编译 CoffeeScript 脚本最快的方式。在应用中部署该代码就意味着所有使用它的用户都需要下载这额外的 162.26 KB 的文件来编译CoffeeScript脚本。随后,在页面载入后,编译器还得在页面中搜索text/coffeescript标签,编译其中的代码,然后才能执行。这种用户体验并不好。

知道了这两点后,我希望在你部署前,能够选择正确的方式——进行离线编译CoffeeScript脚本。

1.4 命令行编译

尽管在浏览器中执行CoffeeScript脚本很有用也相当容易,但这真的不是最好的编译Coffee Script的方式。我们应当在部署前就编译好CoffeeScript脚本。不过,完全有可能你写的Node应用或者其他服务器端的应用压根不在浏览器端,那么浏览器端编译CoffeeScript就没什么用了。

那么,怎么样才算是最好的编译 CoffeeScript 脚本的方式呢?好问题。你可以找到很多第三方用于编译CoffeeScript脚本的库(不同的平台不同的语言下都有),但是,重要的是,你要理解如何编译CoffeeScript脚本,如果有必要,你甚至可以自己写编译脚本。

compile标志

让我们从coffee命令中最重要的标志-c开始。-c标志会读取你传递给它的CoffeeScript脚本,并在同样的位置编译出JavaScript文件。这也是本书中例子的编译方式。

我们继续来创建一个hello_world.coffee的简单文件,其内容如下:

greeting = "Hello, World!"

console.log greeting

现在,我们输入如下命令来编译该文件:

> coffee -c hello_world.coffee

此命令会在coffee文件所在的目录下,将CoffeeScript脚本编译成名为hello_world.js的新的JavaScript文件,该文件的内容如下:

(function() {

var greeting;

greeting = "Hello, World!";

console.log(greeting);

}).call(this);

好了,这样hello_world.js文件就可以部署了!该是喝杯咖啡休息一会儿的时候了。

1.5 CoffeeScript命令行界面

我们已经“玩过”REPL,并学会了如何使用命令行coffee工具来编译CoffeeScript脚本,而coffee命令还提供了其他一些有趣的选项,值得一看。在终端输入如下命令可以查看所有coffee命令提供的选项列表:

> coffee --help

下面是输出结果:

Usage: coffee [options] path/to/script.coffee

-c, --compile    compile to JavaScript and save as .js files

-i, --interactive  run an interactive CoffeeScript REPL

-o, --output    set the directory for compiled JavaScript

-j, --join     concatenate the scripts before compiling

-w, --watch     watch scripts for changes, and recompile

-p, --print     print the compiled JavaScript to stdout

-l, --lint     pipe the compiled JavaScript through JavaScript Lint

-s, --stdio     listen for and compile scripts over stdio

-e, --eval     compile a string from the command line

-r, --require    require a library before executing your script

-b, --bare     compile without the top-level function wrapper

-t, --tokens    print the tokens that the lexer produces

-n, --nodes     print the parse tree that Jison produces

--nodejs    pass options through to the "node" binary

-v, --version    display CoffeeScript version

-h, --help     display this help message

现在,我们来进一步介绍其中一些选项。

1.5.1 output标志

当只是试验性地尝试CoffeeScript时,完全可以将编译后的JavaScript文件和CoffeeScript文件放在同一目录下,不过,你可能想要将所有编译后的JavaScript文件都放在单独的目录中。那么,如何把我们的hello_world.coffee文件编译到比方说public/javascripts目录中呢?

答案很简单:在终端命令中加入-o标志就可以了。

> coffee -o public/javascripts -c hello_world.coffee

此时查看public/javascripts目录,就会发现编译后的hello_world.js文件已经在那儿了。

-o标志不允许更改文件名,所以编译后的JavaScript文件和原CoffeeScript文件的文件名相同,只是两者后缀不同,前者是.js而后者是.coffee。因为无法用coffee命令来更改文件名,你或许会想更改源文件名,或者写一段脚本来完成这样的任务:编译CoffeeScript文件,然后将其文件名改为你喜欢的名字。

1.5.2 bare标志

在本书后续部分会看到,CoffeeScript编译时,会将编译后的JavaScript代码包在一个匿名函数中。这部分内容会在第2章中详细介绍,所以这里先不深究。下面就是一个将生成的JavaScript代码包在匿名函数中的例子。

(源代码:hello_world.js)

(function() {

var greeting;

greeting = "Hello, World!";

console.log(greeting);

}).call(this);

有时,出于某种原因,可能并不想要这个匿名函数包装器。在这种时候,可以给CoffeeScript编译器传递-b标志。

> coffee -b -c hello_world.coffee

这样将会把我们的CoffeeScript脚本编译为如下JavaScript代码。

(源代码:hello_world_bare.js)

var greeting;

greeting = "Hello, World!";

console.log(greeting);

现在知道如何移除这个匿名函数包装器了吧,不过我要提醒你的是,之所以默认编译器会将 JavaScript 代码包在匿名函数中,是有其重要原因的。如果想要了解更多与此匿名函数相关的内容,请看第2章。

1.5.3 print标志

有时,在编译 CoffeeScript 文件时,会想要直接看到编译后的内容。幸运的是,coffee命令提供了-p标志:

> coffee -p hello_world.coffee

此命令会将结果直接输出到终端,如下所示:

(function() {

var greeting;

greeting = "Hello, World!";

console.log(greeting);

}).call(this);

这对调试来说无疑是非常有用的,或者可以作为很好的学习 CoffeeScript 的工具。通过比较CoffeeScript和编译后的JavaScript(如我们在本书中做的那样),你会渐渐明白CoffeeScript内部究竟做了什么。我刚学CoffeeScript的时候,它帮了我很大的忙。通过用它进一步了解编译器的处理方式,也帮助我成为了一名更好的JavaScript开发者。

1.5.4 watch标志

在开发CoffeeScript项目时,可能有人不愿意老在命令行进行编译。CoffeeScript也考虑到了这点,它提供了-w 参数,可以让编译变得更加容易。有了这个参数,就等于告诉 coffee命令始终监听CoffeeScript文件,一旦文件发生变化就立即重新编译。下面是一个例子:

> coffee -w -c app/assets/coffeescript

键入上述命令之后,app/assets/coffeescript目录及其子目录所有.coffee后缀的文件在任何时候被“触碰”(touched[5])发生任何变化,都会自动被重新编译。

在CoffeeScript 1.2.0中,-w参数会监听所有在被监听目录中新增的文件。不过,以我的经验看,这可能是因为底层Node的问题导致的bug。希望,在你阅读本书的时候,这些问题已经得以解决了。不过,不用担心,还有大量的第三方工具可以用来监听文件系统的相关事件,如:文件添加与移除。截止到本书撰写时,我个人倾向于使用Guard[6]。它是一个Ruby gem,实现监听这类事件并执行一些自定义代码,比方说,当这些事件触发的时候,编译CoffeeScript文件。

提示:除了Guard之外,还可以试试Trevor Burnham的Jitter,它实现了与Guard类似的目标:监听并编译所有的CoffeeScript文件。它也是用CoffeeScript写的,值得一试。

1.5.5 执行CoffeeScript文件

我们已经介绍了编译CoffeeScript的几种方式,同时也讨论了在编译CoffeeScript时传递给coffee命令的一些选项,但是,怎么样执行CoffeeScript文件呢?你可能会用CoffeeScript写一个Web服务器,甚至只是写一个处理简单数字运算的脚本。然后,可以使用你已经学到的工具进行编译,接着将它们外链到HTML文件中,并最终在浏览器中执行。这种方式适用于简单脚本,而对于一些诸如Web服务器这样复杂的情况则不然,并且也不具实践意义。

为了解决这类问题,coffee命令允许直接执行CoffeeScript文件,如下所示:

> coffee hello_world.coffee

本书中大部分的例子也都可以使用这种方式来运行(特殊说明的除外)。

1.5.6 其他选项

还有其他一些诸如-n、-t这样的选项。尽管这些选项能够给你一些非常有趣的输出结果,让你看到CoffeeScript编译时是如何处理的,不过,它们在本书中并不会给我们带来太大帮助,所以,这里我不做介绍。但是,我还是鼓励你花些时间去试试这些选项,看看它们会产生什么。通过阅读在线的CoffeeScript命令的带注解的源代码[7],可以了解更多关于这些选项的信息。

1.6 小结

本章介绍了几种不同的编译和执行 CoffeeScript 代码的方式。同时也介绍了这些编译CoffeeScript方式的优缺点,现在我们已经掌握了理解本书后续例子所需的知识。最后,我们深入探讨了coffee命令,学习了可以传递给它的最重要的一些选项和参数。

第2章 基础知识

我们已经介绍了编译和执行 CoffeeScript 这种比较枯燥的内容,下面我们开始介绍如何书写CoffeeScript。

本章将介绍CoffeeScript的语法:标点符号、作用域、变量以及其他一些小东西。

2.1 语法

对 CoffeeScript 最大的争议莫过于它的语法了,特别是它没有标点符号这一点。诸如大括号、分号这样的标点符号在CoffeeScript 的世界中是绝种的,连括号都是濒临灭绝的“物种”。

为了证明这一点,我们来看一段你或许非常熟悉的JavaScript代码。下面这段jQuery代码,用于发送一个远程AJAX请求,然后对返回的结果做相应的处理。

(源代码:jquery_example.js)

$(function() {

$.get('example.php', function(data) {

if (data.errors != null) {

alert("There was an error!");

} else {

$("#content").text(data.message);

}

}, 'json')

})

针对上述例子,CoffeeScript允许我们忽略大量额外的标点符号。下面是用CoffeeScript写的等效代码。

(源代码:jquery_as_coffee.coffee)

$ ->

$.get 'example.php', (data) ->

if data.errors?

alert "There was an error!"

else

$("#content").text data.message

, 'json'

本书后续部分会详细介绍上述例子的细节,不过,现在先来对比一下上述两段代码,看看我们的CoffeeScript代码中移除了哪些JavaScript中的内容。

2.1.1 有意义的空格

首先,我们移除了所有的大括号和分号。

(源代码:jquery.js)

$(function()

$.get('example.php', function(data)

if (data.errors != null)

alert("There was an error!")

else

$("#content").text(data.message)

, 'json')

)

移除了之后是怎么工作的呢?CoffeeScript 是怎样正确解析这段代码的呢?答案很简单,奥妙就是我们几乎天天要写的:空格!和Python一样,CoffeeScript利用有意义的空格来解析表达式。

听说,有些不喜欢这种有意义的空格的人对其充满抱怨。我发现这种抱怨多少有点儿不同寻常。难道你会像下面这样来书写上述JavaScript代码吗?

(源代码:jquery.js)

$(function() {

$.get('example.php', function(data) {

if (data.errors != null) {

alert("There was an error!");

} else {

$("#content").text(data.message);

}

}, 'json')

})

我希望你不会!如果你真的用这种方式书写JavaScript,那我请求你帮帮你的同伴(和你一样用这种方式书写JavaScript的开发者),多花些时间确保你们的代码是正确缩进的。可读性是可维护性的关键,同时也是帮助你将JavaScript代码转化为CoffeeScript的关键。

使用有意义的空格,当你将if语句下一行代码进行缩进时,CoffeeScript编译器就能知道这行代码属于 if 代码块。编译器下次遇到和 if 语句同样级别的缩进的时候,它就能知道该if语句已经结束并将该代码视作与if同级别去执行。

下面是一个简单的例子,展示了在没有正确格式化CoffeeScript时会出现的错误类型。

(源代码:whitespace.coffee)

for num in [1..3]

if num is 1

console.log num

console.log num * 2

if num is 2

console.log num

console.log num * 2

输出 (源代码:whitespace.coffee)

Error: In content/the_basics/whitespace.coffee, Parse error on line 4: Unexpected

➥'INDENT'

at Object.parseError (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/

➥parser.js:470:11)

at Object.parse (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/

➥parser.js:546:22)

at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/coffee-script.

➥js:40:22

at Object.run (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/

➥coffee-script.js:68:34)

at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/command.js:135:29

at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/command.js:110:18

at [object Object].<anonymous> (fs.js:114:5)

at [object Object].emit (events.js:64:17)

at afterRead (fs.js:1081:12)

at Object.wrapper [as oncomplete] (fs.js:252:17)

2.1.2 function关键字

接下来我们把function关键字给移除了。写CoffeeScript代码不再需要写function关键字了。

(源代码:jquery.js)

$( ()->

$.get('example.php', (data)->

if (data.errors != null)

alert("There was an error!")

else

$("#content").text(data.message)

, 'json')

)

我们可以在函数参数列表的右边用->箭头来替换 function 关键字。我知道,一开始会有些难记而且不容易理解,不过,试着想想代码流以及参数的位置,就会觉得这种箭头的方式事实上很形象。

2.1.3 括号

再接着,我们移除了示例代码中的大部分括号,但并非全部。

(源代码:jquery.js)

$ ->

$.get 'example.php', (data)->

if data.errors != null

alert "There was an error!"

else

$("#content").text data.message

, 'json'

为何我们不将所有的括号都移除呢?绝大部分情况下,移除括号是可选的。何时该使用括号的规则有点混乱,特别是涉及到函数的时候。现在来看看我们刚写的那段代码,看看为何有些括号被保留了,有些却被删除了。

当调用alert函数时,如下所示:

alert "There was an error!"

这里可以将括号移除,原因是当调用一个函数同时还传递参数时,括号可以省略。然而,当调用函数又无须传递参数时,就需要加上括号,为的是让 JavaScript 知道这是调用函数,而不是访问变量。正如我之前所说:有点儿混乱。

提示:当对是否移除括号举棋不定的时候,若觉得加上括号可以让代码更整洁、更有可读性,就可以加上括号。

既然在调用函数并传递参数时,可以省略括号,那为何我们还要将代码写成如下形式呢?

$("#content").text data.message

为什么不直接写成下面这样呢?

$ "#content" .text data.message如果这样写的话,上述代码编译后的JavaScript代码会是如下形式:

$("#content".text(data.message));

正如你所见,CoffeeScript不确定你要调用的是哪个对象上的text函数,所以,它假定是字符串"#content"上的text函数。这里我们保留括号,就是为了明确告诉CoffeeScript,调用的是$("#content")返回的jQuery对象上的text方法。

在结束括号的相关讨论之前(别担心,在介绍函数的时候还会再次提到括号的),我想要指出的是,括号还可以用作逻辑分组运算。

(源代码:grouping.coffee)

if x is true and (y is true or z is true)

console.log 'hello, world'

(源代码:grouping.js)

(function() {

if (x === true && (y === true || z === true)) console.log(‘hello, world’);

}).call(this);

2.2 作用域与变量

本节内容中,我们会介绍CoffeeScript中作用域以及变量是如何定义和工作的。在JavaScript中,这是个棘手的问题,通常也是bug和困惑的来源。本节中,我们会看到CoffeeScript是如何让bug与作用域间的关系成为过去式的。

2.2.1 JavaScript中的变量作用域

很多人,包括新手和有一定经验的开发者,都不知道在JavaScript 中有两种声明变量的方式。其中或许有人知道这两种方式,但却未必知道这两者的区别。正因如此,让我们简单来看下这两种方式,至少从基础层面理解这两种方式的区别。

来看下下面这段代码。

(源代码:js_variable_scope.js)

a = 'A';

myFunc = function() {

a = 'AAA';

var b = 'B';

}

如果在浏览器或者其他的JavaScript引擎中运行上述代码,我们会得到如下结果。

输出:

> console.log(a)

A

> myFunc();

> console.log(a)

AAA

> console.log(b)

ReferenceError: b is not defined

当尝试访问变量b时,结果抛出了错误,这或许是你预期的。但是,你可能意想不到的是,在myFunc函数中对变量a的赋值居然会影响到a的值,对吗?那究竟为什么会这样呢?

答案很简单,其根本原因就是这两个变量的定义方式不同。

当不使用var关键字定义变量时,就等于告诉JavaScript创建一个全局的变量。因为变量a已经在全局作用域中存在了,所以,很遗憾,在myFunc中对a定义的新值就替换了原先的值。

变量b是在myFunc函数内部使用var关键字进行定义的,这就等于告诉JavaScript创建一个名为b的变量,并且其作用域只在myFunc函数中。正因为变量b的作用域在函数内部,所以当我们在函数外部访问b时,就会抛出错误,因为这个时候,JavaScript在全局作用域中根本找不到b了。

提示:本节的例子就证明了为什么在定义变量的时候都应该使用var关键字。这绝对是最佳实践,并且CoffeeScript会帮助你始终使用var来定义变量。

2.2.2 CoffeeScript中的变量作用域

现在,我们已经对 JavaScript 中的变量作用域有所了解,下面让我们再来看一下此前的例子,不同的是,这次用CoffeeScript来写。

(源代码:coffeescript_variable_scope.coffee)

a = 'A'

myFunc = ->

a = 'AAA'

b = 'B'

(源代码:coffeescript_variable_scope.js)

(function() {

var a, myFunc;

a = 'A';

myFunc = function() {

var b;

a = 'AAA';

return b = 'B';

};

}).call(this);

暂时先忽略上述代码最外层的匿名封装器函数(后续很快会对其做详细介绍),让我们先来看看 CoffeeScript 是如何处理变量的声明的。我们注意到,每一个变量,包括指向 myFunc函数的变量,都会用var关键字来声明。CoffeeScript会确保使用正确的方式来声明变量。

上述代码中还有另外很有意思的一点,尽管 CoffeeScript 帮助我们进行正确的变量声明,但它并未在myFunc函数中对变量a再次进行声明。这是因为,当CoffeeScript进行编译时,它看到之前已经定义了变量a,它假定你会在myFunc函数中继续使用该变量。

2.2.3 匿名封装器函数

你看到并且也应该注意到了,所有编译后的 JavaScript 代码都会被封装到一个匿名的自运行的函数中。想必现在你对这个函数到底是做什么用的肯定充满好奇。那就让我来告诉你吧。

正如我们此前在JavaScript的变量作用域的例子中看到的,对于不用var关键字定义的变量,可以很容易地访问到。这样声明的变量会始终属于全局作用域,可以被很容易地访问到。

即使我们定义a变量时使用了var关键字,它仍属于全局作用域。你可能会问,为什么会这样呢?原因很简单,我们在函数外定义了变量a。因为该变量是在函数作用域外定义的,所以它在全局作用域都可用。这意味着,如果有另外一个你使用的库中也定义了变量a,那么后面定义的a会把前面定义的a覆盖掉。当然了,在所有的作用域中都是如此,并非仅仅是全局作用域中。

那么如何在全局作用域中定义变量、函数,才能只让程序本身可以访问而别人无法访问呢?使用匿名封装器函数将代码封装起来。这也正是CoffeeScript将代码编译为JavaScript代码时的做法。

现在,看到这里你可能会想到两件事情。第一,“使用匿名封装器函数这种做法的确很聪明,使得可以在其内部随意操作也不会污染全局作用域。”第二,你应当问问自己,“等等,那如何才能将变量、函数暴露到全局作用域中,供外部访问呢?”这两点非常重要。我们来解决第二个问题,因为这是一种很常见的需求。

如果你将整个程序或者库都写在匿名函数内部的话,那么就和CoffeeScript强制的一样了,其他的库或代码都无法访问你的代码。有的时候,我们的确希望如此,但是如果你并不希望如此的话,这里也有几种解决方案。

下面这个例子展示了将一个函数共享到外部的方法。

通过window对象共享函数(源代码:expose_with_window.coffee)

window.sayHi = ->

console.log "Hello, World!"

通过window对象共享函数(源代码:expose_with_window.js)

(function() {

window.sayHi = function() {

return console.log("Hello, World!");

};

}).call(this);

上述例子中,使用了window对象来对外共享sayHi函数。如果代码在浏览器端运行,这就是一种非常好的对外共享函数的方式。然而,随着 Node.JS 以及其他服务器端 JavaScript技术的崛起,越来越流行在这类环境而非浏览器环境中运行 JavaScript 代码。如果我们直接使用coffee命令来执行这段代码的话,会得到如下输出结果。

通过window对象共享函数(源代码:expose_with_window.coffee.output)

ReferenceError: window is not defined

at Object.<anonymous> (.../the_basics/expose_with_window.coffee:3:3)

at Object.<anonymous> (.../the_basics/expose_with_window.coffee:7:4)

at Module._compile (module.js:432:26)

at Object.run (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/

➥coffee-script.js:68:25)

at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/command.js:135:29

at /usr/local/lib/node_modules/coffee-script/lib/coffee-script/command.js:110:18

at [object Object].<anonymous> (fs.js:114:5)

at [object Object].emit (events.js:64:17)

at afterRead (fs.js:1081:12)

at Object.wrapper [as oncomplete] (fs.js:252:17)

在用coffee命令行运行时,window对象是不存在的,自然也无法通过它来共享函数。那么如何解决这个问题呢?答案也很简单。在CoffeeScript为我们创建匿名函数封装器的同时,也将this对象传递进来了。因此,只要将函数绑定到this对象上就可以实现函数的共享了。看下面的例子。

通过this共享函数(源代码:expose_with_this.coffee)

this.sayHi = ->

console.log "Hello, World!"

通过this共享函数(源代码:expose_with_this.js)

(function() {

this.sayHi = function() {

return console.log("Hello, World!");

};

}).call(this);

当在浏览器端执行上述代码时,sayHi函数会共享给外部的JavaScript代码,因为浏览器中的this就指向window对象。当用coffee命令或者在Node.JS环境中执行时,this就会指向全局对象。使用this来共享变量和函数,既能提高代码健壮性又能获得平台保障性。

出于完整性的考虑,下面给出一个更简单、更整洁的共享代码的方式。

通过@共享函数(源代码:expose_with_at.coffee)

@sayHi = ->

console.log "Hello, World!"

通过@共享函数(源代码:expose_with_at.js)

(function() {

this.sayHi = function() {

return console.log("Hello, World!");

};

}).call(this);

正如在编译后JavaScript代码中看到的那样,在CoffeeScript中,@标志会被编译为this。在CoffeeScript中,任何你想使用this的地方,都可以使用@来替代,效果是一样的。尽管使用@并非强制的,不过在CoffeeScript中推荐使用它,在本书中,我也都会使用@。

2.3 插值

本节将介绍CoffeeScript中的字符串插值、heredoc以及注释。字符串插值可以让我们轻松地构建动态字符串,而不必担心令人困惑、易错的合并语法。heredoc可以让我们很容易地构建格式良好的多行字符串。注释部分就不言自明了。

2.3.1 字符串插值

JavaScript中,最令我懊恼的事情之一就是构建动态字符串。来看下面这个例子,用一些动态属性在JavaScript中构建一个HTML文本域。

(源代码:javascript_concatenation.js)

var field, someId, someName, someValue;

someName = 'user[firstName]';

someId = 'firstName';

someValue = 'Bob Example';

field = "<input type='text' name='" + someName + "' id='" + someId + "' value='" +

➥(escape(someValue)) + "'>";

console.log(field);

输出 (源代码:javascript_concatenation.js)

<input type='text' name='user[firstName]' id='firstName' value='Bob%20Example'>

看到了吧,多么丑陋、令人困惑而且暗藏bug的代码啊!我有没有正确地结束单引号(')?双引号(")的数量对吗?我想应该没问题,不过,这确实不是一眼就能够看清楚的。

CoffeeScript沿用了诸如Ruby这样的更现代的语言的概念,提供了两种不同类型的字符串:插值字符串和文本字符串。我们来看看。

2.3.2 插值字符串

为了摆脱我们在HTML 文本域的例子中看到的令人讨厌的嵌套拼接字符串,CoffeeScript允许使用字符串插值来解决这个问题。

什么是字符串插值?字符串插值是这样一种方式:允许在字符串中注入任意的CoffeeScript代码,并在运行时执行。在我们的例子中,我们想要将一些变量注入到HTML字符串中,CoffeeScript允许我们这样做。下面是一个等效的例子,只是这次使用CoffeeScript的字符串插值来实现。

(源代码:html_string_interpolation.coffee)

someName = 'user[firstName]'

someId = 'firstName'

someValue = 'Bob Example'

field = "<input type='text' name='#{someName}' id='#{someId}' value='#{escape

➥someValue}'>"

console.log field

(源代码:html_string_interpolation.js)

(function() {

var field, someId, someName, someValue;

someName = 'user[firstName]';

someId = 'firstName';

someValue = 'Bob Example';

field = "<input type='text' name='" + someName + "' id='" + someId + "' value='" +➥(escape(someValue)) + "'>";

console.log(field);

}).call(this);

输出 (源代码:html_string_interpolation.coffee)

<input type='text' name='user[firstName]' id='firstName' value='Bob%20Example'>

代码是不是更好看了?这样的代码,更具可读性和可维护性,并且书写也很方便。

如你所知,在JavaScript 中,并没有插值字符串这类东西。所有的字符串都是一样的。而在CoffeeScript中则不然,不同类型字符串处理方式是不同的。比方说,我们刚刚所使用的,用双引号括起来的字符串,如果有必要的话,CoffeeScript编译器会将该字符串转化为拼接的JavaScript字符串。而用单引号括起来的字符串在CoffeeScript中被称为文本字符串,稍后就会对其作相应的介绍。

要在双引号字符串中注入CoffeeScript脚本,可以使用#{}语法。所有在这对大括号中的代码都会被编译器单独解析,并将其结果拼接到该字符串上。大括号中的代码可以是任意有效的CoffeeScript代码。

(源代码:string_interpolation_extra.coffee)

text = "Add numbers: #{1 + 1}"

console.log text

text = "Call a function: #{escape "Hello, World!"}"

console.log text

day = 'Sunday'

console.log "It's a beautiful #{if day is 'Sunday' then day else "Day"}"

(源代码:string_interpolation_extra.js)

(function() {

var day, text;

text = "Add numbers: " + (1 + 1);

console.log(text);

text = "Call a function: " + (escape("Hello, World!"));

console.log(text);

day = 'Sunday';

console.log("It's a beautiful " + (day === 'Sunday' ? day : "Day"));

}).call(this);

输出 (源代码:string_interpolation_extra.coffee)

Add numbers: 2

Call a function: Hello%2C%20World%21

It's a beautiful Sunday

2.3.3 文本字符串

顾名思义,文本字符串就是:字符串中的内容是什么,最后执行输出的就是什么。JavaScript中就只有文本字符串。

在CoffeeScript中使用单引号(')来构建文本字符串。让我们再来看看此前构造的HTML文本域的例子。这次,我们使用单引号而不是双引号来构建,看看会发生什么。

(源代码:html_string_literal.coffee)

someName = 'user[firstName]'

someId = 'firstName'

someValue = 'Bob Example'

field = '<input type=\'text\' name=\'#{someName}\'

➥id=\'#{someId}\' value=\'#{escape(someValue)}\'>'

console.log field

(源代码:html_string_literal.js)

(function() {

var field, someId, someName, someValue;

someName = 'user[firstName]';

someId = 'firstName';

someValue = 'Bob Example';

field = '<input type=\'text\' name=\'#{someName}\'

➥id=\'#{someId}\' value=\'#{escape(someValue)}\'>';

console.log(field);

}).call(this);

输出 (源代码:html_string_literal.coffee)

<input type='text' name='#{someName}' id='#{someId}'value='#{escape(someValue)}'>

从输出可以看出,我们并没有获得预期的输出结果。这是因为文本字符串并不支持字符串插值。最终我们看到的仅仅是动态内容的占位符,而非真正要注入到字符串中的动态内容。有时,我们想要的就是这种效果,但这种情况相对比较少见。

尽管文本字符串不允许注入动态内容,但和JavaScript一样,它还支持少量的运算和解析。CoffeeScript中,还允许在文本字符串中使用常用的转义字符。如下所示。

(源代码:literal_string_with_escapes.coffee)

text = 'Header\n\tIndented Text'

console.log text

(源代码:literal_string_with_escapes.js)

(function() {

var text;

text = 'Header\n\tIndented Text';

console.log(text);

}).call(this);

输出 (源代码:literal_string_with_escapes.coffee)

Header

Indented Text

如你所见,换行符(\n)和tab符号(\t)都被正确地解释并在输出中被正确地处理了。和JavaScript一样,CoffeeScript允许使用双反斜杠来转义单反斜杠,如下所示。

(源代码:literal_string_with_backslash.coffee)

text = 'Insert \\some\\ slashes!'

console.log text

(源代码:literal_string_with_backslash.js)

(function() {

var text;

text = 'Insert \\some\\ slashes!';

console.log(text);

}).call(this);

输出 (源代码:literal_string_with_backslash.coffee)

Insert \some\ slashes!

在像 Ruby 这样的语言中,会使用文本字符串来提高性能。这样在程序执行时,运行时环境就不需要去解析字符串,只需要做一些必要的操作。然而,因为CoffeeScript最终会被编译为JavaScript代码,因此,性能问题也就从运行时转移到了编译时。

提示:使用文本字符串代替插值字符串能提高性能,不过也只提高了编译时的性能。我觉得始终用双引号、插值字符串并没有坏处。即便你现在不用插值字符串,以后要将文本字符串替换为插值字符串也很容易。

2.3.4 heredoc

heredoc[8]也叫here document,允许在CoffeeScript中轻松构建多行字符串,并保留该字符串中所有的空格和换行符。heredoc字符串采用和插值字符串以及文本字符串同样的规则。要在CoffeeScript中构建插值heredoc字符串,可以在每个字符串末尾使用三个双引号。要构建文本heredoc字符串,则使用三个单引号。

让我们来看一个简单的例子。在之前的HTML文本域上再增加一些HTML。

(源代码:heredoc.coffee)

someName = 'user[firstName]'

someId = 'firstName'

someValue = 'Bob Example'

field = """

<ul>

<li>

<input type='text' name='#{someName}' id='#{someId}'

value='#{escape(someValue)}'>

</li>

</ul>

"""

console.log field

(源代码:heredoc.js)

(function() {

var field, someId, someName, someValue;

someName = 'user[firstName]';

someId = 'firstName';

someValue = 'Bob Example';

field = "<ul>\n <li>\n <input type='text' name='" + someName + "' id='" + someId➥+ "' value='" + (escape(someValue)) + "'>\n </li>\n</ul>";

console.log(field);

}).call(this);

输出 (源代码:heredoc.coffee)

<ul>

<li>

<input type='text' name='user[firstName]' id='firstName'value='Bob%20Example'>

</li>

</ul>

如你所见,最终的输出,如原文一样,保留了很好的格式。除此之外,源代码中,heredoc开始部分的缩进也很好地保留了,这使保持代码格式良好变得更加容易。

2.3.5 注释

每一门好的语言都需要多种添加注释的方式,CoffeeScript 也不例外。CoffeeScript 中有两种注释方式,不同的注释方式在最终编译后的JavaScript中表现也不同。

2.3.6 内联注释

第一种注释方式是内联注释。内联注释非常简单,只需使用#符号即可创建内联注释。所有在同行的#符号后的内容都会被CoffeeScript编译器忽略。

(源代码:inline_comment.coffee)

# 计算工资

calcPayroll()

payBils() # 支付账单

(源代码:inline_comment.js)

(function() {

calcPayroll();

payBils();

}).call(this);

上述例子中,可以看到,注释最终并没有出现在JavaScript 代码中。对于这种方式究竟是好是坏,还有些争论。要是注释会跟到编译后的 JavaScript 代码就再好不过了。不过,从CoffeeScript直接映射到JavaScript并非总那么好,编译器没法总能正确地知道注释该在什么位置。从有利的方面看,正因为没有了注释,才使得编译后的 JavaScript 代码更加轻量级,也正因如此,可以让我们在CoffeeScript中随意添加注释,而不用担心会使JavaScript代码变得臃肿。

2.3.7 块级注释

CoffeeScript中另外一种注释是块级注释。块级注释用于多行、内容比较多的注释。这类注释包括许可证、版本信息、API使用文档,等等。与内联注释不同,CoffeeScript会将块级注释带到编译后的JavaScript代码中。

(源代码:block_comment.coffee)

###

My Awesome Library v1.0

Copyright: Me!

Released under the MIT License

###

(源代码:block_comment.js)

/*

My Awesome Library v1.0

Copyright: Me!

Released under the MIT License

*/

(function() {

}).call(this);

如你所见,定义块级注释和定义heredoc类似,通过在注释的两端使用三个#符号就可以定义块级注释了。

2.4 扩展的正则表达式

在如何定义、使用和执行正则表达式[9]方面,CoffeeScript和JavaScript是相同的。而当想要书写那些长的、复杂的正则表达式时,CoffeeScript则提供了一些帮助。

我们都有这样的经历,有些正则表达式比较难处理,所以我们想要将其拆分成多行,并为每行表达式都添加注释。CoffeeScript就允许这样做。

与定义 heredoc 和块级注释类似,要定义一个多行正则表达式,只要在表达式的两端使用三个斜杠即可。

让我们来看一个实际使用多行正则表达式的例子:

(源代码:extended_regex.coffee)

REGEX = /// ^

(/ (?! [\s=] ) # 不允许开头空格或者等号符号

[^ [ / \n \\ ]* # 所有其他内容

(?:

(?: \\[\s\S] # 任何转义的内容

| \[ # 字符类

[^ \] \n \\ ]*

(?: \\[\s\S] [^ \] \n \\ )* )*

]

) [^ [ / \n \\ ]*

)*

/) ([imgy]{0,4}) (?!\w)

///

(源代码:extended_regex.js)

(function() {

var REGEX;

REGEX = /^(\/(?![\s=])[^[\/\n\\]*(?:(?:\\[\s\S]|\[[^\]\n\\]*(?:\\[\s\S]➥[^\]\n\\]*)*])[^[\/\n\\]*]*\/)([imgy]{0,4})(?!\w)/;

}).call(this);

从编译后的JavaScript代码中能看到所有额外的空格和注释都被移除了。

2.5 小结

至此,你已经了解了CoffeeScript的语法,我们可以开始介绍CoffeeScript中更有趣、更细节的部分。如果你对本章介绍的内容还没搞明白,请再花几分钟回过头去看看。搞懂本章内容是非常重要的,因为这些内容是本书后续章节的基础,所以,在进入下一章节内容前,请确保你已掌握了本章内容。

相关图书

Rust游戏开发实战
Rust游戏开发实战
JUnit实战(第3版)
JUnit实战(第3版)
Kafka实战
Kafka实战
Rust实战
Rust实战
PyQt编程快速上手
PyQt编程快速上手
Elasticsearch数据搜索与分析实战
Elasticsearch数据搜索与分析实战

相关文章

相关课程