这是今年年初学习 CoffeeScript 的一篇个人总结,觉得也许会对社区有些帮助,就从我的博客转来了这里,因为水平有限,错漏之处在所难免,希望高手指点
Every language feature in CoffeeScript has been designed using this kind of process: attempt to take the beautiful dynamic semantics of JavaScript—object literals, function expressions, prototypal inheritance—and express them in a clean, readable, minimal way. by Jeremy Ashkenas, author of CoffeeScript
CoffeeScript是一门简洁的,构架于 JavaScript 之上的预处理器语言,可以静态编译成 JavaScript,语法主要受 ruby 和 python 影响,目前已经为众多 rails 和 node 项目采用。
为什么要用 CoffeeScript?
什么情况下不推荐使用 CoffeeScript?
CoffeeScript 是一种需要预编译的语言,不能在运行时 (Runtime) 解释,这造成了她普遍被人质疑的一点,就是如果代码中出现运行时错误时难以调试,不过从实际使用上来看,因为 CoffeeScript 的编译结果大部分情况下自然而合理,至少我从来没有发现从生成的 JavaScript 代码回溯到对应的 CoffeeScript 代码有什么困难之处,我们稍后会看到这种对应关系的细节
这种静态编译还有一个额外的好处,就是 CoffeeScript 和现有的环境 (浏览器,Node,Rhino 等) 与库完全兼容
最简单的安装和测试 CoffeeScript 的方法,是使用node.js的npm安装,然后使用命令行脚本实时编译
npm install -g coffee-script
# watch and compile
coffee -w --output lib --compile src
这里假设你的 coffee 代码在 src 目录下,这个 daemon 会自动检测文件的改变,并编译成 js 文件放到 lib 目录下
与 SASS/LESS 和 CSS 的关系不同,CoffeeScript 不是 JavaScript 的超集,不能在 CoffeeScript 程序中写 JavaScript 代码,比如function
等关键字
在 js 中,如果认为当前语句和随后语句是一个整体的话,就不会自己加;
,比如以下 javascript 代码
//javascript code
var y = x+f
(a+b).toString()
//parsed to:
var y = x+f(a+b).toString();
很多 js 中的问题由此引起 (实际上现在把;
放在哪里,在 js 社区内也是个争论的话题)
而 CoffeeScript 在编译时为每条语句加上;
,因此在代码中不需要写;
CoffeeScript 中的注释采用#
# single line comment
###
multi line comment
###
CoffeeScript 中对空白敏感,这种做法来自 python,任何需要({})
的场合下,可以用缩进代替
在 js 中最糟糕的设计就是全局变量,当你忘记用var
声明变量的时候,这个变量会成为全局对象上的一个属性
CoffeeScript 避免了这点
foo = "bar"
会编译成
(function() {
var foo;
foo = "bar";
}).call(this);
任何的代码都会使用Immediate Function包装,这样foo
成为了本地变量,并且,可以通过call
指定的this
引用全局对象
为了方便起见,之后的编译后代码描述不会再加上这个包装
实际上在 CoffeeScript 中,你也不需要再用var
声明变量,编译后会自动加上var
,并且将声明hoisting,即放到作用域的顶部,看一个来自官方文档的例子
outer = 1
change = ->
inner = -1
outer = 10
inner = change()
->
是函数定义的简写方式,之后我们会探讨
编译后的 js 如下:
var change, inner, outer;
outer = 1;
change = function() {
var inner;
inner = -1;
return outer = 10;
};
inner = change();
这是类似 ruby 中的自然的作用域实现方式,inner
在change()
内定义成了局部变量,因为在代码中之前没有定义过
首先是字符串可以用类 ruby 的语法内嵌
target = "world"
alert "hello, #{target}"
其次是字面量,可以用类似YAML的方法定义对象字面量
object1 = one: 1, two: 2
object2 =
one: 1
two: 2
class: "numbers"
注意保留字class
,现在可以直接作为对象的 key 了
数组也可以分行
arr = [
1
2
]
也可以解构赋值 (Destructuring)
obj = {a:"foo", b:"bar"}
{a, b} = obj
arr = [1, 2]
[a, b] = arr
数组的操作引入了来自 ruby 的 Range 概念,并且可以将字符串完全作为数组操作
numbers = [0..9]
numbers[3..5] = [-3, -4, -5]
my = "my string"[0..1]
判断一个值是否在数组内,在 js 中可以用Array.prototype.indexOf
,不过 IE8 及以下不支持,CoffeeScript 提供了跨浏览器的in
操作符解决
arr = ["foo", "bar"]
"foo" in arr
具体的实现上,是一个对indexOf
的 Shim
var arr,
__indexOf = [].indexOf || function(item) {
for (var i = 0, l = this.length; i < l; i++) {
if (i in this && this[i] === item)
return i;
}
return -1;
};
arr = ["foo", "bar"];
__indexOf.call(arr, "foo") >= 0;
for..in
语法可以用在数组上了,背后是用 js 的 for 循环实现的,这比数组的迭代器方法要效率高一些
for name, i in ["Roger", "Roderick"]
alert "#{i} - Release #{name}"
也具有过滤器when
prisoners = ["Roger", "Roderick", "Brian"]
release prisoner for prisoner in prisoners when prisoner[0] is "R"
看起来很像普通英语了,也可以用()
收集遍历的结果
result = (item for item in array when item.name is "test")
遍历对象的属性可以用of
,这是用 js 自己的for..in
实现的
names = sam: seaborn, donna: moss
alert("#{first} #{last}") for first, last of names
CoffeeScript 使用来自 ruby 的省略语法,让控制流变得很紧凑,也引进了unless
,not
,then
等语法糖式的关键字
result = if not true then "false"
result = unless true then "false"
CoffeeScript 中非常好的一点,就是直接取消了 js 中的==
判断,改成全部用===
进行严格比较,js 中的==
会做大量诡异的类型转换,很多情况下是 bug 的来源
if "1" == 1
alert("equal")
else
alert("not equal")
在使用if
来进行空值的判断时,js 有时会让人困扰,因为""和 0 都会被转换成 false,Coffee 提供了?
操作符解决这个问题,她只有在变量为null
或undefined
时才为 false
""? #true
null? #false
也可以用常见的类似 ruby 中||=
的方法,判断赋值,此外还可以用and
,or
,is
关键字代替&&
,||
,==
hash or= {}
hash ?= {}
经常有当某个属性存在的时候,才会调用属性上的方法的情况,这时候也可以用?
knight.hasSword()?.poke()
只有当hasSword()
返回对象不为空时,才会调用poke
方法,以下是编译的 js 代码
var _ref;
if ((_ref = knight.hasSword()) != null) {
_ref.poke();
}
另一种情况是当poke
方法存在时才调用
knight.hasSword().poke?()
对应的 js 代码
var _base;
if (typeof (_base = knight.hasSword()).poke === "function") {
_base.poke();
}
switch case
语句也有了一些语法糖,并且会默认加上break
switch day
when "Sun" then go relax
when "Sat" then go dancing
else go work
CoffeeScript 对 JavaScript 的函数做了很大的简化,举个例子,看一个求和函数
sum = (nums...) ->
nums.reduce (x, y) -> x+y
sum 1,2,3
对应 JavaScript
var sum,
__slice = [].slice;
sum = function() {
var nums;
nums = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return nums.reduce(function(x, y) {
return x + y;
});
};
sum(1, 2, 3);
->
来代替function
->
的前边,且可省略break
和continue
),都有返回值,因此像 ruby 一样,不需要显式return
arguments
不是一个真正的数组,要使用数组方法,必须转换成数组[].slice.call(arguments, 0)
这样,而在 CoffeeScript 中收束 (加...
) 的参数是一个真正的数组CoffeeScript 的函数可以有默认参数,如
times = (a = 1, b = 2) -> a * b
CoffeeScript 的函数调用可以不用()
语法包围参数,像 ruby 一样跟在函数名后面就可以,不过这也有时候会带来问题,特别是没有参数的调用
alert
对应的 js
alert;
而不是alert()
,这和 ruby 不同,需要注意
缩进的格式有时需要小心,比如用多个函数做参数的时候,需要这样写
$(".toggle").toggle ->
"on"
, ->
"off"
对应 js
$(".toggle").toggle(function() {
return "on";
}, function() {
return "off";
});
使用 CoffeeScript 的一个重要理由,就是她用自己的语法实现了很多很常用的 js 编程模式,而且,通常是在社区内广泛被承认的最佳实践,如果不熟悉 JavaScript 的这些模式,可能会在调试代码上遇到一些麻烦,不过,基本上来说还是比较简单易懂的,下面我们会花一些时间研究一下 CoffeeScript 是用什么样的方法来封装这些通用编程模式的
在 js 中,普遍会使用闭包实现各种事件的 handler 或封装模块,以下是 CoffeeScript 对这一普遍模式的实现
closure = do ->
_private = "foo"
-> _private
console.log(closure()) #=> "foo"
do
关键词可以产生一个Immediate Function,下面是对应 js 代码
var closure;
closure = (function() {
var _private;
_private = "foo";
return function() {
return _private;
};
})();
闭包中经常需要绑定this
的值给闭包的私有变量,CoffeeScript 使用特殊的=>
语法省去了这个麻烦
@clickHandler = -> alert "clicked"
element.addEventListener "click", (e) => @clickHandler(e)
使用=>
生成函数,可以看到生成代码中会加上对this
的绑定
var _this = this;
this.clickHandler = function() {
return alert("clicked");
};
element.addEventListener("click", function(e) {
return _this.clickHandler(e);
});
这里 CoffeeScript 对于this
有简单的别名@
在 js 中,所有的对象都是开放的,有时候会扩展原有对象的行为 (比如对数组的 ECMA5 shim),这也称为 Monkey patching
String::dasherize = -> @replace /_/g, "-"
::
代表原型的引用,js 代码如下
String.prototype.dasherize = function() {
return this.replace(/_/g, "-");
};
在 js 中是否要模拟传统编程语言的类,是个一直以来都有争议的话题,不同的项目,不同的团队,在类的使用上会有不同的看法,不过,一旦决定要使用类,那么至少需要一套良好的实现,CoffeeScript 在语言内部实现了类的模拟,我们来看一看一个完整的例子
class Gadget
@CITY = "beijing"
@create: (name, price) ->
new Gadget(name, price)
_price = 0
constructor: (@name, price) ->
_price = price
sell: =>
"Buy #{@name} with #{_price} in #{Gadget.CITY}"
iphone = new Gadget("iphone", 4999)
console.log iphone.name #=> "iphone"
console.log iphone.sell() #=> "Buy iphone with 4999 in beijing"
ipad = Gadget.create("ipad", 3999)
console.log ipad.sell() #=> "Buy ipad with 3999 in beijing"
这个 Gadget 类具有通常语言中类的功能:
constructor
是构造函数,必须用这个名称,类似 ruby 中的 initializename
是实例变量,可以通过iphone.name
获取@name
写在参数中即可,等价于在函数体中的@name = name
_price
是私有变量,需要赋初始值sell
是实例方法create
是类方法,注意这里使用了@create
,这和 ruby 有些像,在定义时的this
指的是这个类本身CITY
是类变量要注意的是,对于实例方法,要用=>
来绑定this
,这样可以作为闭包传递,比如
iphone = new Gadget("iphone", 4999)
$("#sell").click(iphone.sell())
如果不用=>
,闭包被调用时就会丢失实例对象的值 (iphone
)
对于熟悉基于类的面向对象编程的人,CoffeeScript 的类是一目了然的,下面来看看对应的 js 代码
var Gadget,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
Gadget = (function() {
var _price;
Gadget.name = 'Gadget';
Gadget.CITY = "beijing";
Gadget.create = function(name, price) {
return new Gadget(name, price);
};
_price = 0;
function Gadget(name, price) {
this.sell = __bind(this.sell, this);
this.name = name;
_price = price;
}
Gadget.prototype.sell = function() {
return "Buy " + this.name + " with " + _price + " in " + Gadget.CITY;
};
return Gadget;
})();
以上的代码有很多值得注意的地方
new
来生成实例对象_price
就是被封装在闭包内部的私有变量sell
这样的实例方法是原型方法,并且在初始化时使用自定义的 bind 函数绑定实例 (用=>
定义的情况)create
和CITY
这样的类成员使用构造函数的属性实现,重复一下,在 CoffeeScript 类定义中的this
指的是整个闭包Gadget
Gadget.name
是额外定义的类名属性CoffeeScript 中为方便地实现类的继承也定义了自己的语法,我们把上面的类简化,来看一下如何继承:
class Gadget
constructor: (@name) ->
sell: =>
"Buy #{@name}"
class IPhone extends Gadget
constructor: -> super("iphone")
nosell: =>
"Don't #{@sell()}"
iphone = new IPhone
iphone.nosell() #=> Don't Buy iphone
extends
关键字可以继承父类中的所有实例属性,比如sell
super
方法可以调用父类的同名方法constructor
,则她被子类默认调用来看一下对应的 js 代码,这有一些复杂,我们把和上边类定义中重复的地方去掉,只留下继承的实现部分
var Gadget, IPhone,
__extends = function(child, parent) {
for (var key in parent) {
if ({}.hasOwnProperty.call(parent, key))
child[key] = parent[key];
}
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor;
child.__super__ = parent.prototype;
return child;
};
IPhone = (function(_super) {
__extends(IPhone, _super);
IPhone.name = 'IPhone';
function IPhone() {
this.nosell = __bind(this.nosell, this);
IPhone.__super__.constructor.call(this, "iphone");
}
IPhone.prototype.nosell = function() {
return "Don't " + (this.sell());
};
return IPhone;
})(Gadget);
这里重点有三个,
__extends
函数使用了代理构造函数ctor
来实现继承,这是非常普遍的 js 中对象继承的实践模式,进一步解释一下
ctor.prototype = parent.prototype
的意义是只继承定义在 prototype 上的公用属性super
的实现方法是parent.prototype.constructor.call(this)
在 ruby 语言中的 Mixin,能够让你的类获得多个模块的方法,可以说是对多重继承一种很好的实现,虽然在 CoffeeScript 中并没有像 ruby 的include
一样的内置功能,但很容易实现她
class Module
@extend: (obj) ->
for key, value of obj
@[key] = value
@include: (obj) ->
for key, value of obj
@::[key] = value
classProperties =
find: (id) ->
console.log("find #{id}")
instanceProperties =
save: ->
console.log("save")
class User extends Module
@extend classProperties
@include instanceProperties
user = User.find(1)
user = new User
user.save()
classProperties
是类成员模块,使用@extend
来 Mixin,实现是简单的拷贝对象的属性instanceProperties
是实例成员模块,使用@include
来 Mixin,实现是拷贝对象原型的属性CoffeeScript 提供了一门比 JavaScript 更强大,优雅,表现力丰富的语言,但她毕竟架构于 JavaScript 之上,而且是静态地编译成 JavaScript 代码,也就是说,她不能完全避免对 JavaScript 中一些不良部分的滥用,比如eval
,typeof
,instanceof
等,所以,在任何情况下,建议始终开启Strict Mode
"use strict"
严格模式是一个 ECMA5 标准提出的 js 子集,禁用了很多 js 设计中不好的方面,在未来会逐渐成为 js 的语言标准,详细介绍在这里