本文译自The Little Book on CoffeeScript的第2章。
ref:http://article.yeeyan.org/view/260164/251746
首先,在开始本章之前,我还想重申下尽管很多时候CoffeeScript的语法与JavaScript相似,但是它并不是JavaScript的超集,因此,例如function
和var
这类JavaScript关键字并不允许在CoffeeScript中使用。如果你正在编写CoffeeScript文件,里面必须完全是纯CoffeeScript代码,你不能把这两种语言揉到一起。
为什么CoffeeScript不是超集?阻止其成为超集最直接的原因是在CoffeeScript程序中空格是有意义的。而且,既然已经这么决定了,开发团队也帮你一干到底,以精简的名字代替JavaScript的一些关键字和特性,还为避免很多常见的bug而努力。
让我极度兴奋的是,从元的角度上来说,CoffeeScript的解释器实际上就是由CoffeeScript写成的。这看起来似乎解决了先有鸡还是先有蛋的悖论!
好了,让我们从最基本的工作开始。CoffeeScript去掉了分号,它会在编译时为你自动添加。分号在JavaScript社区中引起了大量的争论,以及背后的一些解释器怪异的行为。总之,CoffeeScript为了帮你解决这个问题,简单地从语法上的移除了分号,然后在幕后更具需要添加。
注释格式与Ruby的一致,以一个哈希字符开头。
# A comment
也支持多行注释,而且还会把多行注释添加到生成的JavaScript中。使用三个哈希字符包裹即可。
###
A multiline comment, perhaps a LICENSE.
###
/*
A multiline comment, perhaps a LICENSE.
*/
(function() {
}).call(this);
正如我简单的提过,CoffeeScript对空格是敏感的。实际说来,就是你可以使用制表符来替换花括号({})。这受到了Python语法的影响,而且还能确保你的脚本有一个清晰的格式,否则连编译都通不过。
变量与作用域
CoffeeScript修复了JavaScript中一个最让人头疼的问题——全局变量。在JavaScript中,一不小心的话,就很容易在定义变量时遗漏var
关键字导致产生全局变量。CoffeeScript通过简单的剔除全局变量来解决这个问题。在背后,CoffeeScript使用一个匿名函数把所有脚本都包裹起来,将其限定在局部作用域中,并且为所有的变量赋值前自动添加var
。比如,下面是在CoffeeScript中简单的定义一个变量:
myVariable = "test"
(function() {
var myVariable;
myVariable = "test";
}).call(this);
注意示例代码右上角的深灰色小方块。单击它,代码就会在CoffeeScript和编译后的JavaScript之间来回切换。这是在页面加载是输出的,所以你放心,编译结果是准确的。
如你所见的那样,变量赋值被限定在局部作用域中,不小心创建全局变量是不可能的。CoffeeScript还更进了一步,让覆盖一个高一级的变量也很困难。这大量的减少了程序员会在JavaScript中犯的常见的错误。
然而,有时候全局变量还是有用的。你可以通过直接给全局对象(浏览器中的window
)赋值来获得全局变量,也可以通过下面这种模式。
exports = this
exports.MyVariable = "foo-bar"
(function() {
var exports;
exports = this;
exports.MyVariable = "foo-bar";
}).call(this);
在顶级作用域中,this
就相当于全局对象,你可以创建一个局部变量exports
让阅读你代码的人能够分清楚哪个是脚本创建的全局变量。而且,这还能为支持CommonJS模块铺平了道路,这在本书的后面会做介绍。
函数
CoffeeScript移除了冗长的function
语句,以瘦箭头->
替之。函数可以是一行也可以是多行。函数的最后一个表达式会作为隐式的返回值。换句话说,你不再需要使用return
关键字,除非你想早一点从函数中返回。
记住这点,让我们看一个例子:
func = -> "bar"
(function() {
var func;
func = function() {
return "bar";
};
}).call(this);
结合着编译后的JavaScript你会发现,->
被转成了一个function
表达式,并且"bar"
被自动的返回了。
前面也说了,没有理由阻止我们使用多行的函数,只需要适当地缩进函数体即可:
func = ->
# An extra line
"bar"
函数参数
如何指定参数?CoffeeScript允许你通过在箭头前面的括号中指定参数。
times = (a, b) -> a * b
CoffeeScript还支持默认参数,例如:
times = (a = 1, b = 2) -> a * b
(function() {
var times;
times = function(a, b) {
if (a == null) {
a = 1;
}
if (b == null) {
b = 2;
}
return a * b;
};
}).call(this);
你还可以使用参数槽(splats)接收多个参数,使用...
表示:
sum = (nums...) ->
result = 0
nums.forEach (n) -> result += n
result
(function() {
var sum,
__slice = [].slice;
sum = function() {
var nums, result;
nums = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
result = 0;
nums.forEach(function(n) {
return result += n;
});
return result;
};
}).call(this);
在上面的例子中,nums
是一个包含传递给函数全部参数的数组。它不是一个arugments
对象,而是一个真实的数组对象,这样的话在你想操作它的时候就不需要先使用Array.prototype.splice
或者jQuery.makeArray()
了。
trigger = (events...) ->
events.splice(1, 0, this)
this.constructor.trigger.apply(events)
函数调用
在JavaScript中,可以通过括弧()
、apply()
和call()
来调用函数。然而,像Ruby一样,如果函数被至少一个参数跟着的话,CoffeeScript会自动的调用这个函数。
a = "Howdy!"
alert a
# Equivalent to:
alert(a)
alert inspect a
# Equivalent to:
alert(inspect(a))
尽管括号不是必须的,但是在难以分清谁是被调用的函数哪些是参数时,我推荐还是用上括号。上一个inspect
的示例中,我真心建议你至少使给inspect
的调用加上括号。
alert inspect(a)
如果在调用一个函数时你没有传递参数,CoffeeScript就没有办法判断出你打算调用这个函数,还是只是把它当作一个变。从这点来看,CoffeeScript的行为与Ruby有些差异,后者总是会调用引用函数的变量,CoffeeScript更像Python。这已经变成了我的CoffeeScript程序中常见的错误。因此,在你打算无参数调用函数时多留个心眼,别忘了加上括号。
函数上下文
在JavaScript上下文会频繁的变化。尤其是在回调函数中,CoffeeScript为此提供了一些辅助。其中之一就是->
的变种胖箭头的函数=>
使用胖箭头代替普通箭头是为了确保函数的上下文可以绑定为当前的上下文。例如:
this.clickHandler = -> alert "clicked"
element.addEventListener "click", (e) => this.clickHandler(e)
(function() {
var _this = this;
this.clickHandler = function() {
return alert("clicked");
};
element.addEventListener("click", function(e) {
return _this.clickHandler(e);
});
}).call(this);
你之所以要这样做的原因是,来自addEventListener
的回调函数会以element
为上下文被调用,也就是说,this
就相当于这个元素。如果你想让this
等于当前上下文,除了使用self=this
,胖箭头也是一种方式。
这中绑定的思想与jQuery的 proxy()
或者ES5's的bind()
函数是类似的概念。
对象字面量与数组定义
就如在JavaScript中一样,可以使用一对大括号以及键/值来明确定义对象字面量。然而,与函数调用类似,CoffeeScript使得可以省略括号。事实上,你还可以使用缩进和换行来代替起分割作用的逗号。
object1 = {one: 1, two: 2}
# Without braces
object2 = one: 1, two: 2
# Using new lines instead of commas
object3 =
one: 1
two: 2
User.create(name: "John Smith")
同样的,数组可以使用空格来代替分隔作用的逗号,但是方括号([]
)还是需要的。
array1 = [1, 2, 3]
array2 = [
1
2
3
]
array3 = [1,2,3,]
像你在上例看到的那样,CoffeeScript还能去掉array3
末尾多余的逗号,这也是一个常见的跨浏览器错误源。
流程控制
这种可省略括号的便捷方式延续到了CoffeeScript中的if
和else
关键字。
if true == true
"We're ok"
if true != true then "Panic"
# Equivalent to:
# (1 > 0) ? "Ok" : "Y2K!"
if 1 > 0 then "Ok" else "Y2K!"
(function() {
if (true === true) {
"We're ok";
}
}).call(this);
如你所见,在单行的if
语句中,你需要使用then
关键字,这样CoffeeScirpt才能明白执行体从什么地方开始。CoffeeScript并不支持条件运算符,作为替代你应该使用单行的if/else
语句。
CoffeeScript还支持一项Ruby的特性,即运行在if
语句前使用前缀表达式。
alert "It's cold!" if heat < 5
你还可以使用not
关键字来代替感叹号(!
)来做取反操作。由于很容易错过感叹号,这在某些时候能让你的代码有更强的可读性。
if not true then "Panic"
在上面的例子中,我们还可使使用CoffeeScript的unless
关键字,即if
的否定。
unless true
"Panic"
与not
类似的风格,CoffeeScript还为大家提供了is
语句,编译过去就是===
。
if true is 1
"Type coercion fail!"
你可以使用isnt
代替is not
。
if true isnt true
alert "Opposite day!"
在上面的例子中你可以已经注意到了,CoffeeScript会把==
操作符转化为===
,把!=
转化为!==
。这是这门语言中我最喜欢的一个特性,也是最简单的一个。那这背后有什么原因呢?坦白的讲JavaScript强制的类型转换有点奇怪,并且等于操作符为了比较它们会强制类型转换,这会导致很多令人迷惑的行为和很多的bug。在第7章中还是对此有有更多的讨论。
字符串插值法
CoffeeScript将Ruby风格的字符串插值法引入到了JavaScript中。在双引号的字符串中可以包含#{}
标记,这些标记中可以包含被插入到字符串中的表达式。
favourite_color = "Blue. No, yel..."
question = "Bridgekeeper: What... is your favourite color?
Galahad: #{favourite_color}
Bridgekeeper: Wrong!
"
就上例所示,多行字符串是允许的,不需要在没一行前添加+
。
循环和列表解析
JavaScript中的数组迭代使用一种相当古老的语法,看上去更像一个类似于C之类的老语言,而不是现代的面向对象的语言。ES5引入forEach()
函数来稍微改善了下这种情况,但是这样的话每次迭代都需要调用一次函数,因此运行速度会变慢。再一次,CoffeeScript给出一种漂亮的语法拯救了我们:
for name in ["Roger", "Roderick", "Brian"]
alert "Release #{name}"
(function() {
var name, _i, _len, _ref;
_ref = ["Roger", "Roderick", "Brian"];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
name = _ref[_i];
alert("Release " + name);
}
}).call(this);
如果你需要知道当前迭代索引的话,只需要再多传一个参数:
for name, i in ["Roger the pickpocket", "Roderick the robber"]
alert "#{i} - Release #{name}"
使用前缀的形式你可以一行代码完成迭代。
release prisoner for prisoner in ["Roger", "Roderick", "Brian"]
(function() {
var prisoner, _i, _len, _ref;
_ref = ["Roger", "Roderick", "Brian"];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
prisoner = _ref[_i];
release(prisoner);
}
}).call(this);
就如Python的推导式一样,你可以过滤它们:
prisoners = ["Roger", "Roderick", "Brian"]
release prisoner for prisoner in prisoners when prisoner[0] is "R"
(function() {
var prisoner, prisoners, _i, _len;
prisoners = ["Roger", "Roderick", "Brian"];
for (_i = 0, _len = prisoners.length; _i < _len; _i++) {
prisoner = prisoners[_i];
if (prisoner[0] === "R") {
release(prisoner);
}
}
}).call(this);
你可以使用推导式来迭代对象的全部属性,不过要使用of
代替in
关键字。
names = sam: seaborn, donna: moss
alert("#{first} #{last}") for first, last of names
(function() {
var first, last, names;
names = {
sam: seaborn,
donna: moss
};
for (first in names) {
last = names[first];
alert("" + first + " " + last);
}
}).call(this);
唯一CoffeeScript暴露出来的底层循环语法是while
循环。它与原JavaScript中while
循环的行为差不多,只是包含了已添加的优点,它能返回一个结果数组。看起来像Array.prototype.map()
函数。
num = 6
minstrel = while num -= 1
num + " Brave Sir Robin ran away"
(function() {
var minstrel, num;
num = 6;
minstrel = (function() {
var _results;
_results = [];
while (num -= 1) {
_results.push(num + " Brave Sir Robin ran away");
}
return _results;
})();
}).call(this);
数组
说到使用区间来分割数组,CoffeeScript是受到了Ruby的影响。使用两个数字来定义区间,分别代表区间的第一个和最后一个位置。这两个数字之间使用..
或...
来分隔。如果区间之前没有任何东西,CoffeeScript会将其转换为一个数组。
range = [1..5]
(function() {
var range;
range = [1, 2, 3, 4, 5];
}).call(this);
然而,如果区间被指定到一个变量之后,CoffeeScript则会将其转换为一个slice()
调用。
firstTwo = ["one", "two", "three"][0..1]
(function() {
var firstTwo;
firstTwo = ["one", "two", "three"].slice(0, 2);
}).call(this);
在上面的例子中,区间会返回一个只包含原始数组的前两个元素的新的字符串。你也可以使用同样的语法来把数组中的某个片段替换为其他的数组。
numbers = [0..9]
numbers[3..5] = [-3, -4, -5]
(function() {
var numbers, _ref;
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
[].splice.apply(numbers, [3, 3].concat(_ref = [-3, -4, -5])), _ref;
}).call(this);
更棒的是,JavaScript还能让你在字符串上调用slice()
,因此你可以在字符串上使用区间来获得一个新的子字符串。
my = "my string"[0..2]
在JavaScript中检测数组中是否存在某个值是一件麻烦事,特别是indexOf()
并不是所有的浏览器都支持(IE,我说的就是你!)。CoffeeScript使用in
操作符来解决这个问题,例如:
words = ["rattled", "roudy", "rebbles", "ranks"]
alert "Stop wagging me" if "ranks" in words
别名和存在操作符
CoffeeScript采用了一些有用的别名来减少输入量。其中一个就是@
,是this
的别名。
@saviour = true
另外一个是::
,prototype
的别名。
User::first = -> @records[0]
User.prototype.first = function() {
return this.records[0];
};
在JavaScript中使用if
来做null
检查是很常见的,但是其中有几个陷阱,空字符串和零都被强制转化为false
,这往往会让你犯错。CoffeeScript存在操作符?
只会在变量为null
或者undefined
的时候会返回真,与Ruby的nil?
类似。
praise if brian?
if (typeof brian !== "undefined" && brian !== null) {
praise;
}
你还能用它来替换||
操作符:
velocity = southern ? 40
velocity = typeof southern !== "undefined" && southern !== null ? southern : 40;
如果你在访问属性之前进行null
检查,你可以把存在操作符放在它左边来跳过检查。这与Actice Support的try方法比较类似。
blackKnight.getLegs()?.kick()
var _ref;
if ((_ref = blackKnight.getLegs()) != null) {
_ref.kick();
}
你能够用同样的方法检查一个属性是否是函数,是否可以调用,把存在操作符放在括号之前就行。如果属性不存在,或者不是一个函数,则就不会被调用。
blackKnight.getLegs().kick?()