TutorialsPoint CoffeeScript 教程
来源:W3CSchool
CoffeeScript 关于
CoffeeScript 实用手册
CoffeeScript 是一门编译到 JavaScript 的小巧语言。在 Java 般笨拙的外表下,JavaScript 其实有着一颗华丽的心脏。CoffeeScript 尝试用简洁的方式展示 JavaScript 优秀的部分。
适用人群
本书着重讲解了 CoffeeScript 的基础语法,适合初学者。
学习前提
学习本书前,你需要了解 JavaScript 这门语言,应为 CoffeeScript 是建立在此基础上的。
CoffeeScript 语法
CoffeeScript 服务端和客户端的代码重用
服务端和客户端的代码重用
问题
当你在 CoffeeScript 上创建了一个函数,并希望将它用在有网页浏览器的客户端和有 Node.js 的服务端时。
解决方案
以下列方法输出函数:
# simpleMath.coffee
# these methods are private
add = (a, b) ->
a + b
subtract = (a, b) ->
a - b
square = (x) ->
x * x
# create a namespace to export our public methods
SimpleMath = exports? and exports or @SimpleMath = {}
# items attached to our namespace are available in Node.js as well as client browsers
class SimpleMath.Calculator
add: add
subtract: subtract
square: square
讨论
在上面的例子中,我们创建了一个新的名为 “SimpleMath” 的命名空间。如果 “export” 是有效的,我们的类就会作为一个 Node.js 模块输出。如果 “export” 是无效的,那么 “SimpleMath” 就会被加入全局命名空间,这样就可以被我们的网页使用了。
在 Node.js 中,我们可以使用 “require” 命令包含我们的模块。
$ node
> var SimpleMath = require('./simpleMath');
undefined
> var Calc = new SimpleMath.Calculator();
undefined
> console.log("5 + 6 = ", Calc.add(5, 6));
5 + 6 = 11
undefined
>
在网页中,我们可以通过将模块作为一个脚本嵌入其中。
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>SimpleMath Module Example</title>
<script src="http://img.w3cschool.cn/attachments/image/wk/coffeescript/jquery.min.js"></script>
<script src="simpleMath.js"></script>
<script>
jQuery(document).ready(function (){
var Calculator = new SimpleMath.Calculator();
var result = $('<li>').html("5 + 6 = " + Calculator.add(5, 6));
$('#SampleResults').append(result);
});
</script>
</head>
<body>
<h1>A SimpleMath Example</h1>
<ul id="SampleResults"></ul>
</body>
</html>
输出结果:
A SimpleMath Example
· 5 + 6 = 11
CoffeeScript 比较范围
比较范围
问题
如果你想知道某个变量是否在给定的范围内。
解决方案
使用 CoffeeScript 的连缀比较语法。
maxDwarfism = 147
minAcromegaly = 213
height = 180
normalHeight = maxDwarfism < height < minAcromegaly
# => true
讨论
这是从 Python 中借鉴过来的一个很棒的特性。利用这个特性,不必像下面这样写出完整的比较:
normalHeight = height > maxDwarfism && height < minAcromegaly
CoffeeScript 支持像写数学中的比较表达式一样连缀两个比较,这样更直观。
CoffeeScript 嵌入 JavaScript
嵌入 JavaScript
问题
你想在 CoffeeScript 中嵌入找到的或预先编写的 JavaScript 代码。
解决方案
把 JavaScript 包装到撇号中:
`function greet(name) {
return "Hello "+name;
}`
# Back to CoffeeScript
greet "Coffee"
# => "Hello Coffee"
讨论
这是在 CoffeeScript 代码中集成少量 JavaScript 而不必用 CoffeeScript 语法转换它们的最简单的方法。正如 CoffeeScript Language Reference 中展示的,可以在一定范围内混合这两种语言的代码:
hello = `function (name) {
return "Hello "+name
}`
hello "Coffee"
# => "Hello Coffee"
这里的变量 "hello" 还在 CoffeeScript 中,但赋给它的函数则是用 JavaScript 写的。
CoffeeScript For 循环
For 循环
问题
你想通过一个 for 循环来迭代数组、对象或范围。
解决方案
# for(i = 1; i<= 10; i++)
x for x in [1..10]
# => [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
# To count by 2
# for(i=1; i<= 10; i=i+2)
x for x in [1..10] by 2
# => [ 1, 3, 5, 7, 9 ]
# Perform a simple operation like squaring each item.
x * x for x in [1..10]
# = > [1,4,9,16,25,36,49,64,81,100]
讨论
CoffeeScript 使用推导(comprehension)来代替 for 循环,这些推导最终会被编译成 JavaScript 中等价的 for 循环。
CoffeeScript 类和对象
CoffeeScript 对象的链式调用
对象的链式调用
问题
你想调用一个对象上的多个方法,但不想每次都引用该对象。
解决方案
在每次链式调用后返回 this(即@)对象
class CoffeeCup
constructor: ->
@properties=
strength: 'medium'
cream: false
sugar: false
strength: (newStrength) ->
@properties.strength = newStrength
this
cream: (newCream) ->
@properties.cream = newCream
this
sugar: (newSugar) ->
@properties.sugar = newSugar
this
morningCup = new CoffeeCup()
morningCup.properties # => { strength: 'medium', cream: false, sugar: false }
eveningCup = new CoffeeCup().strength('dark').cream(true).sugar(true)
eveningCup.properties # => { strength: 'dark', cream: true, sugar: true }
讨论
jQuery 库使用类似的手段从每一个相似的方法中返回选择符对象,并在后续方法中通过调整选择的范围修改该对象:
$('p').filter('.topic').first()
对我们自己对象而言,一点点元编程就可以自动设置这个过程并明确声明返回 this 的意图。
addChainedAttributeAccessor = (obj, propertyAttr, attr) ->
obj[attr] = (newValues...) ->
if newValues.length == 0
obj[propertyAttr][attr]
else
obj[propertyAttr][attr] = newValues[0]
obj
class TeaCup
constructor: ->
@properties=
size: 'medium'
type: 'black'
sugar: false
cream: false
addChainedAttributeAccessor(this, 'properties', attr) for attr of @properties
earlgrey = new TeaCup().size('small').type('Earl Grey').sugar('false')
earlgrey.properties # => { size: 'small', type: 'Earl Grey', sugar: false }
earlgrey.sugar true
earlgrey.sugar() # => true
CoffeeScript 类方法和实例方法
类方法和实例方法
问题
你想创建类和实例的方法。
解决方案
类方法
class Songs
@_titles: 0 # Although it's directly accessible, the leading _ defines it by convention as private property.
@get_count: ->
@_titles
constructor: (@artist, @title) ->
@constructor._titles++ # Refers to <Classname>._titles, in this case Songs.titles
Songs.get_count()
# => 0
song = new Songs("Rick Astley", "Never Gonna Give You Up")
Songs.get_count()
# => 1
song.get_count()
# => TypeError: Object <Songs> has no method 'get_count'
实例方法
class Songs
_titles: 0 # Although it's directly accessible, the leading _ defines it by convention as private property.
get_count: ->
@_titles
constructor: (@artist, @title) ->
@_titles++
song = new Songs("Rick Astley", "Never Gonna Give You Up")
song.get_count()
# => 1
Songs.get_count()
# => TypeError: Object function Songs(artist, title) ... has no method 'get_count'
讨论
Coffeescript 会在对象本身中保存类方法(也叫静态方法),而不是在对象原型中(以及单一的对象实例),在保存了记录的同时也将类级的变量保存在中心位置。
CoffeeScript 类变量和实例变量
类变量和实例变量
问题
你想创建类变量和实例变量(属性)。
解决方案
类变量
class Zoo
@MAX_ANIMALS: 50
MAX_ZOOKEEPERS: 3
helpfulInfo: =>
"Zoos may contain a maximum of #{@constructor.MAX_ANIMALS} animals and #{@MAX_ZOOKEEPERS} zoo keepers."
Zoo.MAX_ANIMALS
# => 50
Zoo.MAX_ZOOKEEPERS
# => undefined (it is a prototype member)
Zoo::MAX_ZOOKEEPERS
# => 3
zoo = new Zoo
zoo.MAX_ZOOKEEPERS
# => 3
zoo.helpfulInfo()
# => "Zoos may contain a maximum of 50 animals and 3 zoo keepers."
zoo.MAX_ZOOKEEPERS = "smelly"
zoo.MAX_ANIMALS = "seventeen"
zoo.helpfulInfo()
# => "Zoos may contain a maximum of 50 animals and smelly zoo keepers."
实例变量
你必须在一个类的方法中才能定义实例变量(例如属性),在 constructor 结构中初始化你的默认值。
class Zoo
constructor: ->
@animals = [] # Here the instance variable is defined
addAnimal: (name) ->
@animals.push name
zoo = new Zoo()
zoo.addAnimal 'elephant'
otherZoo = new Zoo()
otherZoo.addAnimal 'lion'
zoo.animals
# => ['elephant']
otherZoo.animals
# => ['lion']
警告!
不要试图在 constructor 外部添加变量(即使在 elsewhere 中提到了,由于潜在的 JavaScript 的原型概念,这不会像预期那样运行正确)。
class BadZoo
animals: [] # Translates to BadZoo.prototype.animals = []; and is thus shared between instances
addAnimal: (name) ->
@animals.push name # Works due to the prototype concept of Javascript
zoo = new BadZoo()
zoo.addAnimal 'elephant'
otherZoo = new BadZoo()
otherZoo.addAnimal 'lion'
zoo.animals
# => ['elephant','lion'] # Oops...
otherZoo.animals
# => ['elephant','lion'] # Oops...
BadZoo::animals
# => ['elephant','lion'] # The value is stored in the prototype
讨论
Coffeescript 会将类变量的值保存在类中而不是它定义的原型中。这在定义类中的变量时是十分有用的,因为这不会被实体属性变量重写。
CoffeeScript 克隆对象(深度复制)
克隆对象(深度复制)
问题
你想复制一个对象,包含其所有子对象。
解决方案
clone = (obj) ->
if not obj? or typeof obj isnt 'object'
return obj
if obj instanceof Date
return new Date(obj.getTime())
if obj instanceof RegExp
flags = ''
flags += 'g' if obj.global?
flags += 'i' if obj.ignoreCase?
flags += 'm' if obj.multiline?
flags += 'y' if obj.sticky?
return new RegExp(obj.source, flags)
newInstance = new obj.constructor()
for key of obj
newInstance[key] = clone obj[key]
return newInstance
x =
foo: 'bar'
bar: 'foo'
y = clone(x)
y.foo = 'test'
console.log x.foo isnt y.foo, x.foo, y.foo
# => true, bar, test
讨论
通过赋值来复制对象与通过克隆函数来复制对象的区别在于如何处理引用。赋值只会复制对象的引用,而克隆函数则会:
- 创建一个全新的对象
- 这个新对象会复制原对象的所有属性,
- 并且对原对象的所有子对象,也会递归调用克隆函数,复制每个子对象的所有属性。
下面是一个通过赋值来复制对象的例子:
x =
foo: 'bar'
bar: 'foo'
y = x
y.foo = 'test'
console.log x.foo isnt y.foo, x.foo, y.foo
# => false, test, test
显然,复制之后修改 y 也就修改了 x。
CoffeeScript 类的混合
类的混合
问题
你有一些通用方法,你想把他们包含到很多不同的类中。
解决方案
使用 mixOf 库函数,它会生成一个混合父类。
mixOf = (base, mixins...) ->
class Mixed extends base
for mixin in mixins by -1 #earlier mixins override later ones
for name, method of mixin::
Mixed::[name] = method
Mixed
...
class DeepThought
answer: ->
42
class PhilosopherMixin
pontificate: ->
console.log "hmm..."
@wise = yes
class DeeperThought extends mixOf DeepThought, PhilosopherMixin
answer: ->
@pontificate()
super()
earth = new DeeperThought
earth.answer()
# hmm...
# => 42
讨论
这适用于轻量级的混合。因此你可以从基类和基类的祖先中继承方法,也可以从混合类的基类和祖先中继承,但是不能从混合类的祖先中继承。与此同时,在声明了一个混合类后,此后的对这个混合类进行的改变是不会反应出来的。
CoffeeScript 创建一个不存在的对象字面值
创建一个不存在的对象字面值
问题
你想初始化一个对象字面值,但如果这个对象已经存在,你不想重写它。
解决方案
使用存在判断运算符(existential operator)。
window.MY_NAMESPACE ?= {}
讨论
这行代码与下面的 JavaScript 代码等价:
window.MY_NAMESPACE = window.MY_NAMESPACE || {};
这是 JavaScript 中一个常用的技巧,即使用对象字面值来定义命名空间。这样先判断是否存在同名的命名空间然后再创建,可以避免重写已经存在的命名空间。
CoffeeScript CoffeeScrip 的 type 函数
CoffeeScrip 的 type 函数
问题
你想在不使用 typeof 的情况下知道一个函数的类型。(要了解为什么 typeof 不靠谱,请参见 http://javascript.crockford.com/remedial.html。)
解决方案
使用下面这个type函数
type = (obj) ->
if obj == undefined or obj == null
return String obj
classToType = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Object]': 'object'
}
return classToType[Object.prototype.toString.call(obj)]
讨论
这个函数模仿了 jQuery 的 $.type函数。
需要注意的是,在某些情况下,只要使用鸭子类型检测及存在运算符就可以不必检测对象的类型了。例如,下面这行代码不会发生异常,它会在 myArray 的确是数组(或者一个带有 push 方法的类数组对象)的情况下向其中推入一个元素,否则什么也不做。
myArray?.push? myValue
CoffeeScript 字符串
CoffeeScript 大写单词首字母
大写单词首字母
问题
你想把字符串中每个单词的首字母转换为大写形式。
解决方案
使用“拆分-映射-拼接”模式:先把字符串拆分成单词,然后通过映射来大写单词第一个字母小写其他字母,最后再将转换后的单词拼接成字符串。
("foo bar baz".split(' ').map (word) -> word[0].toUpperCase() + word[1..-1].toLowerCase()).join ' '
# => 'Foo Bar Baz'
或者使用列表推导(comprehension),也可以实现同样的结果:
(word[0].toUpperCase() + word[1..-1].toLowerCase() for word in "foo bar baz".split /\s+/).join ' '
# => 'Foo Bar Baz'
讨论
“拆分-映射-拼接”是一种常用的脚本编写模式,可以追溯到Perl语言。如果能把这个功能直接通过“扩展类”放到String类里,就更方便了。
需要注意的是,“拆分-映射-拼接”模式存在两个问题。第一个问题,只有在文本形式统一的情况下才能有效拆分文本。如果来源字符串中有分隔符包含多个空白符,就需要考虑怎么过滤掉多余的空单词。一种解决方案是使用正则表达式来匹配空白符的串,而不是像前面那样只匹配一个空格:
("foo bar baz".split(/\s+/).map (word) -> word[0].toUpperCase() + word[1..-1].toLowerCase()).join ' '
# => 'Foo Bar Baz'
但这样做又会导致第二个问题:在结果字符串中,原来的空白符串经过拼接就只剩下一个空格了。
不过,一般来说,这两个问题还是可以接受的。所以,“拆分-映射-拼接”仍然是一种有效的技术。
CoffeeScript 查找子字符串
查找子字符串
问题
你想在一条消息中查找某个关键字第一次或最后一次出现的位置。
解决方案
分别使用 JavaScript 的 indexOf() 和 lastIndexOf() 方法查找字符串第一次和最后一次出现的位置。语法: string.indexOf searchstring, start
message = "This is a test string. This has a repeat or two. This might even have a third."
message.indexOf "This", 0
# => 0
# Modifying the start parameter
message.indexOf "This", 5
# => 23
message.lastIndexOf "This"
# => 49
讨论
还需要想办法统计出给定字符串在一条消息中出现的次数。
CoffeeScript 生成唯一 ID
生成唯一ID
问题
你想随机生成一个唯一的标识符。
解决方案
可以根据一个随机数值生成一个 Base 36 编码的字符串。
uniqueId = (length=8) ->
id = ""
id += Math.random().toString(36).substr(2) while id.length < length
id.substr 0, length
uniqueId() # => n5yjla3b
uniqueId(2) # => 0d
uniqueId(20) # => ox9eo7rt3ej0pb9kqlke
uniqueId(40) # => xu2vo4xjn4g0t3xr74zmndshrqlivn291d584alj
讨论
使用其他技术也可以,但这种方法相对来说性能更高,也更灵活。
CoffeeScript 字符串插值
字符串插值
问题
你想创建一个字符串,让它包含体现某个 CoffeeScript 变量的文本。
解决方案
使用 CoffeeScript 中类似 Ruby 的字符串插值,而不是 JavaScript 的字符串拼接。
插值:
muppet = "Beeker"
favorite = "My favorite muppet is #{muppet}!"
# => "My favorite muppet is Beeker!"
square = (x) -> x * x
message = "The square of 7 is #{square 7}."
# => "The square of 7 is 49."
讨论
CoffeeScript 的插值与 Ruby 类似,多数表达式都可以用在 #{ ... } 插值结构中。
CoffeeScript 支持在插值结构中放入多个有副作用的表达式,但建议大家不要这样做。因为只有表达式的最后一个值会被插入。
# 可以这样做,但不要这样做。否则,你会疯掉。
square = (x) -> x * x
muppet = "Beeker"
message = "The square of 10 is #{muppet='Animal'; square 10}. Oh, and your favorite muppet is now #{muppet}."
# => "The square of 10 is 100. Oh, and your favorite muppet is now Animal."
CoffeeScript 把字符串转换为小写形式
把字符串转换为小写形式
问题
你想把字符串转换成小写形式。
解决方案
使用 JavaScript 的 String 的 toLowerCase() 方法:
"ONE TWO THREE".toLowerCase()
# => 'one two three'
讨论
toLowerCase() 是一个标准的 JavaScript 方法。不要忘了带圆括号。
语法块
通过下面的快捷方式可以添加某种类似 Ruby 的语法块:
String::downcase = -> @toLowerCase()
"ONE TWO THREE".downcase()
# => 'one two three'
上面的代码演示了 CoffeeScript 的两个特性:
- 双冒号 :: 是引用 .prototype 的快捷方式;
- “at” 字符 @ 是引用 this 的快捷方式。
上面的代码会编译成如下 JavaScript 代码:
String.prototype.downcase = function() {
return this.toLowerCase();
};
"ONE TWO THREE".downcase();
提示 尽管上面的用法在类似 Ruby 的语言中很常见,但在 JavaScript 中对本地对象的扩展经常被视为不好的。(请看:Maintainable JavaScript: Don’t modify objects you don’t own;Extending built-in native objects. Evil or not?)
CoffeeScript 匹配字符串
匹配字符串
问题
你想要匹配两个或多个字符串。
解决方案
计算把一个字符串转换成另一个字符串所需的编辑距离或操作数。
levenshtein = (str1, str2) ->
l1 = str1.length
l2 = str2.length
prevDist = [0..l2]
nextDist = [0..l2]
for i in [1..l1] by 1
nextDist[0] = i
for j in [1..l2] by 1
if (str1.charAt i-1) == (str2.charAt j-1)
nextDist[j] = prevDist[j-1]
else
nextDist[j] = 1 + Math.min prevDist[j], nextDist[j-1], prevDist[j-1]
[prevDist,nextDist]=[nextDist, prevDist]
prevDist[l2]
讨论
可以使用赫斯伯格( Hirschberg )或瓦格纳菲舍尔( Wagner–Fischer)的算法来计算来文史特( Levenshtein )距离。这个例子用的是瓦格纳菲舍尔算法。
这个版本的文史特算法和内存呈线性关系,和时间呈二次方关系。
在这里我们使用 str.charAt i 这种表示法而不用 str[i] 这种方式,是因为后者在某些浏览器(如 IE7)中不支持。
起初,"by 1" 在两次循环中看起来似乎是没用的。它在这里是用来避免一个 coffeescript [i..j] 语法的常见错误。如果 str1 或 str2 为空字符串,那么 [1..l1] 或 [1..l2] 将会返回 [1,0] 。添加了 "by 1" 的循环也能编译出更加简洁高效的 javascript 。
最后,循环结尾处对回收数组的优化在这里主要是为了演示 coffeescript 中交换两个变量的语法。
CoffeeScript 重复字符串
重复字符串
问题
你想重复一个字符串。
解决方案
创建一个包含 n+1 个空元素的数组,然后用要重复的字符串作为连接字符将数组元素拼接到一起:
# 创建包含10个foo的字符串
Array(11).join 'foo'
# => "foofoofoofoofoofoofoofoofoofoo"
为字符串重复方法
你也可以在字符串的原型中为其创建方法。它十分简单:
# 为所有的字符串添加重复方法,这会重复返回 n 次字符串
String::repeat = (n) -> Array(n+1).join(this)
讨论
JavaScript 缺少字符串重复函数,CoffeeScript 也没有提供。虽然在这里也可以使用列表推导( comprehensions ),但对于简单的字符串重复来说,还是像这样先构建一个包含 n+1 个空元素的数组,然后再把它拼接起来更方便。
CoffeeScript 拆分字符串
拆分字符串
问题
你想拆分一个字符串。
解决方案
使用 JavaScript 字符串的 split() 方法:
"foo bar baz".split " "
# => [ 'foo', 'bar', 'baz' ]
讨论
String 的这个 split() 方法是标准的 JavaScript 方法。可以用来基于任何分隔符——包括正则表达式来拆分字符串。这个方法还可以接受第二个参数,用于指定返回的子字符串数目。
"foo-bar-baz".split "-"
# => [ 'foo', 'bar', 'baz' ]
"foo bar \t baz".split /\s+/
# => [ 'foo', 'bar', 'baz' ]
"the sun goes down and I sit on the old broken-down river pier".split " ", 2
# => [ 'the', 'sun' ]
CoffeeScript 清理字符串前后的空白符
清理字符串前后的空白符
问题
你想清理字符串前后的空白符。
解决方案
使用 JavaScript 的正则表达式来替换空白符。
要清理字符串前后的空白符,可以使用以下代码:
" padded string ".replace /^\s+|\s+$/g, ""
# => 'padded string'
如果只想清理字符串前面的空白符,使用以下代码:
" padded string ".replace /^\s+/g, ""
# => 'padded string '
如果只想清理字符串后面的空白符,使用以下代码:
" padded string ".replace /\s+$/g, ""
# => ' padded string'
讨论
Opera、Firefox 和 Chrome 中 String 的原型都有原生的 trim 方法,其他浏览器也可以添加一个。对于这个方法而言,还是尽可能使用内置方法,否则就创建一个 polyfill:
unless String::trim then String::trim = -> @replace /^\s+|\s+$/g, ""
" padded string ".trim()
# => 'padded string'
语法块
还可以添加一些类似Ruby中的语法块,定义如下快捷方法:
String::strip = -> if String::trim? then @trim() else @replace /^\s+|\s+$/g, ""
String::lstrip = -> @replace /^\s+/g, ""
String::rstrip = -> @replace /\s+$/g, ""
" padded string ".strip()
# => 'padded string'
" padded string ".lstrip()
# => 'padded string '
" padded string ".rstrip()
# => ' padded string'
要想深入了解 JavaScript 执行 trim 操作时的性能,请参见 Steve Levithan 的这篇博客文章。
CoffeeScript 把字符串转换为大写形式
把字符串转换为大写形式
问题
你想把字符串转换成大写形式。
解决方案
使用 JavaScript 的 String 的 toUpperCase() 方法:
"one two three".toUpperCase()
# => 'ONE TWO THREE'
讨论
toUpperCase() 是一个标准的 JavaScript 方法。不要忘了带圆括号。
语法块
通过下面的快捷方式可以添加某种类似 Ruby 的语法块:
String::upcase = -> @toUpperCase()
"one two three".upcase()
# => 'ONE TWO THREE'
上面的代码演示了 CoffeeScript 的两个特性:
- 双冒号 :: 是引用 .prototype 的快捷方式;
- “at” 字符 @ 是引用 this 的快捷方式。
上面的代码会编译成如下 JavaScript 代码:
String.prototype.upcase = function() {
return this.toUpperCase();
};
"one two three".upcase();
提示 尽管上面的用法在类似 Ruby 的语言中很常见,但在 JavaScript 中对本地对象的扩展经常被视为不好的。(请看:Maintainable JavaScript: Don’t modify objects you don’t own;Extending built-in native objects. Evil or not?)
CoffeeScript 数组
CoffeeScript 检查变量的类型是否为数组
检查变量的类型是否为数组
问题
你希望检查一个变量是否为一个数组。
myArray = []
console.log typeof myArray // outputs 'object'
“typeof” 运算符为数组输出了一个错误的结果。
解决方案
使用下面的代码:
typeIsArray = Array.isArray || ( value ) -> return {}.toString.call( value ) is '[object Array]'
为了使用这个,像下面这样调用 typeIsArray 就可以了。
myArray = []
typeIsArray myArray // outputs true
讨论
上面方法取自 "the Miller Device"。另外一个方式是使用 Douglas Crockford 的片段。
typeIsArray = ( value ) ->
value and
typeof value is 'object' and
value instanceof Array and
typeof value.length is 'number' and
typeof value.splice is 'function' and
not ( value.propertyIsEnumerable 'length' )
CoffeeScript 将数组连接
将数组连接
问题
你希望将两个数组连接到一起。
解决方案
在 JavaScript 中,有两个标准方法可以用来连接数组。
第一种是使用 JavaScript 的数组方法 concat():
array1 = [1, 2, 3]
array2 = [4, 5, 6]
array3 = array1.concat array2
# => [1, 2, 3, 4, 5, 6]
需要指出的是 array1 没有被运算修改。连接后形成的新数组的返回值是一个新的对象。
如果你希望在连接两个数组后不产生新的对象,那么你可以使用下面的技术:
array1 = [1, 2, 3]
array2 = [4, 5, 6]
Array::push.apply array1, array2
array1
# => [1, 2, 3, 4, 5, 6]
在上面的例子中,Array.prototype.push.apply(a, b) 方法修改了 array1 而没有产生一个新的数组对象。
在 CoffeeScript 中,我们可以简化上面的方式,通过给数组创建一个新方法 merge():
Array::merge = (other) -> Array::push.apply @, other
array1 = [1, 2, 3]
array2 = [4, 5, 6]
array1.merge array2
array1
# => [1, 2, 3, 4, 5, 6]
另一种方法,我可以直接将一个 CoffeeScript splat(array2) 放入 push() 中,避免了使用数组原型。
array1 = [1, 2, 3]
array2 = [4, 5, 6]
array1.push array2...
array1
# => [1, 2, 3, 4, 5, 6]
一个更加符合语言习惯的方法是在一个数组语言中直接使用 splat 运算符(...)。这可以用来连接任意数量的数组。
array1 = [1, 2, 3]
array2 = [4, 5, 6]
array3 = [array1..., array2...]
array3
# => [1, 2, 3, 4, 5, 6]
讨论
CoffeeScript 缺少了一种用来连接数组的特殊语法,但是 concat() 和 push() 是标准的 JavaScript 方法。
CoffeeScript 由数组创建一个对象词典
由数组创建一个对象词典
问题
你有一组对象,例如:
cats = [
{
name: "Bubbles"
age: 1
},
{
name: "Sparkle"
favoriteFood: "tuna"
}
]
但是你想让它像词典一样,可以通过关键字访问它,就像使用 cats["Bubbles"]。
解决方案
你需要将你的数组转换为一个对象。通过这样使用 reduce:
# key = The key by which to index the dictionary
Array::toDict = (key) ->
@reduce ((dict, obj) -> dict[ obj[key] ] = obj if obj[key]?; return dict), {}
使用它时像下面这样:
catsDict = cats.toDict('name')
catsDict["Bubbles"]
# => { age: 1, name: "Bubbles" }
讨论
另一种方法是使用数组推导:
Array::toDict = (key) ->
dict = {}
dict[obj[key]] = obj for obj in this when obj[key]?
dict
如果你使用 Underscore.js,你可以创建一个 mixin:
_.mixin toDict: (arr, key) ->
throw new Error('_.toDict takes an Array') unless _.isArray arr
_.reduce arr, ((dict, obj) -> dict[ obj[key] ] = obj if obj[key]?; return dict), {}
catsDict = _.toDict(cats, 'name')
catsDict["Sparkle"]
# => { favoriteFood: "tuna", name: "Sparkle" }
CoffeeScript 由数组创建一个字符串
由数组创建一个字符串
问题
你想由数组创建一个字符串。
解决方案
使用 JavaScript 的数组方法 toString():
["one", "two", "three"].toString()
# => 'one,two,three'
讨论
toString() 是一个标准的 JavaScript 方法。不要忘记圆括号。
CoffeeScript 定义数组范围
定义数组范围
问题
你想定义一个数组的范围。
解决方案
在 CoffeeScript 中,有两种方式定义数组元素的范围。
myArray = [1..10]
# => [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
myArray = [1...10]
# => [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
要想反转元素的范围,则可以写成下面这样。
myLargeArray = [10..1]
# => [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]
myLargeArray = [10...1]
# => [ 10, 9, 8, 7, 6, 5, 4, 3, 2 ]
讨论
包含范围以 “..” 运算符定义,包含最后一个值。 排除范围以 “...” 运算符定义,并且通常忽略最后一个值。
CoffeeScript 筛选数组
筛选数组
问题
你想要根据布尔条件来筛选数组。
解决方案
使用 Array.filter (ECMAScript 5): array = [1..10]
array.filter (x) -> x > 5
# => [6,7,8,9,10]
在 EC5 之前的实现中,可以通过添加一个筛选函数扩展 Array 的原型,该函数接受一个回调并对自身进行过滤,将回调函数返回 true 的元素收集起来。
# 扩展 Array 的原型
Array::filter = (callback) ->
element for element in this when callback(element)
array = [1..10]
# 筛选偶数
filtered_array = array.filter (x) -> x % 2 == 0
# => [2,4,6,8,10]
# 过滤掉小于或等于5的元素
gt_five = (x) -> x > 5
filtered_array = array.filter gt_five
# => [6,7,8,9,10]
讨论
这个方法与 Ruby 的 Array 的 #select 方法类似。
CoffeeScript 列表推导
列表推导
问题
你有一个对象数组,想将它们映射到另一个数组,类似于 Python 的列表推导。
解决方案
使用列表推导,但不要忘记还有 [mapping-arrays]( http://coffeescript-cookbook.github.io/chapters/arrays/mapping-arrays) 。
electric_mayhem = [ { name: "Doctor Teeth", instrument: "piano" },
{ name: "Janice", instrument: "lead guitar" },
{ name: "Sgt. Floyd Pepper", instrument: "bass" },
{ name: "Zoot", instrument: "sax" },
{ name: "Lips", instrument: "trumpet" },
{ name: "Animal", instrument: "drums" } ]
names = (muppet.name for muppet in electric_mayhem)
# => [ 'Doctor Teeth', 'Janice', 'Sgt. Floyd Pepper', 'Zoot', 'Lips', 'Animal' ]
讨论
因为 CoffeeScript 直接支持列表推导,在你使用一个 Python 的语句时,他们会很好地起到作用。对于简单的映射,列表推导具有更好的可读性。但是对于复杂的转换或链式映射,映射数组可能更合适。
CoffeeScript 映射数组
映射数组
问题
你有一个对象数组,想把这些对象映射到另一个数组中,就像 Ruby 的映射一样。
解决方案
使用 map() 和匿名函数,但不要忘了还有列表推导。
electric_mayhem = [ { name: "Doctor Teeth", instrument: "piano" },
{ name: "Janice", instrument: "lead guitar" },
{ name: "Sgt. Floyd Pepper", instrument: "bass" },
{ name: "Zoot", instrument: "sax" },
{ name: "Lips", instrument: "trumpet" },
{ name: "Animal", instrument: "drums" } ]
names = electric_mayhem.map (muppet) -> muppet.name
# => [ 'Doctor Teeth', 'Janice', 'Sgt. Floyd Pepper', 'Zoot', 'Lips', 'Animal' ]
讨论
因为 CoffeeScript 支持匿名函数,所以在 CoffeeScript 中映射数组就像在 Ruby 中一样简单。 映射在 CoffeeScript 中是处理复杂转换和连缀映射的好方法。如果你的转换如同上例中那么简单,那可能将它当成[列表推导]( http://coffeescript-cookbook.github.io/chapters/arrays/list-comprehensions) 看起来会清楚一些。
CoffeeScript 数组最大值
数组最大值
问题
你需要找出数组中包含的最大的值。
解决方案
你可以使用 JavaScript 实现,在列表推导基础上使用 Math.max():
Math.max [12, 32, 11, 67, 1, 3]...
# => 67
另一种方法,在 ECMAScript 5 中,可以使用 Array 的 reduce 方法,它与旧的 JavaScript 实现兼容。
# ECMAScript 5
[12,32,11,67,1,3].reduce (a,b) -> Math.max a, b
# => 67
讨论
Math.max 在这里比较两个数值,返回其中较大的一个。省略号 (...) 将每个数组价值转化为给函数的参数。你还可以使用它与其他带可变数量的参数进行讨论,如执行 console.log 。
CoffeeScript 归纳数组
归纳数组
问题
你有一个对象数组,想要把它们归纳为一个值,类似于 Ruby 中的 reduce() 和 reduceRight() 。
解决方案
可以使用一个匿名函数包含 Array 的 reduce() 和 reduceRight() 方法,保持代码清晰易懂。这里归纳可能会像对数值和字符串应用 + 运算符那么简单。
[1,2,3,4].reduce (x,y) -> x + y
# => 10
["words", "of", "bunch", "A"].reduceRight (x, y) -> x + " " + y
# => 'A bunch of words'
或者,也可能更复杂一些,例如把列表中的元素聚集到一个组合对象中。
people =
{ name: 'alec', age: 10 }
{ name: 'bert', age: 16 }
{ name: 'chad', age: 17 }
people.reduce (x, y) ->
x[y.name]= y.age
x
, {}
# => { alec: 10, bert: 16, chad: 17 }
讨论
Javascript 1.8 中引入了 reduce 和 reduceRight ,而 Coffeescript 为匿名函数提供了简单自然的表达语法。二者配合使用,可以把集合的项合并为组合的结果。
CoffeeScript 删除数组中的相同元素
删除数组中的相同元素
问题
你想从数组中删除相同元素。
解决方案
Array::unique = ->
output = {}
output[@[key]] = @[key] for key in [0...@length]
value for key, value of output
[1,1,2,2,2,3,4,5,6,6,6,"a","a","b","d","b","c"].unique()
# => [ 1, 2, 3, 4, 5, 6, 'a', 'b', 'd', 'c' ]
讨论
在 JavaScript 中有很多的独特方法来实现这一功能。这一次是基于“最快速的方法来查找数组的唯一元素”,出自这里 。
注意: 延长本机对象通常被认为是在 JavaScript 不好的做法,即便它在 Ruby 语言中相当普遍, (参考:Maintainable JavaScript: Don’t modify objects you don’t own
CoffeeScript 反转数组
反转数组
问题
你想要反转数组元素。
解决方案
使用 JavaScript Array 的 reverse() 方法:
["one", "two", "three"].reverse()
# => ["three", "two", "one"]
讨论
reverse() 是标准的 JavaScript 方法,别忘了带圆括号。
CoffeeScript 打乱数组中的元素
打乱数组中的元素
问题
你想打乱数组中的元素。
解决方案
Fisher-Yates shuffle 是一种高效、公正的方式来让数组中的元素随机化。这是一个相当简单的方法:在列表的结尾处开始,用一个随机元素交换最后一个元素列表中的最后一个元素。继续下一个并重复操作,直到你到达列表的起始端,最终列表中所有的元素都已打乱。这 [ Fisher-Yates shuffle Visualization ]( http://bost.ocks.org/mike/shuffle/) 可以帮助你理解算法。
shuffle = (source) ->
# Arrays with < 2 elements do not shuffle well. Instead make it a noop.
return source unless source.length >= 2
# From the end of the list to the beginning, pick element `index`.
for index in [source.length-1..1]
# Choose random element `randomIndex` to the front of `index` to swap with.
randomIndex = Math.floor Math.random() * (index + 1)
# Swap `randomIndex` with `index`, using destructured assignment
[source[index], source[randomIndex]] = [source[randomIndex], source[index]]
source
shuffle([1..9])
# => [ 3, 1, 5, 6, 4, 8, 2, 9, 7 ]
讨论
一种错误的方式
有一个很常见但是错误的打乱数组的方式:通过随机数。
shuffle = (a) -> a.sort -> 0.5 - Math.random()
如果你做了一个随机的排序,你应该得到一个序列随机的顺序,对吧?甚至微软也用这种随机排序算法 。原来,[这种随机排序算法产生有偏差的结果]( http://blog.codinghorror.com/the-danger-of-naivete/) ,因为它存在一种洗牌的错觉。随机排序不会导致一个工整的洗牌,它会导致序列排序质量的参差不齐。
速度和空间的优化
以上的解决方案处理速度是不一样的。该列表,当转换成 JavaScript 时,比它要复杂得多,变性分配比处理裸变量的速度要慢得多。以下代码并不完善,并且需要更多的源代码空间 … 但会编译量更小,运行更快:
shuffle = (a) ->
i = a.length
while --i > 0
j = ~~(Math.random() * (i + 1)) # ~~ is a common optimization for Math.floor
t = a[j]
a[j] = a[i]
a[i] = t
a
扩展 Javascript 来包含乱序数组
下面的代码将乱序功能添加到数组原型中,这意味着你可以在任何希望的数组中运行它,并以更直接的方式来运行它。
Array::shuffle ?= ->
if @length > 1 then for i in [@length-1..1]
j = Math.floor Math.random() * (i + 1)
[@[i], @[j]] = [@[j], @[i]]
this
[1..9].shuffle()
# => [ 3, 1, 5, 6, 4, 8, 2, 9, 7 ]
注意: 虽然它像在 Ruby 语言中相当普遍,但是在 JavaScript 中扩展本地对象通常被认为是不太好的做法 ( 参考: Maintainable JavaScript: Don’t modify objects you don’t own
正如提到的,以上的代码的添加是十分安全的。它仅仅需要添 Array :: shuffle 如果它不存在,就要添加赋值运算符 (? =) 。这样,我们就不会重写到别人的代码,或是本地浏览器的方式。
同时,如果你认为你会使用很多的实用功能,可以考虑使用一个工具库,像 Lo-dash 。他们有很多功能,像跨浏览器的简洁高效的地图。 Underscore 也是一个不错的选择。
CoffeeScript 检测每个元素
检测每个元素
问题
你希望能够在特定的情况下检测出在数组中的每个元素。
解决方案
使用 Array.every (ECMAScript 5):
evens = (x for x in [0..10] by 2)
evens.every (x)-> x % 2 == 0
# => true
Array.every 被加入到 Mozilla 的 Javascript 1.6 ,ECMAScript 5 标准。如果你的浏览器支持,但仍无法实施 EC5 ,那么请检查 [ _.all from underscore.js]( http://documentcloud.github.io/underscore/) 。
对于一个真实例子,假设你有一个多选择列表,如下:
<select multiple id="my-select-list">
<option>1</option>
<option>2</option>
<option>Red Car</option>
<option>Blue Car</option>
</select>
现在你要验证用户只选择了数字。让我们利用 array.every :
validateNumeric = (item)->
parseFloat(item) == parseInt(item) && !isNaN(item)
values = $("#my-select-list").val()
values.every validateNumeric
讨论
这与 Ruby 中的 Array #all? 的方法很相似。
CoffeeScript 使用数组来交换变量
使用数组来交换变量
问题
你想通过数组来交换变量。
解决方案
使用 CoffeeScript 的解构赋值语法:
a = 1
b = 3
[a, b] = [b, a]
a
# => 3
b
# => 1
讨论
解构赋值可以不依赖临时变量实现变量值的交换。
这种语法特别适合在遍历数组的时候只想迭代最短数组的情况:
ray1 = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
ray2 = [ 5, 9, 14, 20 ]
intersection = (a, b) ->
[a, b] = [b, a] if a.length > b.length
value for value in a when value in b
intersection ray1, ray2
# => [ 5, 9 ]
intersection ray2, ray1
# => [ 5, 9 ]
CoffeeScript 对象数组
对象数组
问题
你想要得到一个与你的某些属性匹配的数组对象。
你有一系列的对象,如:
cats = [
{
name: "Bubbles"
favoriteFood: "mice"
age: 1
},
{
name: "Sparkle"
favoriteFood: "tuna"
},
{
name: "flyingCat"
favoriteFood: "mice"
age: 1
}
]
你想用某些特征来滤出想要的对象。例如:猫的位置 ({ 年龄: 1 }) 或者猫的位置 ({ 年龄: 1 , 最爱的食物: "老鼠" })
解决方案
你可以像这样来扩展数组:
Array::where = (query) ->
return [] if typeof query isnt "object"
hit = Object.keys(query).length
@filter (item) ->
match = 0
for key, val of query
match += 1 if item[key] is val
if match is hit then true else false
cats.where age:1
# => [ { name: 'Bubbles', favoriteFood: 'mice', age: 1 },{ name: 'flyingCat', favoriteFood: 'mice', age: 1 } ]
cats.where age:1, name: "Bubbles"
# => [ { name: 'Bubbles', favoriteFood: 'mice', age: 1 } ]
cats.where age:1, favoriteFood:"tuna"
# => []
讨论
这是一个确定的匹配。我们能够让匹配函数更加灵活:
Array::where = (query, matcher = (a,b) -> a is b) ->
return [] if typeof query isnt "object"
hit = Object.keys(query).length
@filter (item) ->
match = 0
for key, val of query
match += 1 if matcher(item[key], val)
if match is hit then true else false
cats.where name:"bubbles"
# => []
# it's case sensitive
cats.where name:"bubbles", (a, b) -> "#{ a }".toLowerCase() is "#{ b }".toLowerCase()
# => [ { name: 'Bubbles', favoriteFood: 'mice', age: 1 } ]
# now it's case insensitive
处理收集的一种方式可以被叫做 “find” ,但是像 underscore 或者 lodash 这些库把它叫做 “where” 。
CoffeeScript 类似 Python 的 zip 函数
类似 Python 的 zip 函数
问题
你想把多个数组连在一起,生成一个数组的数组。换句话说,你需要实现与 Python 中的 zip 函数类似的功能。 Python 的 zip 函数返回的是元组的数组,其中每个元组中包含着作为参数的数组中的第 i 个元素。
解决方案
使用下面的 CoffeeScript 代码:
# Usage: zip(arr1, arr2, arr3, ...)
zip = () ->
lengthArray = (arr.length for arr in arguments)
length = Math.max.apply(Math, lengthArray)
argumentLength = arguments.length
results = []
for i in [0...length]
semiResult = []
for arr in arguments
semiResult.push arr[i]
results.push semiResult
return results
zip([0, 1, 2, 3], [0, -1, -2, -3])
# => [[0, 0], [1, -1], [2, -2], [3, -3]]
CoffeeScript 日期和时间
CoffeeScript 计算复活节的日期
计算复活节的日期
问题
你需要在给出的年份中找到复活节的月份和日期。
解决方案
下面的函数返回数组有两个要素:复活节的月份( 1-12 )和日期。如果没有给出任何参数,给出的结果是当前的一年。这是 在CoffeeScript 的匿名公历算法实现的。
gregorianEaster = (year = (new Date).getFullYear()) ->
a = year % 19
b = ~~(year / 100)
c = year % 100
d = ~~(b / 4)
e = b % 4
f = ~~((b + 8) / 25)
g = ~~((b - f + 1) / 3)
h = (19 * a + b - d - g + 15) % 30
i = ~~(c / 4)
k = c % 4
l = (32 + 2 * e + 2 * i - h - k) % 7
m = ~~((a + 11 * h + 22 * l) / 451)
n = h + l - 7 * m + 114
month = ~~(n / 31)
day = (n % 31) + 1
[month, day]
讨论
Javascript 中的月份是 0-11 。getMonth() 查找的是三月的话将返回数字 2 ,这个函数会返回 3 。如果你想要这个功能是一致的,你可以修改这个函数。
该函数使用~~符号代替来 Math.floor() 。
gregorianEaster() # => [4, 24] (April 24th in 2011)
gregorianEaster 1972 # => [4, 2]
CoffeeScript 计算(美国和加拿大的)感恩节日期
计算(美国和加拿大的)感恩节日期
问题
你需要在给出的年份中找到感恩节的月份和日期。
解决方案
下面的函数返回给出年份的感恩节的日期。如果没有给出任何参数,给出的结果是当前年份。
美国的感恩节是十一月的第四个星期四。
thanksgivingDayUSA = (year = (new Date).getFullYear()) ->
first = new Date year, 10, 1
day_of_week = first.getDay()
22 + (11 - day_of_week) % 7
加拿大的感恩节是在十月的第二个周一。
thanksgivingDayCA = (year = (new Date).getFullYear()) ->
first = new Date year, 9, 1
day_of_week = first.getDay()
8 + (8 - day_of_week) % 7
讨论
thanksgivingDayUSA() #=> 24 (November 24th, 2011)
thanksgivingDayCA() # => 10 (October 10th, 2011)
thanksgivingDayUSA(2012) # => 22 (November 22nd)
thanksgivingDayCA(2012) # => 8 (October 8th)
这个想法很简单:
- 找出哪一天是以下各月份的第一天(美国十一月,加拿大十月)。
- 计算从那天起偏移到下一个工作日的量(美国星期四,加拿大星期一)。
- 将这个偏移量加上第一个可能的假期日期(第二十二个美国感恩节,第八个加拿大感恩节)。
CoffeeScript 计算两个日期中间的天数
计算两个日期中间的天数
问题
你需要找出两个日期间隔了几年,几个月,几天,几个小时,几分钟,几秒。
解决方案
利用 JavaScript 的日期计算函数 getTime() 。它提供了从 1970 年 1 月 1 日开始经过了多少毫秒。
DAY = 1000 * 60 * 60 * 24
d1 = new Date('02/01/2011')
d2 = new Date('02/06/2011')
days_passed = Math.round((d2.getTime() - d1.getTime()) / DAY)
讨论
使用毫秒,使计算时间跨度更容易,以避免日期的溢出错误。所以我们首先计算一天有多少毫秒。然后,给出了 2 个不同的日期,只须知道在 2 个日期之间的毫秒数,然后除以一天的毫秒数,这将得到 2 个不同的日期之间的天数。
如果你想计算出 2 个日期对象的小时数,你可以用毫秒的时间间隔除以一个小时有多少毫秒来得到。同样的可以得到几分钟和几秒。
HOUR = 1000 * 60 * 60
d1 = new Date('02/01/2011 02:20')
d2 = new Date('02/06/2011 05:20')
hour_passed = Math.round((d2.getTime() - d1.getTime()) / HOUR)
CoffeeScript 找到一个月中的最后一天
找到一个月中的最后一天
问题
你需要去找出一个月的最后一天,但是一年中的各月并没有一个固定时间表。
解决方案
利 用JavaScript 的日期下溢来找到给出月份的第一天:
now = new Date
lastDayOfTheMonth = new Date(1900+now.getYear(), now.getMonth()+1, 0)
讨论
JavaScript 的日期构造函数成功地处理溢出和下溢情况,使日期的计算变得很简单。鉴于这种简单操作,不需要担心一个给定的月份里有多少天;只需要用数学稍加推导。在十二月,以上的解决方案就是寻找当前年份的第十三个月的第 0 天日期,那么它就是下一年的一月一日,也计算出来今年十二月份 31 号的日期。
CoffeeScript 找到上一个月(或下一个月)
找到上一个月(或下一个月)
问题
你需要计算相关日期范围例如“上一个月”,“下一个月”。
解决方案
添加或减去当月的数字,JavaScript 的日期构造函数会修复数学知识。
# these examples were written in GMT-6
# Note that these examples WILL work in January!
now = new Date
# => "Sun, 08 May 2011 05:50:52 GMT"
lastMonthStart = new Date 1900+now.getYear(), now.getMonth()-1, 1
# => "Fri, 01 Apr 2011 06:00:00 GMT"
lastMonthEnd = new Date 1900+now.getYear(), now.getMonth(), 0
# => "Sat, 30 Apr 2011 06:00:00 GMT"
讨论
JavaScript 的日期对象会处理下溢和溢出的月和日,并将相应调整日期对象。例如,你可以要求寻找三月的第 42 天,你将获得 4 月 11 日。
JavaScript 对象存储日期为从 1900 开始的每年的年份数,月份为一个 0 到 11 的整数,日期为从 1 到 31 的一个整数。在上述解决方案中,上个月的起始日是要求在本年度某一个月的第一天,但月是从 -1 至 10 。如果月是 -1 的日期对象将实际返回为前一年的十二月:
lastNewYearsEve = new Date 1900+now.getYear(), -1, 31
# => "Fri, 31 Dec 2010 07:00:00 GMT"
对于溢出是同样的:
thirtyNinthOfFourteember = new Date 1900+now.getYear(), 13, 39
# => "Sat, 10 Mar 2012 07:00:00 GMT"
CoffeeScript 计算月球的相位
计算月球的相位
问题
你想找出月球的相位。
解决方案
以下代码提供了一种计算给出日期的月球相位计算方案:
# moonPhase.coffee
# Moon-phase calculator
# Roger W. Sinnott, Sky & Telescope, June 16, 2006
# http://www.skyandtelescope.com/observing/objects/javascript/moon_phases
#
# Translated to CoffeeScript by Mike Hatfield @WebCoding4Fun
proper_ang = (big) ->
tmp = 0
if big > 0
tmp = big / 360.0
tmp = (tmp - (~~tmp)) * 360.0
else
tmp = Math.ceil(Math.abs(big / 360.0))
tmp = big + tmp * 360.0
tmp
jdn = (date) ->
month = date.getMonth()
day = date.getDate()
year = date.getFullYear()
zone = date.getTimezoneOffset() / 1440
mm = month
dd = day
yy = year
yyy = yy
mmm = mm
if mm < 3
yyy = yyy - 1
mmm = mm + 12
day = dd + zone + 0.5
a = ~~( yyy / 100 )
b = 2 - a + ~~( a / 4 )
jd = ~~( 365.25 * yyy ) + ~~( 30.6001 * ( mmm+ 1 ) ) + day + 1720994.5
jd + b if jd > 2299160.4999999
moonElong = (jd) ->
dr = Math.PI / 180
rd = 1 / dr
meeDT = Math.pow((jd - 2382148), 2) / (41048480 * 86400)
meeT = (jd + meeDT - 2451545.0) / 36525
meeT2 = Math.pow(meeT, 2)
meeT3 = Math.pow(meeT, 3)
meeD = 297.85 + (445267.1115 * meeT) - (0.0016300 * meeT2) + (meeT3 / 545868)
meeD = (proper_ang meeD) * dr
meeM1 = 134.96 + (477198.8676 * meeT) + (0.0089970 * meeT2) + (meeT3 / 69699)
meeM1 = (proper_ang meeM1) * dr
meeM = 357.53 + (35999.0503 * meeT)
meeM = (proper_ang meeM) * dr
elong = meeD * rd + 6.29 * Math.sin( meeM1 )
elong = elong - 2.10 * Math.sin( meeM )
elong = elong + 1.27 * Math.sin( 2*meeD - meeM1 )
elong = elong + 0.66 * Math.sin( 2*meeD )
elong = proper_ang elong
elong = Math.round elong
moonNum = ( ( elong + 6.43 ) / 360 ) * 28
moonNum = ~~( moonNum )
if moonNum is 28 then 0 else moonNum
getMoonPhase = (age) ->
moonPhase = "new Moon"
moonPhase = "first quarter" if age > 3 and age < 11
moonPhase = "full Moon" if age > 10 and age < 18
moonPhase = "last quarter" if age > 17 and age < 25
if ((age is 1) or (age is 8) or (age is 15) or (age is 22))
moonPhase = "1 day past " + moonPhase
if ((age is 2) or (age is 9) or (age is 16) or (age is 23))
moonPhase = "2 days past " + moonPhase
if ((age is 3) or (age is 1) or (age is 17) or (age is 24))
moonPhase = "3 days past " + moonPhase
if ((age is 4) or (age is 11) or (age is 18) or (age is 25))
moonPhase = "3 days before " + moonPhase
if ((age is 5) or (age is 12) or (age is 19) or (age is 26))
moonPhase = "2 days before " + moonPhase
if ((age is 6) or (age is 13) or (age is 20) or (age is 27))
moonPhase = "1 day before " + moonPhase
moonPhase
MoonPhase = exports? and exports or @MoonPhase = {}
class MoonPhase.Calculator
getMoonDays: (date) ->
jd = jdn date
moonElong jd
getMoonPhase: (date) ->
jd = jdn date
getMoonPhase( moonElong jd )
讨论
此代码显示一个月球相位计算器对象的方法有两种。计算器 -> getmoonphase 将返回一用个文本表示的日期的月球相位。
这可以用在浏览器和 Node.js 中。
$ node
> var MoonPhase = require('./moonPhase.js');
undefined
> var calc = new MoonPhase.Calculator();
undefined
> calc.getMoonPhase(new Date());
'full moon'
> calc.getMoonPhase(new Date(1972, 6, 30));
'3 days before last quarter'
CoffeeScript 数学
CoffeeScript 数学常数
数学常数
问题
你需要使用常见的数学常数,比如 π 或者 e 。
解决方案
使用 Javascript 的 Math object 来提供通常需要的数学常数。
Math.PI
# => 3.141592653589793
# Note: Capitalization matters! This produces no output, it's undefined.
Math.Pi
# =>
Math.E
# => 2.718281828459045
Math.SQRT2
# => 1.4142135623730951
Math.SQRT1_2
# => 0.7071067811865476
# Natural log of 2. ln(2)
Math.LN2
# => 0.6931471805599453
Math.LN10
# => 2.302585092994046
Math.LOG2E
# => 1.4426950408889634
Math.LOG10E
# => 0.4342944819032518
讨论
另外一个例子是关于一个数学常数用于真实世界的问题,是数学章节有关[弧度和角度]( http://coffeescript-cookbook.github.io/chapters/math/radians-degrees)的部分。
CoffeeScript 更快的 Fibonacci 算法
更快的 Fibonacci 算法
问题
你想计算出 Fibonacci 数列中的数值 N ,但需迅速地算出结果。
解决方案
下面的方案(仍有需改进的地方)最初在 Robin Houston 的博客上被提出来。
这里给出一些关于该算法和改进方法的链接:
- http://bosker.wordpress.com/2011/04/29/the-worst-algorithm-in-the-world/
- http://www.math.rutgers.edu/~erowland/fibonacci
- http://jsfromhell.com/classes/bignumber
- http://www.math.rutgers.edu/~erowland/fibonacci
- http://bigintegers.blogspot.com/2010/11/square-division-power-square-root
- http://bugs.python.org/issue3451
以下的代码来源于:https://gist.github.com/1032685
###
Author: Jason Giedymin <jasong _a_t_ apache -dot- org>
http://www.jasongiedymin.com
https://github.com/JasonGiedymin
CoffeeScript Javascript 的快速 Fibonacci 代码是基于 Robin Houston 博客里的 python 代码。
见下面的链接。
我要介绍一下 Newtonian,Burnikel / Ziegle 和Binet 关于大数目框架算法的实现。
Todo:
- https://github.com/substack/node-bigint
- BZ and Newton mods.
- Timing
###
MAXIMUM_JS_FIB_N = 1476
fib_bits = (n) ->
#代表一个作为二进制数字阵列的整数
bits = []
while n > 0
[n, bit] = divmodBasic n, 2
bits.push bit
bits.reverse()
return bits
fibFast = (n) ->
#快速 Fibonacci
if n < 0
console.log "Choose an number >= 0"
return
[a, b, c] = [1, 0, 1]
for bit in fib_bits n
if bit
[a, b] = [(a+c)*b, b*b + c*c]
else
[a, b] = [a*a + b*b, (a+c)*b]
c = a + b
return b
divmodNewton = (x, y) ->
throw new Error "Method not yet implemented yet."
divmodBZ = () ->
throw new Error "Method not yet implemented yet."
divmodBasic = (x, y) ->
###
这里并没有什么特别的。如果可能的话,也许以后的版本将是Newtonian 或者 Burnikel / Ziegler 的。
###
return [(q = Math.floor x/y), (r = if x < y then x else x % y)]
start = (new Date).getTime();
calc_value = fibFast(MAXIMUM_JS_FIB_N)
diff = (new Date).getTime() - start;
console.log "[#{calc_value}] took #{diff} ms."
CoffeeScript 平方根倒数快速算法
平方根倒数快速算法
问题
你想快速计算某数的平方根倒数。
解决方案
在 Quake Ⅲ Arena 的源代码中,这个奇怪的算法对一个幻数进行整数运算,来计算平方根倒数的浮点近似值。
在 CoffeeScript 中,他使用经典原始的变量,以及由 Chris Lomont 发现的新的最优 32 位幻数。除此之外,还使用 64 位大小的幻数。
另一特征是可以通过控制牛顿迭代法的迭代次数来改变其精确度。
相比于传统的,该算法在性能上更胜一筹,这归功于使用的机器及其精确度。
运行的时候使用 coffee -c script.coffee 来编译 script:
然后复制粘贴编译的 JS 代码到浏览器的 JavaScript 控制台。
注意:你需要一个支持类型数组的浏览器
参考:
- ftp://ftp.idsoftware.com/idstuff/source/quake3-1.32b-source.zip
- http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf
- http://en.wikipedia.org/wiki/Newton%27s_method
- https://developer.mozilla.org/en/JavaScripttypedarrays
- http://en.wikipedia.org/wiki/Fastinversesquare_root
以下的代码来源于:https://gist.github.com/1036533
###
Author: Jason Giedymin <jasong _a_t_ apache -dot- org>
http://www.jasongiedymin.com
https://github.com/JasonGiedymin
在 Quake Ⅲ Arena 的源代码 [1](ftp://ftp.idsoftware.com/idstuff/source/quake3-1.32b-source.zip) 中,这个奇怪的算法对一个幻数进行整数运算,来计算平方根倒数的浮点近似值 [5](http://en.wikipedia.org/wiki/Fast_inverse_square_root)。
在 CoffeeScript 中,我使用经典原始的变量,以及由 Chris Lomont [2](http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf) 发现的新的最优 32 位幻数。除此之外,还使用 64 位大小的幻数。
另一特征是可以通过控制牛顿迭代法 [3](http://en.wikipedia.org/wiki/Newton%27s_method) 的迭代次数来改变其精确度。
相比于传统的,该算法在性能上更胜一筹,归功于使用的机器及其精确度。
运行的时候使用 coffee -c script.coffee 来编译 script:
然后复制粘贴编译的 JS 代码到浏览器的 JavaScript 控制台。
注意:你需要一个支持类型数组 [4](https://developer.mozilla.org/en/JavaScript_typed_arrays) 的浏览器
###
approx_const_quake_32 = 0x5f3759df # See [1]
approx_const_32 = 0x5f375a86 # See [2]
approx_const_64 = 0x5fe6eb50c7aa19f9 # See [2]
fastInvSqrt_typed = (n, precision=1) ->
# 使用类型数组。现在只能在浏览器中操作。
# Node.JS 的版本即将推出。
y = new Float32Array(1)
i = new Int32Array(y.buffer)
y[0] = n
i[0] = 0x5f375a86 - (i[0] >> 1)
for iter in [1...precision]
y[0] = y[0] * (1.5 - ((n * 0.5) * y[0] * y[0]))
return y[0]
### 单次运行示例
testSingle = () ->
example_n = 10
console.log("Fast InvSqrt of 10, precision 1: #{fastInvSqrt_typed(example_n)}")
console.log("Fast InvSqrt of 10, precision 5: #{fastInvSqrt_typed(example_n, 5)}")
console.log("Fast InvSqrt of 10, precision 10: #{fastInvSqrt_typed(example_n, 10)}")
console.log("Fast InvSqrt of 10, precision 20: #{fastInvSqrt_typed(example_n, 20)}")
console.log("Classic of 10: #{1.0 / Math.sqrt(example_n)}")
testSingle()
CoffeeScript 生成可预测的随机数
生成可预测的随机数
问题
你需要生成在一定范围内的随机数,但你也需要对发生器进行“生成种子”操作来提供可预测的值。
解决方案
编写你自己的随机数生成器。当然有很多方法可以做到这一点,这里给出一个简单的示例。 该发生器绝对不可以以加密为目的!
class Rand
# 如果没有种子创建,使用当前时间作为种子
constructor: (@seed) ->
# Knuth and Lewis' improvements to Park and Miller's LCPRNG
@multiplier = 1664525
@modulo = 4294967296 # 2**32-1;
@offset = 1013904223
unless @seed? && 0 <= seed < @modulo
@seed = (new Date().valueOf() * new Date().getMilliseconds()) % @modulo
# 设置新的种子值
seed: (seed) ->
@seed = seed
# 返回一个随机整数满足 0 <= n < @modulo
randn: ->
# new_seed = (a * seed + c) % m
@seed = (@multiplier*@seed + @offset) % @modulo
# 返回一个随机浮点满足 0 <= f < 1.0
randf: ->
this.randn() / @modulo
# 返回一个随机的整数满足 0 <= f < n
rand: (n) ->
Math.floor(this.randf() * n)
#返回一个随机的整数满足min <= f < max
rand2: (min, max) ->
min + this.rand(max-min)
讨论
JavaScript 和 CoffeeScript 都不提供可产生随机数的发生器。编写发生器对于我们来说将是一个挑战,在于权衡量的随机性与发生器的简单性。对随机性的全面讨论已超出了本书的范围。如需进一步阅读,可参考 Donald Kunth 的 The Art of Computer Programming 第 Ⅱ 卷第 3 章的 “ Random Numbers ” ,以及 Numerical Recipes in C 第二版本第 7 章的“ Random Numbers ”。
但是,对于这个随机数发生器只有简单的解释。这是一个线性同余伪随机数发生器,其运行源于一条数学公式 Ij+1 = (aIj+c) % m,其中 a 是乘数,c 是加法偏移量,m 是模数。每次请求随机数时就会执行很大的乘法和加法运算——这里的“很大”与密钥空间有关——得到的结果将以模数的形式被返回密钥空间。
这个发生器的周期为 232。虽然它绝对不能以加密为目的,但是对于最简单的随机性要求来说,它是相当足够的。randn() 在循环之前将遍历整个密钥空间,下一个数由上一个来确定。
如果你想修补这个发生器,强烈建议你去阅读 Knuth 的 The Art of Computer Programming 中的第 3 章。随机数生成是件很容易弄糟的事情,然而 Knuth 会解释如何区分好的和坏的随机数生成。
不要把发生器的输出结果变成模数。如果你需要一个整数的范围,应使用分割的方法。线性同余发生器的低位是不具有随机性的。特别的是,它总是从偶数种子产生奇数,反之亦然。所以如果你需要一个随机的 0 或者 1,不要使用:
# NOT random! Do not do this!
r.randn() % 2
因为你肯定得不到随机数字。反而,你应该使用 r.rand(2)。
CoffeeScript 生成随机数
生成随机数
问题
你需要生成在一定范围内的随机数。
解决方案
使用 JavaScript 的 Math.random() 来获得浮点数,满足 0<=X<1.0 。使用乘法和 Math.floor 得到在一定范围内的数字。
probability = Math.random()
0.0 <= probability < 1.0
# => true
# 注意百分位数不会达到 100。从 0 到 100 的范围实际上是 101 的跨度。
percentile = Math.floor(Math.random() * 100)
0 <= percentile < 100
# => true
dice = Math.floor(Math.random() * 6) + 1
1 <= dice <= 6
# => true
max = 42
min = -13
range = Math.random() * (max - min) + min
-13 <= range < 42
# => true
讨论
对于 JavaScript 来说,它更直接更快。
需要注意到 JavaScript 的 Math.random() 不能通过发生器生成随机数种子来得到特定值。详情可参考产生可预测的随机数。
产生一个从 0 到 n(不包括在内)的数,乘以 n。
产生一个从 1 到 n(包含在内)的数,乘以 n 然后加上 1。
CoffeeScript 转换弧度和度
转换弧度和度
问题
你需要实现弧度和度之间的转换。
解决方案
使用 JavaScript 的 Math.PI 和一个简单的公式来转换两者。
# 弧度转换成度
radiansToDegrees = (radians) ->
degrees = radians * 180 / Math.PI
radiansToDegrees(1)
# => 57.29577951308232
# 度转换成弧度
degreesToRadians = (degrees) ->
radians = degrees * Math.PI / 180
degreesToRadians(1)
# => 0.017453292519943295
CoffeeScript 一个随机整数函数
一个随机整数函数
问题
你想要获得两个整数(包含在内)之间的一个随机整数。
解决方案
使用以下的函数。
randomInt = (lower, upper) ->
[lower, upper] = [0, lower] unless upper? # 用一个参数调用
[lower, upper] = [upper, lower] if lower > upper # Lower 必须小于 upper
Math.floor(Math.random() * (upper - lower + 1) + lower) # 最后一条语句是一个返回值
(randomInt(1) for i in [0...10])
# => [0,1,1,0,0,0,1,1,1,0]
(randomInt(1, 10) for i in [0...10])
# => [7,3,9,1,8,5,4,10,10,8]
CoffeeScript 指数对数运算
指数对数运算
问题
你需要进行包含指数和对数的运算。
解决方案
使用 JavaScript 的 Math 对象来提供常用的数学函数。
# Math.pow(x, y) 返回 x^y
Math.pow(2, 4)
# => 16
# Math.exp(x) 返回 E^x ,被简写为 Math.pow(Math.E, x)
Math.exp(2)
# => 7.38905609893065
# Math.log returns the natural (base E) log
Math.log(5)
# => 1.6094379124341003
Math.log(Math.exp(42))
# => 42
# To get a log with some other base n, divide by Math.log(n)
Math.log(100) / Math.log(10)
# => 2
讨论
若想了解关于数学对象的更多信息,请参阅 Mozilla 开发者网络上的文档。另可参阅数学常量关于数学对象中各种常量的讨论。
CoffeeScript 方法
CoffeeScript 去抖动函数
去抖动函数
问题
你想只执行某个函数一次,在开始或结束时把多个连续的调用合并成一个简单的操作。
解决方案
使用一个命名函数:
debounce: (func, threshold, execAsap) ->
timeout = null
(args...) ->
obj = this
delayed = ->
func.apply(obj, args) unless execAsap
timeout = null
if timeout
clearTimeout(timeout)
else if (execAsap)
func.apply(obj, args)
timeout = setTimeout delayed, threshold || 100
mouseMoveHandler: (e) ->
@debounce((e) ->
# 只能在鼠标光标停止 300 毫秒后操作一次。
300)
someOtherHandler: (e) ->
@debounce((e) ->
# 只能在初次执行 250 毫秒后操作一次。
250, true)
讨论
可参阅 John Hann 的博客文章,了解 JavaScript 去抖动方法。
CoffeeScript 当函数括号不可选
当函数括号不可选
问题
你想要调用一个没有参数的函数,但不希望使用括号。
解决方案
不管怎样都使用括号。
另一个方法是使用 do 表示法,如下:
notify = -> alert "Hello, user!"
do notify if condition
编译成 JavaScript 则可表示为:
var notify;
notify = function() {
return alert("Hello, user!");
};
if (condition) {
notify();
}
讨论
这个方法与 Ruby 类似,在于都可以不使用括号来完成方法的调用。而不同点在于,CoffeeScript 把空的函数名作为函数的指针。这样以来,如果你不赋予一个方法任何参数,那么 CoffeeScript 将无法分辨你是想要调用函数还是把它作为引用。
这是好是坏呢?其实只是有所不同。它创造了一个意想不到的语法实例——括号并不总是可选的——但是它能让你流利地使用名字来传递和接收函数,这对于 Ruby 来说是难以实现的。
对于 CoffeeScript 来说,使用 do 表示法是一个巧妙的方法来克服括号使用恐惧症。尽管有部分人宁愿在函数调用中写出所有括号。
CoffeeScript 递归函数
递归函数
问题
你想在一个函数中调用相同的函数。
解决方案
使用一个命名函数:
ping = ->
console.log "Pinged"
setTimeout ping, 1000
若为未命名函数,则使用 @arguments.callee@:
delay = 1000
setTimeout((->
console.log "Pinged"
setTimeout arguments.callee, delay
), delay)
讨论
虽然 arguments.callee 允许未命名函数的递归,在内存密集型应用中占有一定优势,但是命名函数相对来说目的更加明确,也更易于代码的维护。
CoffeeScript 提示参数
提示参数
问题
你的函数将会被可变数量的参数所调用。
解决方案
使用 splat 。
loadTruck = (firstDibs, secondDibs, tooSlow...) ->
truck:
driversSeat: firstDibs
passengerSeat: secondDibs
trunkBed: tooSlow
loadTruck("Amanda", "Joel")
# => { truck: { driversSeat: "Amanda", passengerSeat: "Joel", trunkBed: [] } }
loadTruck("Amanda", "Joel", "Bob", "Mary", "Phillip")
# => { truck: { driversSeat: "Amanda", passengerSeat: "Joel", trunkBed: ["Bob", "Mary", "Phillip"] } }
使用尾部参数:
loadTruck = (firstDibs, secondDibs, tooSlow..., leftAtHome) ->
truck:
driversSeat: firstDibs
passengerSeat: secondDibs
trunkBed: tooSlow
taxi:
passengerSeat: leftAtHome
loadTruck("Amanda", "Joel", "Bob", "Mary", "Phillip", "Austin")
# => { truck: { driversSeat: 'Amanda', passengerSeat: 'Joel', trunkBed: [ 'Bob', 'Mary', 'Phillip' ] }, taxi: { passengerSeat: 'Austin' } }
loadTruck("Amanda")
# => { truck: { driversSeat: "Amanda", passengerSeat: undefined, trunkBed: [] }, taxi: undefined }
讨论
通过在函数其中的(不多于)一个参数之后添加一个省略号(...),CoffeeScript 能把所有不被其他命名参数采用的参数值整合进一个列表中。就算并没有提供命名参数,它也会制造一个空列表。
CoffeeScript 元编程
CoffeeScript 检测与构建丢失的函数
检测与构建丢失的函数
问题
你想要检测一个函数是否存在,如果不存在则构建该函数。(比如 Internet Explorer 8 的 ECMAScript 5 函数)。
解决方案
使用存在赋值运算符(?=)来把函数分配给类库的原型(使用 :: 简写),然后把它放于一个立即执行函数表达式中(do ->)使其含有所有变量。
do -> Array::filter ?= (callback) ->
element for element in this when callback element
array = [1..10]
array.filter (x) -> x > 5
# => [6,7,8,9,10]
讨论
在 JavaScript (同样地,在 CoffeeScript)中,对象都有一个原型成员,它定义了什么成员函数能够适用于基于该原型的所有对象。
在CoffeeScript中,你可以使用 :: 捷径来访问这个原型。所以如果你想要把过滤函数添加至数组类中,就执行 Array::filter = ... 语句。它能把过滤函数加至所有数组中。
但是,不要去覆盖一个在第一时间还没有构造的原型。比如,如果 Array::filter = ... 已经以快速本地形式存在于浏览器中,或者库制造者拥有其对于 Array::filter = ... 的独特版本,这样以来,你要么换一个慢速的 JavaScript 版本,要么打破这种依赖于其自身 Array::shuffle 的库。
你需要做的仅仅是在函数不存在的时候添加该函数。这就是存在赋值运算符(?=)的意义。如果我们执行 Array::filter = ... 语句,它会首先判断 Array::filter 是否已经存在。如果存在的话,它就会使用现在的版本。否则,它会添加你的版本。
最后,由于存在的赋值运算符在编译时会创建一些变量,我们会通过把它们封装在立即调用函数表达式( IIFE )中来简化代码。这将隐藏那些内部专用的变量,以防止泄露。所以假如我们写的函数已经存在,那么它将运行,基本上什么都没做然后退出,绝对不会对你的代码造成影响。但是假如我们写的函数并不存在,那么我们发送出去的仅是一个作为闭包的函数。所以只有你写的函数能够对代码产生影响。无论哪种方式,?= 的内部运行都会被隐藏。
举例
接下来,我们用上述的方法编译了 CoffeeScript 并附加了说明:
// (function(){ ... })() 是一个 IIFE, 使用 `do ->` 来编译它。
(function() {
// 它来自 `?=` 运算符,用来检查 Array.prototype.filter (`Array::filter`) 是否存在。
// 如果确实存在,我们把它设置给其自身,并返回。如果不存在,则把它设置给函数,并返回函数。
// The IIFE is only used to hide _base and _ref from the outside world.
var _base, _ref;
return (_ref = (_base = Array.prototype).filter) != null ? _ref : _base.filter = function(callback) {
// `element for element in this when callback element`
var element, _i, _len, _results;
_results = [];
for (_i = 0, _len = this.length; _i < _len; _i++) {
element = this[_i];
if (callback(element)) {
_results.push(element);
}
}
return _results;
};
// The end of the IIFE from `do ->`
})();
CoffeeScript 扩展内置对象
扩展内置对象
问题
你想要扩展一个类来增加新的函数或者替换旧的。
解决方案
使用 :: 把你的新函数分配到对象或者类的原型中。
String::capitalize = () ->
(this.split(/\s+/).map (word) -> word[0].toUpperCase() + word[1..-1].toLowerCase()).join ' '
"foo bar baz".capitalize()
# => 'Foo Bar Baz'
讨论
在 JavaScript (同样地,在 CoffeeScript )中,对象都有一个原型成员,它定义了什么成员函数能够适用于基于该原型的所有对象。在 CoffeeScript 中,你可以使用 :: 捷径来直接访问这个原型。
注意:虽然这种做法在很多种语言中相当普遍,比如 Ruby,但是在 JavaScript 中,扩展本地对象通常被认为是不好的做法(可参考:可维护的 JavaScript:不要修改你不拥有的对象;扩展内置的本地对象。对还是错?。)
CoffeeScript jQuery
CoffeeScript AJAX
AJAX
问题
你想要使用 jQuery 来调用 AJAX。
解决方案
$ ?= require 'jquery' # 由于 Node.js 的兼容性
$(document).ready ->
# 基本示例
$.get '/', (data) ->
$('body').append "Successfully got the page."
$.post '/',
userName: 'John Doe'
favoriteFlavor: 'Mint'
(data) -> $('body').append "Successfully posted to the page."
# 高级设置
$.ajax '/',
type: 'GET'
dataType: 'html'
error: (jqXHR, textStatus, errorThrown) ->
$('body').append "AJAX Error: #{textStatus}"
success: (data, textStatus, jqXHR) ->
$('body').append "Successful AJAX call: #{data}"
jQuery 1.5 和更新版本都增加了一种新补充的 API ,用于处理不同的回调。
request = $.get '/'
request.success (data) -> $('body').append "Successfully got the page again."
request.error (jqXHR, textStatus, errorThrown) -> $('body').append "AJAX Error: ${textStatus}."
讨论
其中的 jQuery 和 $ 变量可以互换使用。另请参阅 Callback bindings 。
CoffeeScript 回调绑定
回调绑定
问题
你想要把一个回调与一个对象绑定在一起。
解决方案
$ ->
class Basket
constructor: () ->
@products = []
$('.product').click (event) =>
@add $(event.currentTarget).attr 'id'
add: (product) ->
@products.push product
console.log @products
new Basket()
讨论
通过使用等号箭头(=>)取代正常箭头(->),函数将会自动与对象绑定,并可以访问 @- 可变量。
CoffeeScript 创建 jQuery 插件
创建 jQuery 插件
问题
你想用 CoffeeScript 来创建 jQuery 插件。
解决方案
# 参考 jQuery
$ = jQuery
# 给 jQuery 添加插件对象
$.fn.extend
# 把 pluginName 改成你的插件名字。
pluginName: (options) ->
# 默认设置
settings =
option1: true
option2: false
debug: false
# 合并选项与默认设置。
settings = $.extend settings, options
# Simple logger.
log = (msg) ->
console?.log msg if settings.debug
# _Insert magic here._
return @each ()->
log "Preparing magic show."
# 你可以使用你的设置了。
log "Option 1 value: #{settings.option1}"
讨论
这里有几个关于如何使用新插件的例子。
JavaScript
$("body").pluginName({
debug: true
});
CoffeeScript
$("body").pluginName
debug: true
CoffeeScript AJAX
AJAX
问题
你想要使用 jQuery 来调用 AJAX。
解决方案
$ ?= require 'jquery' # 由于 Node.js 的兼容性
$(document).ready ->
# 基本示例
$.get '/', (data) ->
$('body').append "Successfully got the page."
$.post '/',
userName: 'John Doe'
favoriteFlavor: 'Mint'
(data) -> $('body').append "Successfully posted to the page."
# 高级设置
$.ajax '/',
type: 'GET'
dataType: 'html'
error: (jqXHR, textStatus, errorThrown) ->
$('body').append "AJAX Error: #{textStatus}"
success: (data, textStatus, jqXHR) ->
$('body').append "Successful AJAX call: #{data}"
jQuery 1.5 和更新版本都增加了一种新补充的 API ,用于处理不同的回调。
request = $.get '/'
request.success (data) -> $('body').append "Successfully got the page again."
request.error (jqXHR, textStatus, errorThrown) -> $('body').append "AJAX Error: ${textStatus}."
讨论
其中的 jQuery 和 $ 变量可以互换使用。另请参阅 Callback bindings 。
CoffeeScript 不使用 jQuery 的 Ajax 请求
不使用 jQuery 的 Ajax 请求
问题
你想要通过 AJAX 来从你的服务器加载数据,而不使用 jQuery 库。
解决方案
你将使用本地的 XMLHttpRequest 对象。
通过一个按钮来打开一个简单的测试 HTML 页面。
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>XMLHttpRequest Tester</title>
</head>
<body>
<h1>XMLHttpRequest Tester</h1>
<button id="loadDataButton">Load Data</button>
<script type="text/javascript" src="XMLHttpRequest.js"></script>
</body>
</html>
当单击该按钮时,我们想给服务器发送 Ajax 请求以获取一些数据。对于该例子,我们使用一个 JSON 小文件。
// data.json
{
message: "Hello World"
}
然后,创建 CoffeeScript 文件来保存页面逻辑。此文件中的代码创建了一个函数,当点击加载数据按钮时将会调用该函数。
1 # XMLHttpRequest.coffee
2 loadDataFromServer = ->
3 req = new XMLHttpRequest()
4
5 req.addEventListener 'readystatechange', ->
6 if req.readyState is 4 # ReadyState Complete
7 successResultCodes = [200, 304]
8 if req.status in successResultCodes
9 data = eval '(' + req.responseText + ')'
10 console.log 'data message: ', data.message
11 else
12 console.log 'Error loading data...'
13
14 req.open 'GET', 'data.json', false
15 req.send()
16
17 loadDataButton = document.getElementById 'loadDataButton'
18 loadDataButton.addEventListener 'click', loadDataFromServer, false
讨论
在以上代码中,我们对 HTML 中按键进行了处理(第 16 行)以及添加了一个单击事件监听器(第 17 行)。在事件监听器中,我们把回调函数定义为 loadDataFromServer。
我们在第 2 行定义了 loadDataFromServer 回调的开头。
我们创建了一个 XMLHttpRequest 请求对象(第 3 行),并添加了一个 readystatechange 事件处理器。请求的 readyState 发生改变的那一刻,它就会被触发。
在事件处理器中,我们会检查判断是否满足 readyState=4,若等于则说明请求已经完成。然后检查请求的状态值。状态值为 200 或者 304 都代表着请求成功,其它则表示发生错误。
如果请求确实成功了,那我们就会对从服务器返回的 JSON 重新进行运算,然后把它分配给一个数据变量。此时,我们可以在需要的时候使用返回的数据。
在最后我们需要提出请求。
在第 13 行打开了一个 “GET” 请求来读取 data.json 文件。
在第 14 行把我们的请求发送至服务器。
旧版服务器支持
如果你的应用需要使用旧版本的 Internet Explorer ,你需确保 XMLHttpRequest 对象存在。为此,你可以在创建 XMLHttpRequest 实例之前输入以下代码。
if (typeof @XMLHttpRequest == "undefined")
console.log 'XMLHttpRequest is undefined'
@XMLHttpRequest = ->
try
return new ActiveXObject("Msxml2.XMLHTTP.6.0")
catch error
try
return new ActiveXObject("Msxml2.XMLHTTP.3.0")
catch error
try
return new ActiveXObject("Microsoft.XMLHTTP")
catch error
throw new Error("This browser does not support XMLHttpRequest.")
这段代码确保了 XMLHttpRequest 对象在全局命名空间中可用。
CoffeeScript 正则表达式
CoffeeScript 使用 Heregexes
使用 Heregexes
问题
你需要写一个复杂的正则表达式。
解决方案
使用 CoffeeScript 的 “heregexes” ——可以忽视内部空白字符并可以包含注释的扩展正则表达式。
pattern = ///
^\(?(\d{3})\)? # 采集区域代码,忽略可选的括号
[-\s]?(\d{3}) # 采集前缀,忽略可选破折号或空格
-?(\d{4}) # 采集行号,忽略可选破折号
///
[area_code, prefix, line] = "(555)123-4567".match(pattern)[1..3]
# => ['555', '123', '4567']
讨论
通过打破复杂的正则表达式和注释重点部分,它们变得更加容易去辨认和维护。例如,现在这是一个相当明显的做法去改变正则表达式以容许前缀和行号之间存在可选的空间。
heregexes 中的空白字符
空白字符在 heregexes 中是被忽视的——所以如果要为 ASCII 空格匹配字符,你应该怎么做呢?
我们的解决方案是使用 @\s@ 字符组,它能够匹配空格,制表符和换行符。假如你只想匹配一个空格,你需要使用 \X20 来表示字面上的 ASCII 空格。
CoffeeScript 使用 HTML 命名实体替换 HTML 标签
使用 HTML 命名实体替换 HTML 标签
问题
你需要使用命名实体来替代 HTML 标签:
<br/> => <br/>
解决方案
htmlEncode = (str) ->
str.replace /[&<>"']/g, ($0) ->
"&" + {"&":"amp", "<":"lt", ">":"gt", '"':"quot", "'":"#39"}[$0] + ";"
htmlEncode('<a href="http://bn.com">Barnes & Noble</a>')
# => '<a href="http://bn.com">Barnes & Noble</a>'
讨论
可能有更好的途径去执行上述方法。
CoffeeScript 替换子字符串
替换子字符串
问题
你需要用另一个值替换字符串的一部分。
解决方案
使用 JavaScript 的 replace 方法。它与给定字符串匹配,并返回已编辑的字符串。
第一个版本需要 2 个参数:模式和字符串替换
"JavaScript is my favorite!".replace /Java/, "Coffee"
# => 'CoffeeScript is my favorite!'
"foo bar baz".replace /ba./, "foo"
# => 'foo foo baz'
"foo bar baz".replace /ba./g, "foo"
# => 'foo foo foo'
第二个版本需要 2 个参数:模式和回调函数
"CoffeeScript is my favorite!".replace /(\w+)/g, (match) ->
match.toUpperCase()
# => 'COFFEESCRIPT IS MY FAVORITE!'
每次匹配需要调用回调函数,并且匹配值作为参数传给回调函数。
讨论
正则表达式是一种强有力的方式来匹配和替换字符串。
CoffeeScript 查找子字符串
查找子字符串
问题
你需要搜索一个字符串,并返回匹配的起始位置或匹配值本身。
解决方案
有几种使用正则表达式的方法来实现这个功能。其中一些方法被称为 RegExp 模式或对象还有一些方法被称为 String 对象。
RegExp 对象
第一种方式是在 RegExp 模式或对象中调用 test 方法。test 方法返回一个布尔值:
match = /sample/.test("Sample text")
# => false
match = /sample/i.test("Sample text")
# => true
下一种方式是在 RegExp 模式或对象中调用 exec 方法。exec 方法返回一个匹配信息的数组或空值:
match = /s(amp)le/i.exec "Sample text"
# => [ 'Sample', 'amp', index: 0, input: 'Sample text' ]
match = /s(amp)le/.exec "Sample text"
# => null
String 对象
match 方法使给定的字符串与表达式对象匹配。有 “g” 标识的返回一个包含匹配项的数组,没有 “g” 标识的仅返回第一个匹配项或如果没有找到匹配项则返回 null 。
"Watch out for the rock!".match(/r?or?/g)
# => [ 'o', 'or', 'ro' ]
"Watch out for the rock!".match(/r?or?/)
# => [ 'o', index: 6, input: 'Watch out for the rock!' ]
"Watch out for the rock!".match(/ror/)
# => null
search 方法以字符串匹配正则表达式,且如果找到的话返回匹配的起始位置,未找到的话则返回 -1 。
"Watch out for the rock!".search /for/
# => 10
"Watch out for the rock!".search /rof/
# => -1
讨论
正则表达式是一种可用来测试和匹配子字符串的强大的方法。
CoffeeScript 网络
CoffeeScript 客户端
客户端
问题
你想使用网络上提供的服务。
解决方案
创建一个基本的 TCP 客户机。
在 Node.js 中
net = require 'net'
domain = 'localhost'
port = 9001
connection = net.createConnection port, domain
connection.on 'connect', () ->
console.log "Opened connection to #{domain}:#{port}."
connection.on 'data', (data) ->
console.log "Received: #{data}"
connection.end()
使用示例
可访问 Basic Server :
$ coffee basic-client.coffee
Opened connection to localhost:9001
Received: Hello, World!
讨论
最重要的工作发生在 connection.on 'data' 处理过程中,客户端接收到来自服务器的响应并最有可能安排对它的应答。
另请参阅 Basic Server,Bi-Directional Client 和 Bi-Directional Server 。
练习
- 根据命令行参数或配置文件为选定的目标域和端口添加支持。
CoffeeScript HTTP 客户端
HTTP 客户端
问题
你想创建一个 HTTP 客户端。
解决方案
在这个方法中,我们将使用 node.js's HTTP 库。我们将从一个简单的客户端 GET 请求示例返回计算机的外部 IP 。
关于 GET
http = require 'http'
http.get { host: 'www.google.com' }, (res) ->
console.log res.statusCode
get 函数,从 node.js's http 模块,发出一个 GET 请求到一个 http 服务器。响应是以回调的形式,我们可以在一个函数中处理。这个例子仅仅输出响应状态代码。检查一下:
$ coffee http-client.coffee
200
我的 IP 是什么?
如果你是在一个类似局域网的依赖于 NAT 的网络中,你可能会面临找出外部 IP 地址的问题。让我们为这个问题写一个小的 coffeescript 。
http = require 'http'
http.get { host: 'checkip.dyndns.org' }, (res) ->
data = ''
res.on 'data', (chunk) ->
data += chunk.toString()
res.on 'end', () ->
console.log data.match(/([0-9]+\.){3}[0-9]+/)[0]
我们可以从监听 'data' 事件的结果对象中得到数据,知道它结束了一次 'end' 的触发事件。当这种情况发生时,我们可以做一个简单的正则表达式来匹配我们提取的 IP 地址。试一试:
$ coffee http-client.coffee
123.123.123.123
讨论
请注意 http.get 是 http.request 的快捷方式。后者允许您使用不同的方法发出 HTTP 请求,如 POST 或 PUT。
在这个问题上的 API 和整体信息,检查 node.js's http 和 https 文档页面。此外, HTTP spec 可能派上用场。
练习
- 为键值存储 HTTP 服务器创建一个客户端,使用基本的 HTTP 服务器方法。
CoffeeScript 基本的 HTTP 服务器
基本的 HTTP 服务器
问题
你想在网络上创建一个 HTTP 服务器。在这个方法中,我们将逐步从最小的服务器成为一个功能键值存储。
解决方案
我们将使用 node.js HTTP 库并在 Coffeescript 中创建最简单的 web 服务器。
开始 'hi\n'
我们可以通过导入 node.js HTTP 模块开始。这会包含 createServer ,一个简单的请求处理程序返回 HTTP 服务器。我们可以使用该服务器监听 TCP 端口。
http = require 'http'
server = http.createServer (req, res) -> res.end 'hi\n'
server.listen 8000
要运行这个例子,只需放在一个文件中并运行它。你可以用 ctrl-c 终止它。我们可以使用 curl 命令测试它,可用在大多数 *nix 平台:
$ curl -D - http://localhost:8000/
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked
hi
发生什么了?
让我们一点点来反馈服务器上发生的事情。这时,我们可以友好的对待用户并提供他们一些 HTTP 头文件。
http = require 'http'
server = http.createServer (req, res) ->
console.log req.method, req.url
data = 'hi\n'
res.writeHead 200,
'Content-Type': 'text/plain'
'Content-Length': data.length
res.end data
server.listen 8000
再次尝试访问它,但是这一次使用不同的 URL 路径,比如 http://localhost:8000/coffee 。你会看到这样的服务器控制台:
$ coffee http-server.coffee
GET /
GET /coffee
GET /user/1337
得到的东西
假如我们的网络服务器能够保存一些数据会怎么样?我们将在通过 GET 方法 请求检索的元素中设法想出一个简单的键值存储。并提供一个关键路径,服务器将请求返回相应的值,如果不存在则返回 404 错误。
http = require 'http'
store = # we'll use a simple object as our store
foo: 'bar'
coffee: 'script'
server = http.createServer (req, res) ->
console.log req.method, req.url
value = store[req.url[1..]]
if not value
res.writeHead 404
else
res.writeHead 200,
'Content-Type': 'text/plain'
'Content-Length': value.length + 1
res.write value + '\n'
res.end()
server.listen 8000
我们可以试试几种 url,看看它们如何回应:
$ curl -D - http://localhost:8000/coffee
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 7
Connection: keep-alive
script
$ curl -D - http://localhost:8000/oops
HTTP/1.1 404 Not Found
Connection: keep-alive
Transfer-Encoding: chunked
使用你的头文件
text/plain 是站不住脚的。如果我们使用 application/json 或 text/xml 会怎么样?同时,我们的存储检索过程也可以用一点重构——一些异常的抛出 & 处理怎么样? 来看看我们能想出什么:
http = require 'http'
# known mime types
[any, json, xml] = ['*/*', 'application/json', 'text/xml']
# gets a value from the db in format [value, contentType]
get = (store, key, format) ->
value = store[key]
throw 'Unknown key' if not value
switch format
when any, json then [JSON.stringify({ key: key, value: value }), json]
when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml]
else throw 'Unknown format'
store =
foo: 'bar'
coffee: 'script'
server = http.createServer (req, res) ->
console.log req.method, req.url
try
key = req.url[1..]
[value, contentType] = get store, key, req.headers.accept
code = 200
catch error
contentType = 'text/plain'
value = error
code = 404
res.writeHead code,
'Content-Type': contentType
'Content-Length': value.length + 1
res.write value + '\n'
res.end()
server.listen 8000
这个服务器仍然会返回一个匹配给定键的值,如果不存在则返回 404 错误。但它根据标头 Accept 将响应在 JSON 或 XML 结构中。可亲眼看一下:
$ curl http://localhost:8000/
Unknown key
$ curl http://localhost:8000/coffee
{"key":"coffee","value":"script"}
$ curl -H "Accept: text/xml" http://localhost:8000/coffee
<key>coffee</key>
<value>script</value>
$ curl -H "Accept: image/png" http://localhost:8000/coffee
Unknown format
你需要有所返回
我们的最后一步是提供客户端存储数据的能力。我们将通过监听 POST 请求来保持 RESTiness。
http = require 'http'
# known mime types
[any, json, xml] = ['*/*', 'application/json', 'text/xml']
# gets a value from the db in format [value, contentType]
get = (store, key, format) ->
value = store[key]
throw 'Unknown key' if not value
switch format
when any, json then [JSON.stringify({ key: key, value: value }), json]
when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml]
else throw 'Unknown format'
# puts a value in the db
put = (store, key, value) ->
throw 'Invalid key' if not key or key is ''
store[key] = value
store =
foo: 'bar'
coffee: 'script'
# helper function that responds to the client
respond = (res, code, contentType, data) ->
res.writeHead code,
'Content-Type': contentType
'Content-Length': data.length
res.write data
res.end()
server = http.createServer (req, res) ->
console.log req.method, req.url
key = req.url[1..]
contentType = 'text/plain'
code = 404
switch req.method
when 'GET'
try
[value, contentType] = get store, key, req.headers.accept
code = 200
catch error
value = error
respond res, code, contentType, value + '\n'
when 'POST'
value = ''
req.on 'data', (chunk) -> value += chunk
req.on 'end', () ->
try
put store, key, value
value = ''
code = 200
catch error
value = error + '\n'
respond res, code, contentType, value
server.listen 8000
在一个 POST 请求中注意数据是如何接收的。通过在“数据”和“结束”请求对象的事件中附上一些处理程序,我们最终能够从客户端缓冲和保存数据。
$ curl -D - http://localhost:8000/cookie
HTTP/1.1 404 Not Found # ...
Unknown key
$ curl -D - -d "monster" http://localhost:8000/cookie
HTTP/1.1 200 OK # ...
$ curl -D - http://localhost:8000/cookie
HTTP/1.1 200 OK # ...
{"key":"cookie","value":"monster"}
讨论
给 http.createServer 一个函数 (request,response) - >…… 它将返回一个服务器对象,我们可以用它来监听一个端口。让服务器与 request 和 response 对象交互。使用 server.listen 8000 监听端口 8000。
在这个问题上的 API 和整体信息,参考 node.js http 和 https 文档页面。此外,HTTP spec 可能派上用场。
练习
在服务器和开发人员之间创建一个层,允许开发人员做类似的事情:
server = layer.createServer
'GET /': (req, res) ->
...
'GET /page': (req, res) ->
...
'PUT /image': (req, res) ->
...
CoffeeScript 服务器
服务器
问题
你想在网络上提供一个服务器。
解决方案
创建一个基本的 TCP 服务器。
在 Node.js 中
net = require 'net'
domain = 'localhost'
port = 9001
server = net.createServer (socket) ->
console.log "Received connection from #{socket.remoteAddress}"
socket.write "Hello, World!\n"
socket.end()
console.log "Listening to #{domain}:#{port}"
server.listen port, domain
使用示例
可访问 Basic Client:
$ coffee basic-server.coffee
Listening to localhost:9001
Received connection from 127.0.0.1
Received connection from 127.0.0.1
[...]
讨论
函数将为每个客户端新连接的新插口传递给 @net.createServer@ 。基本的服务器与访客只进行简单地交互,但是复杂的服务器会将插口连上一个专用的处理程序,然后返回等待下一个用户的任务。
另请参阅 Basic Client,Bi-Directional Server 和 Bi-Directional Client。
练习
- 为选定的目标域和基于命令行参数或配置文件的端口添加支持。
CoffeeScript 双向客户端
双向客户端
问题
你想通过网络提供持续的服务,与客户保持持续的联系。
解决方案
创建一个双向 TCP 客户机。
在 Node.js 中
net = require 'net'
domain = 'localhost'
port = 9001
ping = (socket, delay) ->
console.log "Pinging server"
socket.write "Ping"
nextPing = -> ping(socket, delay)
setTimeout nextPing, delay
connection = net.createConnection port, domain
connection.on 'connect', () ->
console.log "Opened connection to #{domain}:#{port}"
ping connection, 2000
connection.on 'data', (data) ->
console.log "Received: #{data}"
connection.on 'end', (data) ->
console.log "Connection closed"
process.exit()
使用示例
$ coffee bi-directional-client.coffee
Opened connection to localhost:9001
Pinging server
Received: You have 0 peers on this server
Pinging server
Received: You have 0 peers on this server
Pinging server
Received: You have 1 peer on this server
[...]
Connection closed
讨论
这个特殊示例发起与服务器联系并在 @connection.on 'connect'@ 处理程序中开启对话。大量的工作在一个真正的用户中,然而 @connection.on 'data'@ 处理来自服务器的输出。@ping@ 函数递归是为了说明连续与服务器通信可能被真实的用户移除。
另请参阅 Bi-Directional Server,Basic Client 和 Basic Server。
练习
- 为选定的目标域和基于命令行参数或配置文件的端口添加支持。
CoffeeScript 双向服务器
双向服务器
问题
你想通过网络提供持续的服务,与客户保持持续的联系。
解决方案
创建一个双向 TCP 服务器。
在 Node.js 中
net = require 'net'
domain = 'localhost'
port = 9001
server = net.createServer (socket) ->
console.log "New connection from #{socket.remoteAddress}"
socket.on 'data', (data) ->
console.log "#{socket.remoteAddress} sent: #{data}"
others = server.connections - 1
socket.write "You have #{others} #{others == 1 and "peer" or "peers"} on this server"
console.log "Listening to #{domain}:#{port}"
server.listen port, domain
使用示例
$ coffee bi-directional-server.coffee
Listening to localhost:9001
New connection from 127.0.0.1
127.0.0.1 sent: Ping
127.0.0.1 sent: Ping
127.0.0.1 sent: Ping
[...]
讨论
大部分工作在 @socket.on 'data'@ 中 ,处理所有的输入端。真正的服务器可能会将数据传给另一个函数处理并生成任何响应以便源程序处理。
练习
- 为选定的目标域和基于命令行参数或配置文件的端口添加支持。
CoffeeScript 设计模式
CoffeeScript 适配器模式
适配器模式
问题
想象你去国外旅行,一旦你意识到你的电源线插座与酒店房间墙上的插座不兼容时,幸运的是你记得带你的电源适配器。它将一边连接你的电源线插座另一边连接墙壁插座,允许它们之间进行通信。
同样的情况也可能会出现在代码中,当两个 ( 或更多 ) 实例 ( 类、模块等 ) 想跟对方通信,但其通信协议 ( 例如,他们所使用的语言交流 ) 不同。在这种情况下,Adapter 模式更方便。它会充当翻译,从一边到另一边。
解决方案
# a fragment of 3-rd party grid component
class AwesomeGrid
constructor: (@datasource)->
@sort_order = 'ASC'
@sorter = new NullSorter # in this place we use NullObject pattern (another useful pattern)
setCustomSorter: (@customSorter) ->
@sorter = customSorter
sort: () ->
@datasource = @sorter.sort @datasource, @sort_order
# don't forget to change sort order
class NullSorter
sort: (data, order) -> # do nothing; it is just a stub
class RandomSorter
sort: (data)->
for i in [data.length-1..1] #let's shuffle the data a bit
j = Math.floor Math.random() * (i + 1)
[data[i], data[j]] = [data[j], data[i]]
return data
class RandomSorterAdapter
constructor: (@sorter) ->
sort: (data, order) ->
@sorter.sort data
agrid = new AwesomeGrid ['a','b','c','d','e','f']
agrid.setCustomSorter new RandomSorterAdapter(new RandomSorter)
agrid.sort() # sort data with custom sorter through adapter
讨论
当你要组织两个具有不同接口的对象之间的交互时,适配器是有用的。它可以当你使用第三方库或者使用遗留代码时使用。在任何情况下小心使用适配器:它可以是有用的,但它也可以导致设计错误。
CoffeeScript 桥接模式
桥接模式
问题
你需要为代码保持一个可靠的接口,可以经常变化或者在多种实现间转换。
解决方案
使用桥接模式作为不同的实现和剩余代码的中间体。
假设你开发了一个浏览器的文本编辑器保存到云。然而,现在你需要通过独立客户端的端口将其在本地保存。
class TextSaver
constructor: (@filename, @options) ->
save: (data) ->
class CloudSaver extends TextSaver
constructor: (@filename, @options) ->
super @filename, @options
save: (data) ->
# Assuming jQuery
# Note the fat arrows
$( =>
$.post "#{@options.url}/#{@filename}", data, =>
alert "Saved '#{data}' to #{@filename} at #{@options.url}."
)
class FileSaver extends TextSaver
constructor: (@filename, @options) ->
super @filename, @options
@fs = require 'fs'
save: (data) ->
@fs.writeFile @filename, data, (err) => # Note the fat arrow
if err? then console.log err
else console.log "Saved '#{data}' to #{@filename} in #{@options.directory}."
filename = "temp.txt"
data = "Example data"
saver = if window?
new CloudSaver filename, url: 'http://localhost' # => Saved "Example data" to temp.txt at http://localhost
else if root?
new FileSaver filename, directory: './' # => Saved "Example data" to temp.txt in ./
saver.save data
讨论
桥接模式可以帮助你将特定实现的代码置于看不见的地方,这样你就可以专注于你的程序中的具体代码。在上面的示例中,应用程序的其余部分可以称为 saver.save data ,不考虑文件的最终结束。
CoffeeScript 生成器模式
生成器模式
问题
你需要准备一个复杂的、多部分的对象,你希望操作不止一次或有不同的配置。
解决方案
创建一个生成器封装对象的产生过程。
Todo.txt 格式提供了一个先进的但还是纯文本的方法来维护待办事项列表。手工输入每个项目有损耗且容易出错,然而 TodoTxtBuilder 类可以解决我们的麻烦:
class TodoTxtBuilder
constructor: (defaultParameters={ }) ->
@date = new Date(defaultParameters.date) or new Date
@contexts = defaultParameters.contexts or [ ]
@projects = defaultParameters.projects or [ ]
@priority = defaultParameters.priority or undefined
newTodo: (description, parameters={ }) ->
date = (parameters.date and new Date(parameters.date)) or @date
contexts = @contexts.concat(parameters.contexts or [ ])
projects = @projects.concat(parameters.projects or [ ])
priorityLevel = parameters.priority or @priority
createdAt = [date.getFullYear(), date.getMonth()+1, date.getDate()].join("-")
contextNames = ("@#{context}" for context in contexts when context).join(" ")
projectNames = ("+#{project}" for project in projects when project).join(" ")
priority = if priorityLevel then "(#{priorityLevel})" else ""
todoParts = [priority, createdAt, description, contextNames, projectNames]
(part for part in todoParts when part.length > 0).join " "
builder = new TodoTxtBuilder(date: "10/13/2011")
builder.newTodo "Wash laundry"
# => '2011-10-13 Wash laundry'
workBuilder = new TodoTxtBuilder(date: "10/13/2011", contexts: ["work"])
workBuilder.newTodo "Show the new design pattern to Lucy", contexts: ["desk", "xpSession"]
# => '2011-10-13 Show the new design pattern to Lucy @work @desk @xpSession'
workBuilder.newTodo "Remind Sean about the failing unit tests", contexts: ["meeting"], projects: ["compilerRefactor"], priority: 'A'
# => '(A) 2011-10-13 Remind Sean about the failing unit tests @work @meeting +compilerRefactor'
讨论
TodoTxtBuilder 类负责所有文本的生成,让程序员关注每个工作项的独特元素。此外,命令行工具或 GUI 可以插入这个代码且之后仍然保持支持,提供轻松、更高版本的格式。
前期建设
并不是每次创建一个新的实例所需的对象都要从头开始,我们将负担转移到一个单独的对象,可以在对象创建过程中进行调整。
builder = new TodoTxtBuilder(date: "10/13/2011")
builder.newTodo "Order new netbook"
# => '2011-10-13 Order new netbook'
builder.projects.push "summerVacation"
builder.newTodo "Buy suntan lotion"
# => '2011-10-13 Buy suntan lotion +summerVacation'
builder.contexts.push "phone"
builder.newTodo "Order tickets"
# => '2011-10-13 Order tickets @phone +summerVacation'
delete builder.contexts[0]
builder.newTodo "Fill gas tank"
# => '2011-10-13 Fill gas tank +summerVacation'
练习
- 扩大 project- 和 context-tag 生成代码来过滤掉重复的条目。
- 一些 Todo.txt 用户喜欢在任务描述中插入项目和上下文的标签。添加代码来识别这些标签和过滤器的结束标记。
CoffeeScript 命令模式
命令模式
问题
你需要让另一个对象处理你自己的可执行的代码。
解决方案
使用 Command pattern 传递函数的引用。
# Using a private variable to simulate external scripts or modules
incrementers = (() ->
privateVar = 0
singleIncrementer = () ->
privateVar += 1
doubleIncrementer = () ->
privateVar += 2
commands =
single: singleIncrementer
double: doubleIncrementer
value: -> privateVar
)()
class RunsAll
constructor: (@commands...) ->
run: -> command() for command in @commands
runner = new RunsAll(incrementers.single, incrementers.double, incrementers.single, incrementers.double)
runner.run()
incrementers.value() # => 6
讨论
以函数作为一级的对象且从 Javascript 函数的变量范围中继承,CoffeeScript 使语言模式几乎看不出来。事实上,任何函数传递回调函数可以作为一个命令。
jqXHR 对象返回 jQuery AJAX 方法使用此模式。
jqxhr = $.ajax
url: "/"
logMessages = ""
jqxhr.success -> logMessages += "Success!\n"
jqxhr.error -> logMessages += "Error!\n"
jqxhr.complete -> logMessages += "Completed!\n"
# On a valid AJAX request:
# logMessages == "Success!\nCompleted!\n"
CoffeeScript 修饰模式
修饰模式
问题
你有一组数据,需要在多个过程、可能变换的方式下处理。
解决方案
使用修饰模式来构造如何更改应用。
miniMarkdown = (line) ->
if match = line.match /^(#+)\s*(.*)$/
headerLevel = match[1].length
headerText = match[2]
"<h#{headerLevel}>#{headerText}</h#{headerLevel}>"
else
if line.length > 0
"<p>#{line}</p>"
else
''
stripComments = (line) ->
line.replace /\s*\/\/.*$/, '' # Removes one-line, double-slash C-style comments
class TextProcessor
constructor: (@processors) ->
reducer: (existing, processor) ->
if processor
processor(existing or '')
else
existing
processLine: (text) ->
@processors.reduce @reducer, text
processString: (text) ->
(@processLine(line) for line in text.split("\n")).join("\n")
exampleText = '''
# A level 1 header
A regular line
// a comment
## A level 2 header
A line // with a comment
'''
processor = new TextProcessor [stripComments, miniMarkdown]
processor.processString exampleText
# => "<h1>A level 1 header</h1>\n<p>A regular line</p>\n\n<h2>A level 2 header</h2>\n<p>A line</p>"
结果
<h1>A level 1 header</h1>
<p>A regular line</p>
<h2>A level 1 header</h2>
<p>A line</p>
讨论
TextProcessor 服务有修饰的作用,可将个人、专业文本处理器绑定在一起。这使 miniMarkdown 和 stripComments 组件只专注于处理一行文本。未来的开发人员只需要编写函数返回一个字符串,并将它添加到阵列的处理器即可。
我们甚至可以修改现有的修饰对象动态:
smilies =
':)' : "smile"
':D' : "huge_grin"
':(' : "frown"
';)' : "wink"
smilieExpander = (line) ->
if line
(line = line.replace symbol, "<img src='#{text}.png' alt='#{text}' />") for symbol, text of smilies
line
processor.processors.unshift smilieExpander
processor.processString "# A header that makes you :) // you may even laugh"
# => "<h1>A header that makes you <img src='smile.png' alt='smile' /></h1>"
processor.processors.shift()
# => "<h1>A header that makes you :)</h1>"
CoffeeScript 工厂方法模式
工厂方法模式
问题
直到开始运行你才知道需要的是什么种类的对象。
解决方案
使用 工厂方法(Factory Method) 模式和选择对象都是动态生成的。
你需要将一个文件加载到编辑器,但是直到用户选择文件时你才知道它的格式。一个类使用工厂方法 ( Factory Method ) 模式可以根据文件的扩展名提供不同的解析器。
class HTMLParser
constructor: ->
@type = "HTML parser"
class MarkdownParser
constructor: ->
@type = "Markdown parser"
class JSONParser
constructor: ->
@type = "JSON parser"
class ParserFactory
makeParser: (filename) ->
matches = filename.match /\.(\w*)$/
extension = matches[1]
switch extension
when "html" then new HTMLParser
when "htm" then new HTMLParser
when "markdown" then new MarkdownParser
when "md" then new MarkdownParser
when "json" then new JSONParser
factory = new ParserFactory
factory.makeParser("example.html").type # => "HTML parser"
factory.makeParser("example.md").type # => "Markdown parser"
factory.makeParser("example.json").type # => "JSON parser"
讨论
在这个示例中,你可以关注解析的内容,忽略细节文件的格式。更先进的工厂方法,例如,搜索版本控制文件中的数据本身,然后返回一个更精确的解析器(例如,返回一个 HTML5 解析器而不是 HTML v4 解析器)。
CoffeeScript 解释器模式
解释器模式
问题
其他人需要以控制方式运行你的一部分代码。相对地,你选择的语言不能以一种简洁的方式表达问题域。
解决方案
使用解释器模式来创建一个你翻译为特定代码的领域特异性语言( domain-specific language )。
我们来做个假设,例如用户希望在你的应用程序中执行数学运算。你可以让他们正向运行代码来演算指令(eval)但这会让他们运行任意代码。相反,你可以提供一个小型的“堆栈计算器(stack calculator)”语言,用来做单独分析,以便只运行数学运算,同时报告更有用的错误信息。
class StackCalculator
parseString: (string) ->
@stack = [ ]
for token in string.split /\s+/
@parseToken token
if @stack.length > 1
throw "Not enough operators: numbers left over"
else
@stack[0]
parseToken: (token, lastNumber) ->
if isNaN parseFloat(token) # Assume that anything other than a number is an operator
@parseOperator token
else
@stack.push parseFloat(token)
parseOperator: (operator) ->
if @stack.length < 2
throw "Can't operate on a stack without at least 2 items"
right = @stack.pop()
left = @stack.pop()
result = switch operator
when "+" then left + right
when "-" then left - right
when "*" then left * right
when "/"
if right is 0
throw "Can't divide by 0"
else
left / right
else
throw "Unrecognized operator: #{operator}"
@stack.push result
calc = new StackCalculator
calc.parseString "5 5 +" # => { result: 10 }
calc.parseString "4.0 5.5 +" # => { result: 9.5 }
calc.parseString "5 5 + 5 5 + *" # => { result: 100 }
try
calc.parseString "5 0 /"
catch error
error # => "Can't divide by 0"
try
calc.parseString "5 -"
catch error
error # => "Can't operate on a stack without at least 2 items"
try
calc.parseString "5 5 5 -"
catch error
error # => "Not enough operators: numbers left over"
try
calc.parseString "5 5 5 foo"
catch error
error # => "Unrecognized operator: foo"
讨论
作为一种替代编写我们自己的解释器的选择,你可以将现有的 CoffeeScript 解释器与更自然的(更容易理解的)表达自己的算法的正常方式相结合。
class Sandwich
constructor: (@customer, @bread='white', @toppings=[], @toasted=false)->
white = (sw) ->
sw.bread = 'white'
sw
wheat = (sw) ->
sw.bread = 'wheat'
sw
turkey = (sw) ->
sw.toppings.push 'turkey'
sw
ham = (sw) ->
sw.toppings.push 'ham'
sw
swiss = (sw) ->
sw.toppings.push 'swiss'
sw
mayo = (sw) ->
sw.toppings.push 'mayo'
sw
toasted = (sw) ->
sw.toasted = true
sw
sandwich = (customer) ->
new Sandwich customer
to = (customer) ->
customer
send = (sw) ->
toastedState = sw.toasted and 'a toasted' or 'an untoasted'
toppingState = ''
if sw.toppings.length > 0
if sw.toppings.length > 1
toppingState = " with #{sw.toppings[0..sw.toppings.length-2].join ', '} and #{sw.toppings[sw.toppings.length-1]}"
else
toppingState = " with #{sw.toppings[0]}"
"#{sw.customer} requested #{toastedState}, #{sw.bread} bread sandwich#{toppingState}"
send sandwich to 'Charlie' # => "Charlie requested an untoasted, white bread sandwich"
send turkey sandwich to 'Judy' # => "Judy requested an untoasted, white bread sandwich with turkey"
send toasted ham turkey sandwich to 'Rachel' # => "Rachel requested a toasted, white bread sandwich with turkey and ham"
send toasted turkey ham swiss sandwich to 'Matt' # => "Matt requested a toasted, white bread sandwich with swiss, ham and turkey"
这个实例可以允许功能层实现返回修改后的对象,从而外函数可以依次修改它。示例通过借用动词和介词的用法,把自然语法提供给结构,当被正确使用时,会像自然语句一样结束。这样,利用 CoffeeScript 语言技能和你现有的语言技能可以帮助你关于捕捉代码的问题。
CoffeeScript 备忘录模式
备忘录模式
问题
你想预测对一个对象做出改变后的反应。
解决方案
使用备忘录模式(Memento Pattern)来跟踪一个对象的变化。使用这个模式的类会输出一个存储在其他地方的备忘录对象。
如果你的应用程序可以让用户编辑文本文件,例如,他们可能想要撤销上一个动作。你可以在用户改变文件之前保存文件现有的状态,然后回滚到上一个位置。
class PreserveableText
class Memento
constructor: (@text) ->
constructor: (@text) ->
save: (newText) ->
memento = new Memento @text
@text = newText
memento
restore: (memento) ->
@text = memento.text
pt = new PreserveableText "The original string"
pt.text # => "The original string"
memento = pt.save "A new string"
pt.text # => "A new string"
pt.save "Yet another string"
pt.text # => "Yet another string"
pt.restore memento
pt.text # => "The original string"
讨论
备忘录对象由 PreserveableText#save 返回,为了安全保护,分别地存储着重要的状态信息。你可以序列化备忘录以便来保证硬盘中的“撤销”缓冲或者是那些被编辑的图片等数据密集型对象。
CoffeeScript 观察者模式
观察者模式
问题
当一个事件发生时你不得不向一些对象发布公告。
解决方案
使用观察者模式(Observer Pattern)。
class PostOffice
constructor: () ->
@subscribers = []
notifyNewItemReleased: (item) ->
subscriber.callback(item) for subscriber in @subscribers when subscriber.item is item
subscribe: (to, onNewItemReleased) ->
@subscribers.push {'item':to, 'callback':onNewItemReleased}
class MagazineSubscriber
onNewMagazine: (item) ->
alert "I've got new "+item
class NewspaperSubscriber
onNewNewspaper: (item) ->
alert "I've got new "+item
postOffice = new PostOffice()
sub1 = new MagazineSubscriber()
sub2 = new NewspaperSubscriber()
postOffice.subscribe "Mens Health", sub1.onNewMagazine
postOffice.subscribe "Times", sub2.onNewNewspaper
postOffice.notifyNewItemReleased "Times"
postOffice.notifyNewItemReleased "Mens Health"
讨论
这里你有一个观察者对象( PostOffice )和可观察对象( MagazineSubscriber, NewspaperSubscriber )。为了通报发布新的周期性可观察对象的事件,应该对 PostOffice 进行订阅。每一个被订阅的对象都存储在 PostOffice 的内部订阅数组中。当新的实体周期发布时每一个订阅者都会收到通知。
CoffeeScript 单件模式
单件模式
问题
许多时候你想要一个,并且只要一个类的实例。比如,你可能需要一个创建服务器资源的类,并且你想要保证使用一个对象就可以控制这些资源。但是使用时要小心,因为单件模式可以很容易被滥用来模拟不必要的全局变量。
解决方案
公有类只包含获得一个实例的方法。实例被保存在该公共对象的闭包中,并且总是有返回值。
这很奏效因为 CoffeeScript 允许你在一个类的声明中定义可执行的状态。但是,因为大多数 CoffeeScript 编译成一个 IIFE 包,如果这个方式适合你,你就不需要在类的声明中放置私有的类。之后的内容可能对开发模块化代码有所帮助,例如 CommonJS(Node.js)或 Require.js 中可见(见实例讨论)。
class Singleton
# You can add statements inside the class definition
# which helps establish private scope (due to closures)
# instance is defined as null to force correct scope
instance = null
# Create a private class that we can initialize however
# defined inside this scope to force the use of the
# singleton class.
class PrivateClass
constructor: (@message) ->
echo: -> @message
# This is a static method used to either retrieve the
# instance or create a new one.
@get: (message) ->
instance ?= new PrivateClass(message)
a = Singleton.get "Hello A"
a.echo() # => "Hello A"
b = Singleton.get "Hello B"
b.echo() # => "Hello A"
Singleton.instance # => undefined
a.instance # => undefined
Singleton.PrivateClass # => undefined
讨论
通过上面的实例我们可以看到,所有的实例是如何从同一个 Singleton 类的实例中输出的。你也可以看到,私有类和实例变量都无法在 Singleton class 外被访问到。 Singleton class 的本质是提供一个静态方法得到只返回一个私有类的实例。它也对外界也隐藏私有类,因此你无法创建一个自己的私有类。
隐藏或使私有类在内部运作的想法是更受偏爱的。尤其是由于缺省的 CoffeeScript 将编译的代码封装在自己的 IIFE(闭包)中,你可以定义类而无须担心会被文件外部访问到。在这个实例中,注意,用惯用的模块导出特点来强调模块中可被公共访问的部分。(请看 “导出到全局命名空间” 中对此理解更深入的讨论)。
root = exports ? this
# Create a private class that we can initialize however
# defined inside the wrapper scope.
class ProtectedClass
constructor: (@message) ->
echo: -> @message
class Singleton
# You can add statements inside the class definition
# which helps establish private scope (due to closures)
# instance is defined as null to force correct scope
instance = null
# This is a static method used to either retrieve the
# instance or create a new one.
@get: (message) ->
instance ?= new ProtectedClass(message)
# Export Singleton as a module
root.Singleton = Singleton
我们可以注意到 coffeescript 是如此简单地实现这个设计模式。为了更好地参考和讨论 JavaScript 的实现,请看初学者必备 JavaScript 设计模式。
CoffeeScript 策略模式
策略模式
问题
解决问题的方式有多种,但是你需要在程序运行时选择(或是转换)这些方法。
解决方案
在策略对象(Strategy objects)中封装你的算法。
例如,给定一个未排序的列表,我们可以在不同情况下改变排序算法。
基类
StringSorter = (algorithm) ->
sort: (list) -> algorithm list
策略
bubbleSort = (list) ->
anySwaps = false
swapPass = ->
for r in [0..list.length-2]
if list[r] > list[r+1]
anySwaps = true
[list[r], list[r+1]] = [list[r+1], list[r]]
swapPass()
while anySwaps
anySwaps = false
swapPass()
list
reverseBubbleSort = (list) ->
anySwaps = false
swapPass = ->
for r in [list.length-1..1]
if list[r] < list[r-1]
anySwaps = true
[list[r], list[r-1]] = [list[r-1], list[r]]
swapPass()
while anySwaps
anySwaps = false
swapPass()
list
使用策略
sorter = new StringSorter bubbleSort
unsortedList = ['e', 'b', 'd', 'c', 'x', 'a']
sorter.sort unsortedList
# => ['a', 'b', 'c', 'd', 'e', 'x']
unsortedList.push 'w'
# => ['a', 'b', 'c', 'd', 'e', 'x', 'w']
sorter.algorithm = reverseBubbleSort
sorter.sort unsortedList
# => ['a', 'b', 'c', 'd', 'e', 'w', 'x']
讨论
“没有作战计划在第一次接触敌人时便能存活下来。” 用户如是,但是我们可以运用从变化的情况中获得的知识来做出适应改变。在示例末尾,例如,数组中的最新项是乱序排列的,知道了这个细节,我们便可以通过切换算法来加速排序,只要简单地重赋值就可以了。
练习
- 将 StringSorter 扩展为 AlwaysSortedArray 类来实现规则序列的所有功能,但是要基于插入方法自动分类新的项(例如 push 对比 shift)。
CoffeeScript 模板方法模式
模板方法模式
问题
定义一个算法的结构,作为一系列的高层次的步骤,使每一个步骤的行为可以指定,使属于一个族的算法都具有相同的结构但是有不同的行为。
解决方案
使用模板方法( Template Method )在父类中描述算法的结构,再授权一个或多个具体子类来具体地进行实现。
例如,想象你希望模拟各种类型的文件的生成,并且每个文件要包含一个标题和正文。
class Document
produceDocument: ->
@produceHeader()
@produceBody()
produceHeader: ->
produceBody: ->
class DocWithHeader extends Document
produceHeader: ->
console.log "Producing header for DocWithHeader"
produceBody: ->
console.log "Producing body for DocWithHeader"
class DocWithoutHeader extends Document
produceBody: ->
console.log "Producing body for DocWithoutHeader"
docs = [new DocWithHeader, new DocWithoutHeader]
doc.produceDocument() for doc in docs
讨论
在这个实例中,算法用两个步骤来描述文件的生成:其一是产生文件的标题,另一步是生成文件的正文。父类中是实现每一个步骤的空的方法,多态性使得每一个具体的子类可以通过重写一步步的方法来实现对方法不同的利用。在本实例中,DocWithHeader 实现了正文和标题的步骤, DocWithoutHeader 只是实现了正文的步骤。
不同类型文件的生成就是简单的将文档对象存储在一个数组中,简单的遍历每个文档对象并调用其 produceDocument 方法的问题。
CoffeeScript 数据库
CoffeeScript MongoDB
MongoDB
问题
你需要与一个 MongoDB 数据库连接的接口。
解决方案
对于 Node.js
安装
- 如果你的计算机中还没有 MongoDB ,需要安装。
- 安装本地 MongoDB 模块。
保存记录
mongo = require 'mongodb'
server = new mongo.Server "127.0.0.1", 27017, {}
client = new mongo.Db 'test', server, {w:1}
# save() updates existing records or inserts new ones as needed
exampleSave = (dbErr, collection) ->
console.log "Unable to access database: #{dbErr}" if dbErr
collection.save { _id: "my_favorite_latte", flavor: "honeysuckle" }, (err, docs) ->
console.log "Unable to save record: #{err}" if err
client.close()
client.open (err, database) ->
client.collection 'coffeescript_example', exampleSave
查找记录
mongo = require 'mongodb'
server = new mongo.Server "127.0.0.1", 27017, {}
client = new mongo.Db 'test', server, {w:1}
exampleFind = (dbErr, collection) ->
console.log "Unable to access database: #{dbErr}" if dbErr
collection.find({ _id: "my_favorite_latte" }).nextObject (err, result) ->
if err
console.log "Unable to find record: #{err}"
else
console.log result # => { id: "my_favorite_latte", flavor: "honeysuckle" }
client.close()
client.open (err, database) ->
client.collection 'coffeescript_example', exampleFind
对于浏览器
一个基于 REST 的接口在工程中,会提供基于 AJAX 的访问通道。
讨论
这个方法将 save 和 find 分开进单独的实例,其目的是分散 MongoDB 指定的连接任务的关注点以及回收任务。async 模块可以帮助这样的异步调用。
CoffeeScript SQLite
SQLite
问题
你需要 Node.js 内部与 SQLite 数据库连接的接口。
解决方案
使用 SQLite 模块。
sqlite = require 'sqlite'
db = new sqlite.Database
# The module uses asynchronous methods,
# so we chain the calls the db.execute
exampleCreate = ->
db.execute "CREATE TABLE snacks (name TEXT(25), flavor TEXT(25))",
(exeErr, rows) ->
throw exeErr if exeErr
exampleInsert()
exampleInsert = ->
db.execute "INSERT INTO snacks (name, flavor) VALUES ($name, $flavor)",
{ $name: "Potato Chips", $flavor: "BBQ" },
(exeErr, rows) ->
throw exeErr if exeErr
exampleSelect()
exampleSelect = ->
db.execute "SELECT name, flavor FROM snacks",
(exeErr, rows) ->
throw exeErr if exeErr
console.log rows[0] # => { name: 'Potato Chips', flavor: 'BBQ' }
# :memory: creates a DB in RAM
# You can supply a filepath (like './example.sqlite') to create/open one on disk
db.open ":memory:", (openErr) ->
throw openErr if openErr
exampleCreate()
讨论
你也可以提前准备你的 SQL 查询语句。
sqlite = require 'sqlite'
async = require 'async' # Not required but added to make the example more concise
db = new sqlite.Database
createSQL = "CREATE TABLE drinks (name TEXT(25), price NUM)"
insertSQL = "INSERT INTO drinks (name, price) VALUES (?, ?)"
selectSQL = "SELECT name, price FROM drinks WHERE price < ?"
create = (onFinish) ->
db.execute createSQL, (exeErr) ->
throw exeErr if exeErr
onFinish()
prepareInsert = (name, price, onFinish) ->
db.prepare insertSQL, (prepErr, statement) ->
statement.bindArray [name, price], (bindErr) ->
statement.fetchAll (fetchErr, rows) -> # Called so that it executes the insert
onFinish()
prepareSelect = (onFinish) ->
db.prepare selectSQL, (prepErr, statement) ->
statement.bindArray [1.00], (bindErr) ->
statement.fetchAll (fetchErr, rows) ->
console.log rows[0] # => { name: "Mia's Root Beer", price: 0.75 }
onFinish()
db.open ":memory:", (openErr) ->
async.series([
(onFinish) -> create onFinish,
(onFinish) -> prepareInsert "LunaSqueeze", 7.95, onFinish,
(onFinish) -> prepareInsert "Viking Sparkling Grog", 4.00, onFinish,
(onFinish) -> prepareInsert "Mia's Root Beer", 0.75, onFinish,
(onFinish) -> prepareSelect onFinish
])
SQL 的 SQLite 版本的以及 node-SQLite 模块文档提供了更完整的信息。
CoffeeScript 测试
CoffeeScript 使用 Jasmine 测试
使用 Jasmine 测试
问题
假如你正在使用 CoffeeScript 写一个简单地计算器,并且想要验证其功能是否与预期一致。可以使用 Jasmine 测试框架。
讨论
在使用 Jasmine 测试框架时,你要在一个参数(spec)文档中写测试,文档描述的是代码需要测试的预期功能。
例如,我们希望计算器可以实现加法和减法的功能,并且可以正确进行正数和负数的运算。我们的 spec 文档如下列所示。
# calculatorSpec.coffee
describe 'Calculator', ->
it 'can add two positive numbers', ->
calculator = new Calculator()
result = calculator.add 2, 3
expect(result).toBe 5
it 'can handle negative number addition', ->
calculator = new Calculator()
result = calculator.add -10, 5
expect(result).toBe -5
it 'can subtract two positive numbers', ->
calculator = new Calculator()
result = calculator.subtract 10, 6
expect(result).toBe 4
it 'can handle negative number subtraction', ->
calculator = new Calculator()
result = calculator.subtract 4, -6
expect(result).toBe 10
配置 Jasmine
在你运行测试之前,必须要先下载并配置 Jasmine。包括:1.下载最新的 Jasmine 压缩文件;2.在你的项目工程中创建一个 spec 以及一个 spec/jasmine 目录;3.将下载的 Jasmine 文件解压到 spec/jasmine 目录中;4.创建一个测试流
创建测试流
Jasmine 可以使用 spec runner 的 HTML 文档在 web 浏览器中运行你的测试。 spec runner 是一个简单地 HTML 页面,连接着 Jasmine 以及你的代码所需要的必要的 JavaScript 和 CSS 文件。示例如下。
1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
2 "http://www.w3.org/TR/html4/loose.dtd">
3 <html>
4 <head>
5 <title>Jasmine Spec Runner</title>
6 <link rel="shortcut icon" type="image/png" href="spec/jasmine/jasmine_favicon.png">
7 <link rel="stylesheet" type="text/css" href="spec/jasmine/jasmine.css">
8 <script src="http://img.w3cschool.cn/attachments/image/wk/coffeescript/jquery.min.js"></script>
9 <script src="http://img.w3cschool.cn/attachments/image/wk/coffeescript/jasmine.js"></script>
10 <script src="http://img.w3cschool.cn/attachments/image/wk/coffeescript/jasmine-html.js"></script>
11 <script src="http://img.w3cschool.cn/attachments/image/wk/coffeescript/jasmine-jquery-1.3.1.js"></script>
12
13 <!-- include source files here... -->
14 <script src="http://img.w3cschool.cn/attachments/image/wk/coffeescript/calculator.js"></script>
15
16 <!-- include spec files here... -->
17 <script src="http://img.w3cschool.cn/attachments/image/wk/coffeescript/calculatorSpec.js"></script>
18
19 </head>
20
21 <body>
22 <script type="text/javascript">
23 (function() {
24 var jasmineEnv = jasmine.getEnv();
25 jasmineEnv.updateInterval = 1000;
26
27 var trivialReporter = new jasmine.TrivialReporter();
28
29 jasmineEnv.addReporter(trivialReporter);
30
31 jasmineEnv.specFilter = function(spec) {
32 return trivialReporter.specFilter(spec);
33 };
34
35 var currentWindowOnload = window.onload;
36
37 window.onload = function() {
38 if (currentWindowOnload) {
39 currentWindowOnload();
40 }
41 execJasmine();
42 };
43
44 function execJasmine() {
45 jasmineEnv.execute();
46 }
47
48 })();
49 </script>
50 </body>
51 </html>
此 spec runner 可以在 GitHub gist 上下载。
使用 SpecRunner.html ,只是简单地参考你编译后的 JavaScript 文件,并且在 jasmine.js 以及其依赖项后编译的测试文件。
在上述示例中,我们在第 14 行包含了尚待开发的 calculator.js 文件,在第17行编译了 calculatorSpec.js 文件。
运行测试
要运行我们的测试,只需要简单地在 web 浏览器中打开 SpecRunner.html 页面。在我们的示例中可以看到 4 个失败的 specs 共 8 个失败情况(如下)。
看来我们的测试是失败的,因为 jasmine 无法找到 Calculator 变量。那是因为它还没有被创建。现在让我们来创建一个新文件命名为 js/calculator.coffee 。
# calculator.coffee
window.Calculator = class Calculator
编译 calculator.coffee 并刷新浏览器来重新运行测试组。
现在我们还有4个失败而不是原来的8个了,只用一行代码便做出了50%的改进。
测试通过
实现我们的方法来看是否可以通过测试。
# calculator.coffee
window.Calculator = class Calculator
add: (a, b) ->
a + b
subtract: (a, b) ->
a - b
当我们刷新页面时可以看到全部通过。
重构测试
既然测试全部通过了,我们应看一看我们的代码或测试是否可以被重构。
在我们的 spec 文件中,每个测试都创建了自己的 calculator 实例。这会使我们的测试相当的重复,特别是对于大型的测试套件。理想情况下,我们应该考虑将初始化代码移动到每次测试之前运行。
幸运的是 Jasmine 拥有一个 beforeEach 函数,就是为了这一目的设置的。
describe 'Calculator', ->
calculator = null
beforeEach ->
calculator = new Calculator()
it 'can add two positive numbers', ->
result = calculator.add 2, 3
expect(result).toBe 5
it 'can handle negative number addition', ->
result = calculator.add -10, 5
expect(result).toBe -5
it 'can subtract two positive numbers', ->
result = calculator.subtract 10, 6
expect(result).toBe 4
it 'can handle negative number subtraction', ->
result = calculator.subtract 4, -6
expect(result).toBe 10
当我们重新编译我们的 spec 然后刷新浏览器,可以看到测试仍然全部通过。
CoffeeScript 使用 Nodeunit 测试
使用 Nodeunit 测试
问题
假如你正在使用 CoffeeScript 并且想要验证功能是否与预期一致,便可以决定使用 Nodeunit 测试框架。
讨论
Nodeunit 是一种 JavaScript 对于单元测试库( Unit Testing libraries )中 xUnit 族的实现,Java, Python, Ruby, Smalltalk 中均可以使用。
当使用 xUnit 族测试框架时,你需要将所需测试的描述预期功能的代码写在一个文件中。
例如,我们希望我们的计算器可以进行加法和减法,并且对于正负数均可以正确计算,我们的测试如下。
# test/calculator.test.coffee
Calculator = require '../calculator'
exports.CalculatorTest =
'test can add two positive numbers': (test) ->
calculator = new Calculator
result = calculator.add 2, 3
test.equal(result, 5)
test.done()
'test can handle negative number addition': (test) ->
calculator = new Calculator
result = calculator.add -10, 5
test.equal(result, -5)
test.done()
'test can subtract two positive numbers': (test) ->
calculator = new Calculator
result = calculator.subtract 10, 6
test.equal(result, 4)
test.done()
'test can handle negative number subtraction': (test) ->
calculator = new Calculator
result = calculator.subtract 4, -6
test.equal(result, 10)
test.done()
安装 Nodeunit
在可以运行你的测试之前,你必须先安装 Nodeunit :
首先创建一个 package.json 文件
{
"name": "calculator",
"version": "0.0.1",
"scripts": {
"test": "./node_modules/.bin/nodeunit test"
},
"dependencies": {
"coffee-script": "~1.4.0",
"nodeunit": "~0.7.4"
}
}
接下来从一个终端运行。
$ npm install
运行测试
使用代码行可以简便地运行测试文件:
$ npm test
测试失败,因为我们并没有 calculator.coffee
suki@Yuzuki:nodeunit_testing (master)$ npm test
npm WARN package.json calculator@0.0.1 No README.md file found!
> calculator@0.0.1 test /Users/suki/tmp/nodeunit_testing
> ./node_modules/.bin/nodeunit test
/Users/suki/tmp/nodeunit_testing/node_modules/nodeunit/lib/nodeunit.js:72
if (err) throw err;
^
Error: ENOENT, stat '/Users/suki/tmp/nodeunit_testing/test'
npm ERR! Test failed. See above for more details.
npm ERR! not ok code 0
我们创建一个简单文件
# calculator.coffee
class Calculator
module.exports = Calculator
并且重新运行测试套件。
suki@Yuzuki:nodeunit_testing (master)$ npm test
npm WARN package.json calculator@0.0.1 No README.md file found!
> calculator@0.0.1 test /Users/suki/tmp/nodeunit_testing
> ./node_modules/.bin/nodeunit test
calculator.test
✖ CalculatorTest - test can add two positive numbers
TypeError: Object #<Calculator> has no method 'add'
...
✖ CalculatorTest - test can handle negative number addition
TypeError: Object #<Calculator> has no method 'add'
...
✖ CalculatorTest - test can subtract two positive numbers
TypeError: Object #<Calculator> has no method 'subtract'
...
✖ CalculatorTest - test can handle negative number subtraction
TypeError: Object #<Calculator> has no method 'subtract'
...
FAILURES: 4/4 assertions failed (31ms)
npm ERR! Test failed. See above for more details.
npm ERR! not ok code 0
通过测试
让我们对方法进行实现来观察测试是否可以通过。
# calculator.coffee
class Calculator
add: (a, b) ->
a + b
subtract: (a, b) ->
a - b
module.exports = Calculator
当我们重新运行测试时可以看到全部通过:
suki@Yuzuki:nodeunit_testing (master)$ npm test
npm WARN package.json calculator@0.0.1 No README.md file found!
> calculator@0.0.1 test /Users/suki/tmp/nodeunit_testing
> ./node_modules/.bin/nodeunit test
calculator.test
✔ CalculatorTest - test can add two positive numbers
✔ CalculatorTest - test can handle negative number addition
✔ CalculatorTest - test can subtract two positive numbers
✔ CalculatorTest - test can handle negative number subtraction
OK: 4 assertions (27ms)
重构测试
既然测试全部通过,我们应看一看我们的代码或测试是否可以被重构。
在我们的测试文件中,每个测试都创建了自己的 calculator 实例。这会使我们的测试相当的重复,特别是对于大型的测试套件。理想情况下,我们应该考虑将初始化代码移动到每次测试之前运行。
通常在其他的 xUnit 库中,Nodeunit 会提供一个 setUp(以及 tearDown )功能会在测试前调用。
Calculator = require '../calculator'
exports.CalculatorTest =
setUp: (callback) ->
@calculator = new Calculator
callback()
'test can add two positive numbers': (test) ->
result = @calculator.add 2, 3
test.equal(result, 5)
test.done()
'test can handle negative number addition': (test) ->
result = @calculator.add -10, 5
test.equal(result, -5)
test.done()
'test can subtract two positive numbers': (test) ->
result = @calculator.subtract 10, 6
test.equal(result, 4)
test.done()
'test can handle negative number subtraction': (test) ->
result = @calculator.subtract 4, -6
test.equal(result, 10)
test.done()
我们可以重新运行测试,仍然可以全部通过。




