写在前面的话

  之前在工作群里看到一个排行榜:

  在羡慕美国同行的薪水超高的同时,也奇怪为何 CoffeeScript 的生命力超过了自己之前的想象。

  JavaScript 本是 Brendan Eich 在10天内做完的急就章之作,在设计之初,即带有大量的大意设计和缺陷。可以说,即使到了 1.5 时代,JavaScript 作为一门现代语言,依然要提防 Douglas Crockford 在《JavaScript:The Good Parts》中提出的种种陷阱。历年来,各路框架作者和超集语言作者,都不断在 JavaScript 上做出各种各样炫目的模式用法和衍生方言,足见其可提高空间之大。包括 CoffeeScript/TypeScript/Dart/Elm 等解决方案的出现,其实就是在倡导使用 Pre-JavaScript 的语言编写抽象逻辑,然后编译成原生 JavaScript 运行。

  CoffeeScript 即是 JavaScript 1.5 时代的 Pre-JavaScript 语言中的佼佼者。其设计的语法和句法利用了 Ruby 和 Python 的优点,然而又能去除 JavaScript 中容易产生二义性的部分,可以认为是一种变换写法的 JavaScript 语言子集,也就是美化过的“The Good Parts”。CoffeeScript 的定位,本来是一门 little language,它的目的不是取代 JavaScript,而是用更好的风格来编写 JavaScript,因此其最终目标也是编译成 JavaScript。因此,很多人都把它当做下一代 JavaScript 标准出现以前的过渡用法。

  2015年以来,ES6.0以及后来的 ES2015等标准的制定以及现代浏览器对原生语法支持的逐步实现,使得大部分众望所归的语言特性都可以在原生 JavaScript 中找到。旧的 CoffeeScript(CoffeeScript 1) 编译的结果已然不兼容新的ES2015的发展方向,CoffeeScript 作为一个过渡时期的产物,似乎已然完成了它的历史使命。

  但 CoffeeScript 的发展并没有停止。CofffeeScript 紧随现代 JavaScript 推出了 CoffeeScript 2。这一版本的 CoffeeScript 不仅保留了大部分上一版本 Ruby/Python 风格的优美语法,也大量兼容了 ES2015 的新特性(除了 import/export 这个在前后端实行起来经常需要转义和 polyfill 的特性以外),成为了一门更加现代的 little langugage。

  可能有读者会问,既然已经有了 ES2015,为什么还要再来一门编程语言呢?笔者认为,不同的编程语言,其实是不同的思考和设计工具。虽说图灵完备的语言总是等价的,但通过另一个角度来对问题和解建模,可以更有效地提高自己对原本掌握的语言的理解。因此,了解 CoffeeScript 的设计和使用理念,一定能对使用原生 JavaScript 编程有所脾益。

  本文基本上是 coffeescript.org 在2.0版本后文档的摘译和简化。每一个小的知识点会配上若干的代码块,两个相连的代码块总是代表一段 CoffeeScript 代码和它编译生成的 JavaScript 代码。阅读本文需要一定的 JavaScript 基础。

简明教程

CoffeeScript 是什么?

CoffeeScript 是一门编译成 Javascript 的小语言。在 Java 风格的笨拙锈色之下,JavaScript 有着一颗华丽的心。CoffeeScript 试图用简明的方式,把 JavaScript 中的精粹部分表现出来。

CoffeeScript 的黄金法则是:“它只是 JavaScript”。代码会被一对一地编译成对等的 JS,不会有运行时解释。可以在 CoffeeScript 中无缝地使用任何现存的 JavaScript 库(反之亦然)。编译输出是可读,美化打印过的,而且趋向于和等价的手写 JavaScript 跑得一样快。

安装和试用 coffee

最新版本:2.1.0

1
2
3
4
5
# 为一个项目局部安装:
npm install --save-dev coffeescript

# 全局安装以在任意处执行.coffee 文件:
npm install --global coffeescript

假设我们有一个 test.coffee 的文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Assignment:
number = 42
opposite = true

# Conditions:
number = -42 if opposite

# Functions:
square = (x) -> x * x

# Arrays:
list = [1, 2, 3, 4, 5]

# Objects:
math =
root: Math.sqrt
square: square
cube: (x) -> x * square x

# Splats:
race = (winner, runners...) ->
print winner, runners

# Existence:
alert "I knew it!" if elvis?

# Array comprehensions:
cubes = (math.cube num for num in list)

运行coffee -c test.coffee就会得到一个test.js 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Generated by CoffeeScript 2.1.0
(function() {
// Assignment:
var cubes, list, math, num, number, opposite, race, square;

number = 42;

opposite = true;

if (opposite) {
// Conditions:
number = -42;
}

// Functions:
square = function(x) {
return x * x;
};

// Arrays:
list = [1, 2, 3, 4, 5];

// Objects:
math = {
root: Math.sqrt,
square: square,
cube: function(x) {
return x * square(x);
}
};

// Splats:
race = function(winner, ...runners) {
return print(winner, runners);
};

if (typeof elvis !== "undefined" && elvis !== null) {
// Existence:
alert("I knew it!");
}

// Array comprehensions:
cubes = (function() {
var i, len, results;
results = [];
for (i = 0, len = list.length; i < len; i++) {
num = list[i];
results.push(math.cube(num));
}
return results;
})();

}).call(this);

缺省的 coffee 编译结果为了最大限度地保证不污染顶层的变量,总是会把文件编译成一个立即执行的函数,使得所有变量的声明和使用局限在一个小作用域里面。当然,它也有个 bare 模式可以去掉这种立即执行函数,如果使用importexport 的功能,也可以自动进入 bare 模式。这个模式的细节很繁琐,请读者自行查阅文档。

普通函数

1
2
3
4
5
6
7
8
9
square = (x) -> x * x
cube = (x) -> square(x) * x

# 带有默认值的函数参数
fill = (container, liquid = "coffee") ->
"Filling the #{container} with #{liquid}..."

# 空函数
a = ->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var cube, square;

square = function(x) {
return x * x;
};

cube = function(x) {
return square(x) * x;
};

var fill;

// 带有默认值的函数参数
fill = function(container, liquid = "coffee") {
return `Filling the ${container} with ${liquid}...`;
};

// 空函数
var a;

a = function() {};

使用->生成 function 函数。带默认值的参数,同样是 ES2015 的内容。

字符串

1
2
3
4
author = "Wittgenstein"
quote = "A picture is a fact. -- #{ author }"

sentence = "#{ 22 / 7 } is a decent approximation of π"
1
2
3
4
5
6
7
var author, quote, sentence;

author = "Wittgenstein";

quote = `A picture is a fact. -- ${author}`;

sentence = `${22 / 7} is a decent approximation of π`;

CoffeeScript 的字符串同 JavaScript 一样,可以用"'分界。用”引用的字符串,可以用#{} 来进行内插(甚至可以在对象的key 里执行内插)。用’引用的字符串是字面量。

双引号的多行字符串可以自动被编译器连接起来,当然,所有的缩进都失效了:

1
2
3
4
5
6
mobyDick = "Call me Ishmael. Some years ago --
never mind how long precisely -- having little
or no money in my purse, and nothing particular
to interest me on shore, I thought I would sail
about a little and see the watery part of the
world..."
1
2
3
var mobyDick;

mobyDick = "Call me Ishmael. Some years ago -- never mind how long precisely -- having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world...";

使用三引号"""和三个单引号''',还可以生成保留格式(特别是缩进)的字符串:

1
2
3
4
5
6
7
8
9
10
11
html = """
<strong>
cup of coffeescript
</strong>
"""

html = '''
<strong>
cup of coffeescript
</strong>
'''
1
2
3
4
5
6
7
var html;

html = "<strong>\n cup of coffeescript\n</strong>";

var html;

html = '<strong>\n cup of coffeescript\n</strong>';

用双引号制造出来的块字符串,也一样支持字符串内插功能。

对象和数组

CoffeeScript 支持 JavaScript 格式的对象和数组字面量,也支持更简明的无逗号的、依靠换行的字面量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
song = ["do", "re", "mi", "fa", "so"]

singers = {Jagger: "Rock", Elvis: "Roll"}

# 用换行来代替分隔符
bitlist = [
1, 0, 1
0, 0, 1
1, 1, 0
]

# 类 YAML 格式,用换行来代替分隔符
kids =
brother:
name: "Max"
age: 11
sister:
name: "Ida"
age: 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var bitlist, kids, singers, song;

song = ["do", "re", "mi", "fa", "so"];

singers = {
Jagger: "Rock",
Elvis: "Roll"
};

bitlist = [1, 0, 1, 0, 0, 1, 1, 1, 0];

kids = {
brother: {
name: "Max",
age: 11
},
sister: {
name: "Ida",
age: 9
}
};

CoffeeScript 也支持 ES2015的字面量语法:

1
2
3
4
name = "Michelangelo"
mask = "orange"
weapon = "nunchuks"
turtle = {name, mask, weapon}
1
2
3
4
5
6
7
8
9
var mask, name, output, turtle, weapon;

name = "Michelangelo";

mask = "orange";

weapon = "nunchuks";

turtle = {name, mask, weapon};

注释

聪明的读者可能已经发现了注释应该怎么写,但这里还是要总结下。CoffeeScript 里的注释其实是脚本语言的注释的应用:

1
2
3
4
5
6
7
###
Fortune Cookie Reader v1.0
Released under the MIT License
###

sayFortune = (fortune) ->
console.log fortune # in bed!
1
2
3
4
5
6
7
8
9
10
/*
Fortune Cookie Reader v1.0
Released under the MIT License
*/
var sayFortune;

sayFortune = function(fortune) {
return console.log(fortune); // in bed!
};

值得注意的是,###支持了类型注解。

词法作用域和变量安全

聪明的读者可能也发现了,在 CoffeeScript 中,是不需要手写var这样的关键字的。实际上,ES2015为了解决过去的 JavaScript 中不存在块级作用域这样的问题,专门提出的constlet解决方案,CoffeeScript 也不支持。在有多层变量的时候,CoffeeScript 会自动地推导变量的作用域,保证内层的变量绝不会污染任何外层变量。每个变量的实际作用域,会被限制在它首次被声明的地方。JavaScript 无意之中忘加var关键字而污染全局变量的情况便不复存在了。

1
2
3
4
5
outer = 1
changeNumbers = ->
inner = -1
outer = 10
inner = changeNumbers()
1
2
3
4
5
6
7
8
9
10
11
var changeNumbers, inner, outer;

outer = 1;

changeNumbers = function() {
var inner;
inner = -1;
return outer = 10;
};

inner = changeNumbers();

outer 因为在外部作用域里已经声明过了,所以不需要重复声明。而inner只是在内部作用域里被使用,所以还额外声明了一个函数内的 inner来专门隔离它的作用域。外部专门声明的var inner其实就是一个编译器为了谨慎做的声明顶部上推,在这个编译的例子里不影响任何语义。

因为无法使用var关键字,所以实际上我们是无法遮蔽(shadow)住外部变量的,只能在内部作用域引用外部变量。

If,Else,Unless, 与条件赋值

if/else 里可以不用写小括号和大括号,使用 python 式的缩进定界,用户也可以使用单行 if 和 unless。

1
2
3
4
5
6
7
8
9
10
mood = greatlyImproved if singing

if happy and knowsIt
# 用缩进来确定这一段代码是在条件块里的
clapsHands()
chaChaCha()
else
showIt()

date = if friday then sue else jill
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var date, mood;

if (singing) {
mood = greatlyImproved;
}

if (happy && knowsIt) {
clapsHands();
chaChaCha();
} else {
showIt();
}

date = friday ? sue : jill;

不定参数语法

Java 程序员里面可能都习惯了类似 … 语法的不定参数语法来了。在 CoffeeScript 中它被称为 Splats 参数。ES2015吸收了这一语法,做出了 rest 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
gold = silver = rest = "unknown"

awardMedals = (first, second, others...) ->
gold = first
silver = second
rest = others

contenders = [
"Michael Phelps"
"Liu Xiang"
"Yao Ming"
"Allyson Felix"
"Shawn Johnson"
"Roman Sebrle"
"Guo Jingjing"
"Tyson Gay"
"Asafa Powell"
"Usain Bolt"
]

awardMedals contenders...

alert """
Gold: #{gold}
Silver: #{silver}
The Field: #{rest.join ', '}
"""

# 特殊的数组省略语法
popular = ['pepperoni', 'sausage', 'cheese']
unwanted = ['anchovies', 'olives']

all = [popular..., unwanted..., 'mushrooms']

# 对象属性语法

user =
name: 'Werner Heisenberg'
occupation: 'theoretical physicist'

currentUser = { user..., status: 'Uncertain' }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var awardMedals, contenders, gold, rest, silver;

gold = silver = rest = "unknown";

awardMedals = function(first, second, ...others) {
gold = first;
silver = second;
return rest = others;
};

contenders = ["Michael Phelps", "Liu Xiang", "Yao Ming", "Allyson Felix", "Shawn Johnson", "Roman Sebrle", "Guo Jingjing", "Tyson Gay", "Asafa Powell", "Usain Bolt"];

awardMedals(...contenders);

alert(`Gold: ${gold}\nSilver: ${silver}\nThe Field: ${rest.join(', ')}`);

# 特殊的数组省略语法
var all, popular, unwanted;

popular = ['pepperoni', 'sausage', 'cheese'];

unwanted = ['anchovies', 'olives'];

all = [...popular, ...unwanted, 'mushrooms'];

// 特殊的对象属性语法
var currentUser, user,
_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

user = {
name: 'Werner Heisenberg',
occupation: 'theoretical physicist'
};

currentUser = _extends({}, user, {
status: 'Uncertain'
});

循环和表处理

CoffeeScript 的 for 循环可以处理数组、对象和 range,有点类似 Python。而且,它的 for 循环是有返回值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Eat lunch.
eat = (food) -> "#{food} eaten."
eat food for food in ['toast', 'cheese', 'wine']

# Fine five course dining.
courses = ['greens', 'caviar', 'truffles', 'roast', 'cake']
menu = (i, dish) -> "Menu Item #{i}: #{dish}"
menu i + 1, dish for dish, i in courses

# Health conscious meal.
foods = ['broccoli', 'spinach', 'chocolate']
eat food for food in foods when food isnt 'chocolate'

# 一个使用返回值的例子
countdown = (num for num in [10..1])

# 遍历对象的例子
yearsOld = max: 10, ida: 9, tim: 11

ages = for child, age of yearsOld
"#{child} is #{age}"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Eat lunch.
var courses, dish, eat, food, foods, i, j, k, l, len, len1, len2, menu, ref;

eat = function(food) {
return `${food} eaten.`;
};

ref = ['toast', 'cheese', 'wine'];
for (j = 0, len = ref.length; j < len; j++) {
food = ref[j];
eat(food);
}

// Fine five course dining.
courses = ['greens', 'caviar', 'truffles', 'roast', 'cake'];

menu = function(i, dish) {
return `Menu Item ${i}: ${dish}`;
};

for (i = k = 0, len1 = courses.length; k < len1; i = ++k) {
dish = courses[i];
menu(i + 1, dish);
}

// Health conscious meal.
foods = ['broccoli', 'spinach', 'chocolate'];

for (l = 0, len2 = foods.length; l < len2; l++) {
food = foods[l];
if (food !== 'chocolate') {
eat(food);
}
}

// 返回值的例子转化为 Javascript 变得如此之长,可以看到 CoffeeScript 的精炼
var countdown, num;

countdown = (function() {
var i, results;
results = [];
for (num = i = 10; i >= 1; num = --i) {
results.push(num);
}
return results;
})();

// 遍历对象的例子
var age, ages, child, yearsOld;

yearsOld = {
max: 10,
ida: 9,
tim: 11
};

ages = (function() {
var results;
results = [];
for (child in yearsOld) {
age = yearsOld[child];
results.push(`${child} is ${age}`);
}
return results;
})();

特别地,CoffeScript 提供一个低级的 while 循环,它同样是带有返回值的。

1
2
3
4
5
6
7
8
9
10
# Econ 101
if this.studyingEconomics
buy() while supply > demand
sell() until supply > demand

# Nursery Rhyme
num = 6
lyrics = while num -= 1
"#{num} little monkeys, jumping on the bed.
One fell out and bumped his head."
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Econ 101
var lyrics, num;

if (this.studyingEconomics) {
while (supply > demand) {
buy();
}
while (!(supply > demand)) {
sell();
}
}

// Nursery Rhyme
num = 6;

lyrics = (function() {
var results;
results = [];
while (num -= 1) {
results.push(`${num} little monkeys, jumping on the bed. One fell out and bumped his head.`);
}
return results;
})();

JavaScript 老手可能会很习惯匿名函数立即执行的用法,CoffeeScript 用 do 关键字支持把循环和匿名函数立即执行结合起来:

1
2
3
4
5
for filename in list
do (filename) ->
if filename not in ['.DS_Store', 'Thumbs.db', 'ehthumbs.db']
fs.readFile filename, (err, contents) ->
compile filename, contents.toString()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var filename, fn, i, len;

fn = function(filename) {
if (filename !== '.DS_Store' && filename !== 'Thumbs.db' && filename !== 'ehthumbs.db') {
return fs.readFile(filename, function(err, contents) {
return compile(filename, contents.toString());
});
}
};
for (i = 0, len = list.length; i < len; i++) {
filename = list[i];
fn(filename);
}

数组分片和 range 分片

CoffeeScript 同样以索引操作符的形式(类 Python 和 C++)支持对数组的 slicing。

1
2
3
4
5
6
7
8
9
10
11
12
13
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]

start = numbers[0..2]

middle = numbers[3...-2]

end = numbers[-2..]

copy = numbers[..]

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

numbers[3..6] = [-3, -4, -5, -6]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var copy, end, middle, numbers, start;

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

start = numbers.slice(0, 3);

middle = numbers.slice(3, -2);

end = numbers.slice(-2);

copy = numbers.slice(0);

var numbers, ref,
splice = [].splice;

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

splice.apply(numbers, [3, 4].concat(ref = [-3, -4, -5, -6])), ref;

一切皆表达式

表达式特别于普通语句的地方,就是它们总是可以求值的。
诸位读者可能已经注意到,CoffeeScript 中几乎没有 return。在函数中,最后一行表达式,就是整个块的表达式返回值,这点也是 CoffeeScript 从 Ruby 那里吸收来的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
grade = (student) ->
if student.excellentWork
"A+"
else if student.okayStuff
if student.triedHard then "B" else "B-"
else
"C"

eldest = if 24 > 21 then "Liz" else "Ike"

six = (one = 1) + (two = 2) + (three = 3)

# The first ten global properties.

globals = (name for name of window)[0...10]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var eldest, grade;

grade = function(student) {
if (student.excellentWork) {
return "A+";
} else if (student.okayStuff) {
if (student.triedHard) {
return "B";
} else {
return "B-";
}
} else {
return "C";
}
};

eldest = 24 > 21 ? "Liz" : "Ike";

var one, six, three, two;

six = (one = 1) + (two = 2) + (three = 3);

// The first ten global properties.
var globals, name;

globals = ((function() {
var results;
results = [];
for (name in window) {
results.push(name);
}
return results;
})()).slice(0, 10);

比较有意思的是,try/catch 也是可以有返回值的:

1
2
3
4
5
6
7
alert(
try
nonexistent / undefined
catch error
"And the error is ... #{error}"
)

1
2
3
4
5
6
7
8
9
10
var error;

alert((function() {
try {
return nonexistent / void 0;
} catch (error1) {
error = error1;
return `And the error is ... ${error}`;
}
})());

当然,有些语句在 JavaScript 中也是不能当做表达式的,比如breakcontinuereturn,如果你在代码块中使用了它们,CoffeeScript不会试图进行转换。

操作符与别名

==操作符经常引起出乎意料的、与其他语言中表现不一致的行为。所以 CoffeeScript 里没有==,而会试着进行再编译。把==编译成===!=编译成!==。另外,它还提供了可读性更好的两个操作符,is会被编译为===,isnt 会被编译为isnt

除此之外,还可以用not作取反操作符!的别名。

对于逻辑操作符,and会被编译为&&or会被便以为||

在 JavaScript 中,有时候条件语句需要另起一行或者在单行内用分号断句,但CoffeeScript 中的 then 就可以帮我们把条件语句(特别是 while、if、switch 的结构中)和被执行语句连起来。

YAML中一样,onyes是布尔值true的同义词,offno是布尔值false的同义词。

同其他脚本语言一样,unlessif的反写。

同 Ruby 一样,@property可以当做this.property来用(少写一个字符)。

可以用in来做数组元素的存在检查,用of来做 JavaScript 键值对的存在性检查。

for循环中,from关键字会被编译成 ES2015 的of

为了简化数学表达式,**代表幂运算,而//执行地板除法(同 Python 一样,去掉小数向下取整)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-7 % 5 == -2 # The remainder of 7 / 5
-7 %% 5 == 3 # n %% 5 is always between 0 and 4

tabs.selectTabAtIndex((tabs.currentIndex - count) %% tabs.length)

launch() if ignition is on

volume = 10 if band isnt SpinalTap

letTheWildRumpusBegin() unless answer is no

if car.speed < limit then accelerate()

winner = yes if pick in [47, 92, 13]

print inspect "My name is #{@name}"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var modulo = function(a, b) { return (+a % (b = +b) + b) % b; };

-7 % 5 === -2; // The remainder of 7 / 5

modulo(-7, 5) === 3; // n %% 5 is always between 0 and 4

tabs.selectTabAtIndex(modulo(tabs.currentIndex - count, tabs.length));

var volume, winner;

if (ignition === true) {
launch();
}

if (band !== SpinalTap) {
volume = 10;
}

if (answer !== false) {
letTheWildRumpusBegin();
}

if (car.speed < limit) {
accelerate();
}

if (pick === 47 || pick === 92 || pick === 13) {
winner = true;
}

print(inspect(`My name is ${this.name}`));

存在操作符

在 JavaScript 中确认一个变量是否存在是很困难的。因为if (variable) 不仅会在变量不存在时生效,在变量为0值(0,空字符串,false)时也会生效。CoffeeScript 的?操作符只在变量为nullundefined的时候返回false,这很像 Ruby 中的 nil?(实际上高版本的 ES 也会有一个叫 Null Propagation Operator的类似特性)。

这样我们就可以做更加安全的条件赋值了(比a = a || value安全)。

1
2
3
4
5
6
solipsism = true if mind? and not world?

speed = 0
speed ?= 15

footprints = yeti ? "bear"
1
2
3
4
5
6
7
8
9
10
11
var footprints, solipsism, speed;

if ((typeof mind !== "undefined" && mind !== null) && (typeof world === "undefined" || world === null)) {
solipsism = true;
}

speed = 0;

if (speed == null) {
speed = 15;
}

注意看,一个?被翻译成了对undefinednull的严格求不等!==
。但是,?unless搭配的时候,就会被翻译成==

1
2
3
4
major = 'Computer Science'

unless major?
signUpForClass 'Introduction to Wines'
1
2
3
4
5
6
7
var major;

major = 'Computer Science';

if (major == null) {
signUpForClass('Introduction to Wines');
}

可以用存在操作符来达到其他语言中经常出现的流利调用,流利调用失败用户会得到undefined而不会出现烦人的空指针(在 JavaScript 中实际上是不能在undefined上调用特定成员的TypeError)异常:

1
zip = lottery.drawWinner?().address?.zipcode
1
2
3
var ref, zip;

zip = typeof lottery.drawWinner === "function" ? (ref = lottery.drawWinner().address) != null ? ref.zipcode : void 0 : void 0;

无括号的链式调用

.和断行缩进可以像其他脚本语言一样进行无括号链式调用:

1
2
3
4
5
6
$ 'body'
.click (e) ->
$ '.box'
.fadeIn 'fast'
.addClass 'show'
.css 'background', 'white'
1
2
3
$('body').click(function(e) {
return $('.box').fadeIn('fast').addClass('show');
}).css('background', 'white');

解构赋值

用过 ES2015 以后版本的读者应该能够理解什么是解构赋值了:

1
2
3
4
theBait   = 1000
theSwitch = 0

[theBait, theSwitch] = [theSwitch, theBait]
1
2
3
4
5
6
7
var theBait, theSwitch;

theBait = 1000;

theSwitch = 0;

[theBait, theSwitch] = [theSwitch, theBait];

解构赋值搭配上多返回值的函数调用也很有用:

1
2
3
4
5
weatherReport = (location) ->
# Make an Ajax request to fetch the weather...
[location, 72, "Mostly Sunny"]

[city, temp, forecast] = weatherReport "Berkeley, CA"
1
2
3
4
5
6
7
8
var city, forecast, temp, weatherReport;

weatherReport = function(location) {
// Make an Ajax request to fetch the weather...
return [location, 72, "Mostly Sunny"];
};

[city, temp, forecast] = weatherReport("Berkeley, CA");

解构赋值对任意深度嵌套的数组和对象也是一个有用的特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
futurists =
sculptor: "Umberto Boccioni"
painter: "Vladimir Burliuk"
poet:
name: "F.T. Marinetti"
address: [
"Via Roma 42R"
"Bellagio, Italy 22021"
]

{sculptor} = futurists

{poet: {name, address: [street, city]}} = futurists
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var city, futurists, name, sculptor, street;

futurists = {
sculptor: "Umberto Boccioni",
painter: "Vladimir Burliuk",
poet: {
name: "F.T. Marinetti",
address: ["Via Roma 42R", "Bellagio, Italy 22021"]
}
};

({sculptor} = futurists);

({
poet: {
name,
address: [street, city]
}
} = futurists);

解构赋值也可以搭配 splats 不定参数使用:

1
2
3
tag = "<impossible>"

[open, contents..., close] = tag.split("")
1
2
3
4
5
6
var close, contents, i, open, ref, tag,
slice = [].slice;

tag = "<impossible>";

ref = tag.split(""), open = ref[0], contents = 3 <= ref.length ? slice.call(ref, 1, i = ref.length - 1) : (i = 1, []), close = ref[i++];

取数组末尾元素的例子:

1
2
3
4
text = "Every literary critic believes he will
outwit history and have the last word"

[first, ..., last] = text.split " "
1
2
3
4
5
var first, last, ref, text;

text = "Every literary critic believes he will outwit history and have the last word";

ref = text.split(" "), first = ref[0], last = ref[ref.length - 1];

与构造函数结合的例子:

1
2
3
4
5
class Person
constructor: (options) ->
{@name, @age, @height = 'average'} = options

tim = new Person name: 'Tim', age: 4
1
2
3
4
5
6
7
8
9
10
11
12
13
var Person, tim;

Person = class Person {
constructor(options) {
({name: this.name, age: this.age, height: this.height = 'average'} = options);
}

};

tim = new Person({
name: 'Tim',
age: 4
});

绑定(胖)函数

->定义的函数会直接转化为普通的function

JavaScript 中this指向的值可以被动态绑定(这真是由来已久的弊病)。有经验的读者肯定能理解,我们在传递回调的时候, 原始this经常会丢失,变成新作用域里的this

而使用胖箭头=>定义的函数,可以把当前上下文的this绑定到函数里(好像调用了一个隐式的bind一样)。当我们在 Prototype 和 JQuery 之类的毁掉库里使用函数时,这回变得非常有用。

1
2
3
4
5
6
7
8
Account = (customer, cart) ->
@customer = customer
@cart = cart

# 如果我们这里使用->箭头,就会编译出 function,function 的 this 是不绑定在当前的 this 上的。
$('.shopping_cart').on 'click', (event) =>
# 此处的@customer和外部的@customer一致。
@customer.purchase @cart
1
2
3
4
5
6
7
8
9
10
var Account;

Account = function(customer, cart) {
this.customer = customer;
this.cart = cart;
// JavaScript 中的=>实际上使 this 进入外部上下文的了词法作用域
return $('.shopping_cart').on('click', (event) => {
return this.customer.purchase(this.cart);
});
};

如果我们在这里使用->,函数里的@customer实际上就会指向undefined。因为$(‘.shopping_cart’) 这个东西指向的是 dom 变量,很可能没有customer这个属性。

胖函数是 CoffeeScript 里最受欢迎的特性,也被 ES2015 吸收了,使用 CoffeeScript 中的=>就会被编译成JavaScript 中的=>

生成器函数

CoffeeScript 通过yield关键字支持 ES2015中的generator functions。CoffeeScript 中没有function*(){}这样的无意义结构,只要有yield就够了。

1
2
3
4
5
6
7
8
perfectSquares = ->
num = 0
loop
num += 1
yield num * num
return

window.ps or= perfectSquares()
1
2
3
4
5
6
7
8
9
10
var perfectSquares;

perfectSquares = function*() {
var num;
num = 0;
while (true) {
num += 1;
yield num * num;
}
};

yield当然也可以配合for...from使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
fibonacci = ->
[previous, current] = [1, 1]
loop
[previous, current] = [current, previous + current]
yield current
return

getFibonacciNumbers = (length) ->
results = [1]
for n from fibonacci()
results.push n
break if results.length is length
results
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var fibonacci, getFibonacciNumbers;

fibonacci = function*() {
var current, previous;
[previous, current] = [1, 1];
while (true) {
[previous, current] = [current, previous + current];
yield current;
}
};

getFibonacciNumbers = function(length) {
var n, ref, results;
results = [1];
ref = fibonacci();
for (n of ref) {
results.push(n);
if (results.length === length) {
break;
}
}
return results;
};

异步函数

ES2017 的异步函数是通过await支持的。同生成器函数一样,CoffeeScript 中的不需要async关键字,只要有await就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Your browser must support async/await and speech synthesis
# to run this example.

sleep = (ms) ->
new Promise (resolve) ->
window.setTimeout resolve, ms

say = (text) ->
window.speechSynthesis.cancel()
window.speechSynthesis.speak new SpeechSynthesisUtterance text

countdown = (seconds) ->
for i in [seconds..1]
say i
await sleep 1000 # wait one second
say "Blastoff!"

countdown 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Your browser must support async/await and speech synthesis
// to run this example.
var countdown, say, sleep;

sleep = function(ms) {
return new Promise(function(resolve) {
return window.setTimeout(resolve, ms);
});
};

say = function(text) {
window.speechSynthesis.cancel();
return window.speechSynthesis.speak(new SpeechSynthesisUtterance(text));
};

countdown = async function(seconds) {
var i, j, ref;
for (i = j = ref = seconds; ref <= 1 ? j <= 1 : j >= 1; i = ref <= 1 ? ++j : --j) {
say(i);
await sleep(1000); // wait one second
}
return say("Blastoff!");
};

countdown(3);

CoffeeScript 1 就提供了classextends关键字,作为原型相关函数的语法糖。ES2015吸收了这两个关键字,CoffeeScript 2 直接把这两个关键字编译成 ES2015的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal
constructor: (@name) ->

move: (meters) ->
alert @name + " moved #{meters}m."

class Snake extends Animal
move: ->
alert "Slithering..."
super 5

class Horse extends Animal
move: ->
alert "Galloping..."
super 45

sam = new Snake "Sammy the Python"
tom = new Horse "Tommy the Palomino"

sam.move()
tom.move()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var Animal, Horse, Snake, sam, tom;

Animal = class Animal {
constructor(name) {
this.name = name;
}

move(meters) {
return alert(this.name + ` moved ${meters}m.`);
}

};

Snake = class Snake extends Animal {
move() {
alert("Slithering...");
return super.move(5);
}

};

Horse = class Horse extends Animal {
move() {
alert("Galloping...");
return super.move(45);
}

};

sam = new Snake("Sammy the Python");

tom = new Horse("Tommy the Palomino");

sam.move();

tom.move();

可以用@开头的函数来定义静态方法:

1
2
3
4
5
6
7
8
9
class Teenager
@say: (speech) ->
words = speech.split ' '
fillers = ['uh', 'um', 'like', 'actually', 'so', 'maybe']
output = []
for word, index in words
output.push word
output.push fillers[Math.floor(Math.random() * fillers.length)] unless index is words.length - 1
output.join ', '
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Teenager;

Teenager = class Teenager {
static say(speech) {
var fillers, i, index, len, output, word, words;
words = speech.split(' ');
fillers = ['uh', 'um', 'like', 'actually', 'so', 'maybe'];
output = [];
for (index = i = 0, len = words.length; i < len; index = ++i) {
word = words[index];
output.push(word);
if (index !== words.length - 1) {
output.push(fillers[Math.floor(Math.random() * fillers.length)]);
}
}
return output.join(', ');
}

};

原型式继承

ES2015提供了一个短路操作符给原型链相关的操作:

1
2
String::dasherize = ->
this.replace /_/g, "-"
1
2
3
String.prototype.dasherize = function() {
return this.replace(/_/g, "-");
};

Switch/When/Else

CoffeeScript 中的switch不用担心忘记写break出现错误的跳转:

1
2
3
4
5
6
7
8
9
10
switch day
when "Mon" then go work
when "Tue" then go relax
when "Thu" then go iceFishing
when "Fri", "Sat"
if day is bingoDay
go bingo
go dancing
when "Sun" then go church
else go work
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
switch (day) {
case "Mon":
go(work);
break;
case "Tue":
go(relax);
break;
case "Thu":
go(iceFishing);
break;
case "Fri":
case "Sat":
if (day === bingoDay) {
go(bingo);
go(dancing);
}
break;
case "Sun":
go(church);
break;
default:
go(work);
}

在之前读者已经看到了 CoffeeScript 中大量的语句都可以带有返回值,switch也不例外。以下的例子里面,switch 后面连表达式都没有:

1
2
3
4
5
6
7
8
score = 76
grade = switch
when score < 60 then 'F'
when score < 70 then 'D'
when score < 80 then 'C'
when score < 90 then 'B'
else 'A'
# grade == 'C'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var grade, score;

score = 76;

grade = (function() {
switch (false) {
case !(score < 60):
return 'F';
case !(score < 70):
return 'D';
case !(score < 80):
return 'C';
case !(score < 90):
return 'B';
default:
return 'A';
}
})();

// grade == 'C'

Try/Catch/Finally

CoffeeScript 中的try/catch/finally块可以完全不带大括号:

1
2
3
4
5
6
7
try
allHellBreaksLoose()
catsAndDogsLivingTogether()
catch error
print error
finally
cleanUp()
1
2
3
4
5
6
7
8
9
10
11
var error;

try {
allHellBreaksLoose();
catsAndDogsLivingTogether();
} catch (error1) {
error = error1;
print(error);
} finally {
cleanUp();
}

链式比较

CoffeeScript 从 Python 中借来了链式比较,这样范围比较的语句就变得更加简单了。

1
2
3
cholesterol = 127

healthy = 200 > cholesterol > 60
1
2
3
4
5
var cholesterol, healthy;

cholesterol = 127;

healthy = (200 > cholesterol && cholesterol > 60);

块状的正则表达式

我们已经看到了块状(多行)字符串和注释,CoffeeScript 同样支持块状正则表达式。模仿自 Perl 的/x修饰符,CoffeeScript 使用///来对多行表达式定界,这样多行的正则比单行的正则就更加可读了:

1
2
3
4
5
6
NUMBER     = ///
^ 0b[01]+ | # binary
^ 0o[0-7]+ | # octal
^ 0x[\da-f]+ | # hex
^ \d*\.?\d+ (?:e[+-]?\d+)? # decimal
///i
1
2
3
4
5
6
var NUMBER;

NUMBER = /^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i; // binary
// octal
// hex
// decimal

标签化的模板字面量

CoffeeScript 支持 ES2015中的标签化模板字面量,提供了自定义的字符串内插的能力。

1
2
3
4
5
6
7
8
upperCaseExpr = (textParts, expressions...) ->
textParts.reduce (text, textPart, i) ->
text + expressions[i - 1].toUpperCase() + textPart

greet = (name, adjective) ->
upperCaseExpr"""
Hi #{name}. You look #{adjective}!
"""
1
2
3
4
5
6
7
8
9
10
11
var greet, upperCaseExpr;

upperCaseExpr = function(textParts, ...expressions) {
return textParts.reduce(function(text, textPart, i) {
return text + expressions[i - 1].toUpperCase() + textPart;
});
};

greet = function(name, adjective) {
return upperCaseExpr`Hi ${name}. You look ${adjective}!`;
};

模块

CoffeeScript 中的模块和 ES2015中的模块很相似,都是importexport语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'local-file.coffee'
import 'coffeescript'

import _ from 'underscore'
import * as underscore from 'underscore'

import { now } from 'underscore'
import { now as currentTimestamp } from 'underscore'
import { first, last } from 'underscore'
import utilityBelt, { each } from 'underscore'

export default Math
export square = (x) -> x * x
export class Mathematics
least: (x, y) -> if x < y then x else y

export { sqrt }
export { sqrt as squareRoot }
export { Mathematics as default, sqrt as squareRoot }

export * from 'underscore'
export { max, min } from 'underscore'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import 'local-file.coffee';

import 'coffeescript';

import _ from 'underscore';

import * as underscore from 'underscore';

import {
now
} from 'underscore';

import {
now as currentTimestamp
} from 'underscore';

import {
first,
last
} from 'underscore';

import utilityBelt, {
each
} from 'underscore';

export default Math;

export var square = function(x) {
return x * x;
};

export var Mathematics = class Mathematics {
least(x, y) {
if (x < y) {
return x;
} else {
return y;
}
}

};

export {
sqrt
};

export {
sqrt as squareRoot
};

export {
Mathematics as default,
sqrt as squareRoot
};

export * from 'underscore';

export {
max,
min
} from 'underscore';

嵌入式 JavaScript

在 CoffeeScript 中可以用倒引号(`)直接引用 JavaScript 代码:

1
2
3
4
5
6
7
8
hi = `function() {
return [document.title, "Hello JavaScript"].join(": ");
}`

# \`会被编译成`,\\\`会被编译成\`
markdown = `function () {
return \`In Markdown, write code like \\\`this\\\`\`;
}`
1
2
3
4
5
6
7
8
9
10
11
var hi;

hi = function() {
return [document.title, "Hello JavaScript"].join(": ");
};

var markdown;

markdown = function () {
return `In Markdown, write code like \`this\``;
};

用三个倒引号可以很轻松地引用一大块 JavaScript 代码:

1
2
3
4
5
\```
function time() {
return `The time is ${new Date().toLocaleTimeString()}`;
}
\```
1
2
3
4
function time() {
return `The time is ${new Date().toLocaleTimeString()}`;
}
;

JSX

CoffeeScript 不需要专门的插件或者设置,就可以理解 JSX。

同普通的 JSX 一样,<>指明了XML 元素,由 {}包围的代码会被内插替换。为了避免和大于、小于的比较混淆,类似i < len的代码必须带空格,不能写作i<len。编译器有时候能够在你忘记加空格的时候,猜到你的意图,但不要心怀侥幸,还是要尽量自己加空格。

1
2
3
4
5
6
7
8
9
renderStarRating = ({ rating, maxStars }) ->
<aside title={"Rating: #{rating} of #{maxStars} stars"}>
{for wholeStar in [0...Math.floor(rating)]
<Star className="wholeStar" key={wholeStar} />}
{if rating % 1 isnt 0
<Star className="halfStar" />}
{for emptyStar in [Math.ceil(rating)...maxStars]
<Star className="emptyStar" key={emptyStar} />}
</aside>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var renderStarRating;

renderStarRating = function({rating, maxStars}) {
var emptyStar, wholeStar;
return <aside title={`Rating: ${rating} of ${maxStars} stars`}>
{(function() {
var i, ref, results;
results = [];
for (wholeStar = i = 0, ref = Math.floor(rating); 0 <= ref ? i < ref : i > ref; wholeStar = 0 <= ref ? ++i : --i) {
results.push(<Star className="wholeStar" key={wholeStar} />);
}
return results;
})()}
{(rating % 1 !== 0 ? <Star className="halfStar" /> : void 0)}
{(function() {
var i, ref, ref1, results;
results = [];
for (emptyStar = i = ref = Math.ceil(rating), ref1 = maxStars; ref <= ref1 ? i < ref1 : i > ref1; emptyStar = ref <= ref1 ? ++i : --i) {
results.push(<Star className="emptyStar" key={emptyStar} />);
}
return results;
})()}
</aside>;
};

类型注解

Flow 风格的注释式类型注解可以在 CoffeeScript 中提供静态类型检查的能力(TypeScript 的粉丝可能要头痛了):

1
2
3
4
5
6
7
8
9
10
11
# @flow

###::
type Obj = {
num: number,
};
###

fn = (str ###: string ###, obj ###: Obj ###) ###: string ### ->
str + obj.num

1
2
3
4
5
6
7
8
9
10
11
// @flow
/*::
type Obj = {
num: number,
};
*/
var fn;

fn = function(str/*: string */, obj/*: Obj */)/*: string */ {
return str + obj.num;
};

如果用户已经成功安装了 Flow最新的 CoffeeScript,可以用如下命令进行类型检查:

1
coffee --bare --no-header --compile app.coffee && npm run flow

--bare --no-header非常重要,因为 Flow 要求文件中的第一行是// @flow注释。