(六)我的JavaScript系列:更好的JavaScript之CoffeeScript
世界上的很多天才都在为构建更好的JavaScript而努力。已经有了很多尝试,其中最有前途的,无非就是CoffeeScript和TypeScript了。面对CoffeeScript,我有一见如故的感觉;而TypeScript也激发了我极大的兴趣。CoffeeScript和TypeScript一样,都是编译为JavaScript的语言,它们都增强了JavaScript的表达能力。这篇文章是讲CoffeeScript的,TypeScript将放在下一篇再讲。
所谓编译为JavaScript,是指CoffeeScript和TypeScript没有实现自己的运行时,它们都是编译为等价的JavaScript代码,然后放在JavaScript的解释器上运行。
CoffeeScript
简洁性
CoffeeScript给人最大的印象就是其简洁的表达。下面代码是我从CoffeeScript中文摘抄下来的:
# 赋值:
number = 42
opposite = true
# 条件:
number = -42 if opposite
# 函数:
square = (x) -> x * x
# 数组:
list = [1, 2, 3, 4, 5]
# 对象:
math =
root: Math.sqrt
square: square
cube: (x) -> x * square x
# Splats:
race = (winner, runners...) ->
print winner, runners
# 存在性:
alert "I knew it!" if elvis?
# 数组 推导(comprehensions):
cubes = (math.cube num for num in list)
上面的代码会编译为等价的JavaScript代码:
var cubes, list, math, num, number, opposite, race, square,
__slice = [].slice;
number = 42;
opposite = true;
if (opposite) {
number = -42;
}
square = function(x) {
return x * x;
};
list = [1, 2, 3, 4, 5];
math = {
root: Math.sqrt,
square: square,
cube: function(x) {
return x * square(x);
}
};
race = function() {
var runners, winner;
winner = arguments[0], runners = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
return print(winner, runners);
};
if (typeof elvis !== "undefined" && elvis !== null) {
alert("I knew it!");
}
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;
})();
run: cubes
CoffeeScript力求简洁。其简洁性首先表现在对一些仅用于语法控制的符号进行了去除。这其中包括:
-
取消分号
-
取消
var
声明 -
取消大括号包围内层代码,使用缩进取代
-
函数调用在没有歧义的情况下可以省略括号
var
声明涉及到复杂又很鸡肋的JavaScript变量作用域机制。这部分内容先放着不讲。CoffeeScript通过完全取消var
声明机制而使得问题得到简化。总之,在CoffeeScript世界里,变量不用事先声明,直接用就是了。而且这种用法基本没有什么危险。
缩进在CoffeeScript中不仅仅在于美化代码,其代表了代码层次的组织,是有特别的含义的。简单地说就是,不适用大括号包围内层代码,而是内层代码要缩进。不同的缩进代表了不同的代码层次。形式和内容是一致的。
缩进的例子:
#if缩进
if true
'true'
else
'false'
#while缩进
while true
'true'
#函数缩进
(n) ->
n * n
#对象字面量缩进
kids =
brother:
name: "Max"
age: 11
sister:
name: "Ida"
age: 9
在不引起歧义的情况下,CoffeeScript的函数调用可以省略括号。例如console.log(object)
可以简化为console.log object
。所谓引起歧义的一个例子就是无参数的情况下,console.log
就不知道是取出函数型属性log
还是调用函数log
了。
CoffeeScript的函数表达式也做了极致的精简精简。一个单行函数的定义可以这样:
square = (x) -> x * x
而多行函数也是通过缩进来组织的。一个空的函数最为简洁,是这样:->
。
函数的这种简洁表达使得传递回调函数非常便利。一个数组的map
可能像这样就足够了:
list = [1, 2, 3]
list.map (e) -> e+1
而等效的JavaScript代码就不能这么马虎:
list = [1, 2, 3];
list.map(function(e) { return e + 1; });
增强的表达
CoffeeScript提供了JavaScript所没有的一些强大的表达语法,这也是被称为语法糖的东西。在我印象中,这种增强性是很多的,我举出两个有代表性的例子:
-
字符串插值法
-
列表解析
其中字符串插值法是对现有字符串能力的一种扩充和语法上的简化;而列表解析就要涉及到观念上的改变了。前者是一种改良,后者则是一种变革。
字符串插值法
在CoffeeScript的字符串里,可以用#{…}
嵌入一个表达式。例如:
"#{ 22 / 7 } is a decent approximation of π"
等价于:
"" + (22 / 7) + " is a decent approximation of π";
插值在这里起到占位的作用,使得动态内容的字符串更容易构建。我想人人都能接受这样的表达。
列表解析
列表解析是CoffeeScript的世界里的重要一员。它改变了循环的思路。CoffeeScript没有提供像JavaScript那样的for循环结构,而是统统转化为列表解析。一个常规的JavaScript for循环,像下面这样:
food_list = ['toast', 'cheese', 'wine'];
for (i = 0, len = food_list.length; i < len; i++) {
food = food_list[i];
eat(food);
}
用CoffeeScript实现就是:
food_list = ['toast', 'cheese', 'wine']
eat food for food in food_list #做个小补充,for循环的单条语句的写法
单单是上面的例子不足以显示列表解析的强大(却看到它的简洁了)。在继续这个话题之前,我觉得我有必要补充一下另一个涉及到CoffeeScript理念的东西了:一切皆是表达式。
在CoffeeScript世界里,一切语句都是表达式语句,都会返回一个值。函数调用默认会返回最后一条语句的值。if条件结构也会返回值,其返回的是执行的最后一条语句的值。循环结构有些不同,其会将每次循环的结果都保存在一个数组里,作为此循环结构的值。例如下面代码的list
结果就是[5, 4, 3, 2, 1]
。
num = 6
list = while num -= 1
num
回到列表解析的主题。与while一样,for结构也是一种循环的表达,其结果也是一个数组。回到先前的例子,下面的小代码的list
结果就是['t', 'c', 'w']。
food_list = ['toast', 'cheese', 'wine']
list = (food[0] for food in food_list)
我们已经看到for循环的each
形式
eat food for food in food_list
以及它的map
形式
(food[0] for food in food_list)
下面给出它的filter形式
(food for food in food_list when food is 'wine')
列表解析的特色的地方在于它改变了我们组织循环的方式和解析数组的模式。这是一种声明式的编程方法,告诉程序你想要什么而不去关心构建的过程。
类的支持
类是CoffeeScript对JavaScript的一个很重要的补充。JavaScript的原型功能很强大,写法上又恨别扭。正确地设置原型链以实现继承关系也是个很大的挑战。CoffeeScript从语法上直接支持类的定义,自然且隐藏细节。
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()
从实现上来说,CoffeeScript的类与JavaScript的构造函数和原型链那一套并无二致。所以,理解原型机制也是理解CoffeeScript类的基础。
关于JavaScript的糟粕
CoffeeScript的另一个目标是从语法层面上直接消除JavaScript的被人诟病的一些糟粕部分。前面已经说过关于分号的部分。关于var
声明的部分。分号的机制暂且不去例会,总之CoffeeScript不用再去写分号了。
在JavaScript当中,最为人诟病的糟粕部分有两处,因为它们使用的情况最多而且容易出错。
-
全局变量
-
相等比较
全局变量
JavaScript的作用域规则很复杂,涉及到var
声明机制和变量提升。在JavaScript里,构造一个全局变量是很容易的,有三种方式:
-
在全局的环境里用
var
声明var name = 'name';
-
在函数内用省略
var
的方式定义function foo() { name = 'name'; }
-
绑定到window的属性
window.name = 'name';
其中第1种和第2种方式是最常见的错误用法。首先不推荐直接在全局环境中编码,而是应该用一个匿名函数包裹起来,将程序的作用域限制在这个匿名函数中。第二种用法完完全全就是忘记了var
声明。而我在实际的JavaScript编码中,忘记var
声明是常有的事(就像经常忘记行末补上分号一样)。
而在CoffeeScript里面就完全没有这种担心了。首先,编译后的JavaScript代码不会暴露在全局环境里,所有的代码都是自动包裹在一个匿名函数(function(){ ... })();
内。然后,所有的变量都会自动加上var
声明。这就使得不小心污染全局的情况很难发生,除非使用赋值到window
上。
相等比较
我们都知道JavaScript有两种比较运算符:==
和===
。我们也知道==
在使用的过程中会很坑,所以平时都宁愿多打一个字符而使用===
。CoffeeScript的只有一种比较运算符==
,而它会编译成JavaScript的===
,从而很好地避过了这道坑。
是否该使用CoffeeScript
CoffeeScript简化和增强了JavaScript的表达能力,尽可能地从语法层面上就能避免JavaScript的一些坑。用它写代码,会让人有更清晰舒适的感觉,而且不容易犯错。CoffeeScript的初衷就是提供更好的JavaScript。
然而,CoffeeScript与JavaScript是不兼容的。它既不是JavaScript的子集,也不是超集,而是与JavaScript有着显然不同思路的一种语言。用CoffeeScript编程就必然要转换观念,尽管这种观念更好更自然,但却是有些固步自封的人望而却步的主要原因了。
CoffeeScript并不是适合每一个人的。有些人对于用缩进组织代码层次完全不能接受,也不能接受用箭头函数表达法。对于他们来说,去掉function关键字和大括号的组织怎么看都怎么地不顺眼。
列表解析很强大,却也显得过于简洁了。对于习惯了构造冗杂JavaScript程序的人们来说,并不习惯这种表达方式。
总之,是不可强求别人去学习使用CoffeeScript。JavaScript已经足够强大,只要足够小心,完全可以使用JavaScript很好地完成工作。对于那些想要尝试CoffeeScript,我们也要给予鼓励的态度,他们是求新求变的勇士。CoffeeScript真的值得一试,而且它真的很小巧,完全掌握它不是件困难的事。
对于在团队推行CoffeeScript,我本人更是持有保守的看法。如果团队从一开始就使用CoffeeScript还好。如果是要从CoffeeScript转为JavaScript,就要谨慎行之。一种可行的方式是先尝试在一个小项目中使用CoffeeScrip,看看效果如何。
对于个人来说,就没有什么限制了。如果真的喜欢,就去尝试吧。你可以使用CoffeeScript写脚本,构建自己的网站,做一些小玩意。