CoffeeScript 实用手册

CoffeeScript是一门编译到JavaScript的小巧语言。在Java般笨拙的外表下,JavaScript其实有着一颗华丽的心脏。CoffeeScript尝试用简洁的方式展示JavaScript优秀的部分。

适用人群

本书着重讲解了CoffeeScript的基础语法,适合初学者。

学习前提

学习本书前,你需要了解JavaScript这门语言,应为CoffeeScript是建立在此基础上的。

相关教程

《javascript教程》

CoffeeScript 实用手册

CoffeeScript是一门编译到JavaScript的小巧语言。在Java般笨拙的外表下,JavaScript其实有着一颗华丽的心脏。CoffeeScript尝试用简洁的方式展示JavaScript优秀的部分。

适用人群

本书着重讲解了CoffeeScript的基础语法,适合初学者。

学习前提

学习本书前,你需要了解JavaScript这门语言,应为CoffeeScript是建立在此基础上的。

相关教程

《javascript教程》

服务端和客户端的代码重用

问题

当你在CoffeeScript上创建了一个函数,并希望将它用在有网页浏览器的客户端和有Node.js的服务端时。

解决方案

以下列方法输出函数:

# simpleMath.coffee# these methods are privateadd = (a, b) ->    a + bsubtract = (a, b) ->    a - bsquare = (x) ->    x * x# create a namespace to export our public methodsSimpleMath = exports? and exports or @SimpleMath = {}# items attached to our namespace are available in Node.js as well as client browsersclass 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 =  11undefined>

在网页中,我们可以通过将模块作为一个脚本嵌入其中。

<!DOCTYPE HTML><html lang="en-US"><head>    <meta charset="UTF-8">    <title>SimpleMath Module Example</title>    <script src="https://atts.51coolma.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的连缀比较语法。

maxDwarfism = 147minAcromegaly = 213height = 180normalHeight = maxDwarfism < height < minAcromegaly# => true

讨论

这是从Python中借鉴过来的一个很棒的特性。利用这个特性,不必像下面这样写出完整的比较:

normalHeight = height > maxDwarfism && height < minAcromegaly

CoffeeScript支持像写数学中的比较表达式一样连缀两个比较,这样更直观。

相关教程

《python基础教程》

嵌入 JavaScript

问题

你想在CoffeeScript中嵌入找到的或预先编写的JavaScript代码。

解决方案

把JavaScript包装到撇号中:

`function greet(name) {return "Hello "+name;}`# Back to CoffeeScriptgreet "Coffee"# => "Hello Coffee"

讨论

这是在CoffeeScript代码中集成少量JavaScript而不必用CoffeeScript语法转换它们的最简单的方法。正如CoffeeScript Language Reference中展示的,可以在一定范围内混合这两种语言的代码:

hello = `function (name) {return "Hello "+name}`hello "Coffee"# => "Hello Coffee"

这里的变量"hello"还在CoffeeScript中,但赋给它的函数则是用JavaScript写的。

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循环。

对象的链式调用

问题

你想调用一个对象上的多个方法,但不想每次都引用该对象。

解决方案

在每次链式调用后返回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        thismorningCup = 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]            objclass TeaCup    constructor:  ->        @properties=            size: 'medium'            type: 'black'            sugar: false            cream: false        addChainedAttributeAccessor(this, 'properties', attr) for attr of @propertiesearlgrey = new TeaCup().size('small').type('Earl Grey').sugar('false')earlgrey.properties # => { size: 'small', type: 'Earl Grey', sugar: false }earlgrey.sugar trueearlgrey.sugar() # => true

类方法和实例方法

问题

你想创建类和实例的方法。

解决方案

类方法

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.titlesSongs.get_count()# => 0song = new Songs("Rick Astley", "Never Gonna Give You Up")Songs.get_count()# => 1song.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()# => 1Songs.get_count()# => TypeError: Object function Songs(artist, title) ... has no method 'get_count'

讨论

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# => 50Zoo.MAX_ZOOKEEPERS# => undefined (it is a prototype member)Zoo::MAX_ZOOKEEPERS# => 3zoo = new Zoozoo.MAX_ZOOKEEPERS# => 3zoo.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 namezoo = 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 Javascriptzoo = 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会将类变量的值保存在类中而不是它定义的原型中。这在定义类中的变量时是十分有用的,因为这不会被实体属性变量重写。

克隆对象(深度复制)

问题

你想复制一个对象,包含其所有子对象。

解决方案

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 newInstancex =  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 = xy.foo = 'test'console.log x.foo isnt y.foo, x.foo, y.foo# => false, test, test

显然,复制之后修改y也就修改了x。

类的混合

问题

你有一些通用方法,你想把他们包含到很多不同的类中。

解决方案

使用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: ->    42class PhilosopherMixin  pontificate: ->    console.log "hmm..."    @wise = yesclass DeeperThought extends mixOf DeepThought, PhilosopherMixin  answer: ->    @pontificate()    super()earth = new DeeperThoughtearth.answer()# hmm...# => 42

讨论

这适用于轻量级的混合。因此你可以从基类和基类的祖先中继承方法,也可以从混合类的基类和祖先中继承,但是不能从混合类的祖先中继承。与此同时,在声明了一个混合类后,此后的对这个混合类进行的改变是不会反应出来的。

创建一个不存在的对象字面值

问题

你想初始化一个对象字面值,但如果这个对象已经存在,你不想重写它。

解决方案

使用存在判断运算符(existential operator)。

window.MY_NAMESPACE ?= {} 

讨论

这行代码与下面的JavaScript代码等价:

window.MY_NAMESPACE = window.MY_NAMESPACE || {};

这是JavaScript中一个常用的技巧,即使用对象字面值来定义命名空间。这样先判断是否存在同名的命名空间然后再创建,可以避免重写已经存在的命名空间。

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

大写单词首字母

问题

你想把字符串中每个单词的首字母转换为大写形式。

解决方案

使用“拆分-映射-拼接”模式:先把字符串拆分成单词,然后通过映射来大写单词第一个字母小写其他字母,最后再将转换后的单词拼接成字符串。

("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'

但这样做又会导致第二个问题:在结果字符串中,原来的空白符串经过拼接就只剩下一个空格了。

不过,一般来说,这两个问题还是可以接受的。所以,“拆分-映射-拼接”仍然是一种有效的技术。

查找子字符串

问题

你想在一条消息中查找某个关键字第一次或最后一次出现的位置。

解决方案

分别使用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 parametermessage.indexOf "This", 5# => 23message.lastIndexOf "This"# => 49

讨论

还需要想办法统计出给定字符串在一条消息中出现的次数。

生成唯一ID

问题

你想随机生成一个唯一的标识符。

解决方案

可以根据一个随机数值生成一个Base 36编码的字符串。

uniqueId = (length=8) ->  id = ""  id += Math.random().toString(36).substr(2) while id.length < length  id.substr 0, lengthuniqueId()    # => n5yjla3buniqueId(2)   # => 0duniqueId(20)  # => ox9eo7rt3ej0pb9kqlkeuniqueId(40)  # => xu2vo4xjn4g0t3xr74zmndshrqlivn291d584alj

讨论

使用其他技术也可以,但这种方法相对来说性能更高,也更灵活。

字符串插值

问题

你想创建一个字符串,让它包含体现某个CoffeeScript变量的文本。

解决方案

使用CoffeeScript中类似Ruby的字符串插值,而不是JavaScript的字符串拼接。

插值:

muppet = "Beeker"favorite = "My favorite muppet is #{muppet}!"# => "My favorite muppet is Beeker!"
square = (x) -> x * xmessage = "The square of 7 is #{square 7}."# => "The square of 7 is 49."

讨论

CoffeeScript的插值与Ruby类似,多数表达式都可以用在#{ ... }插值结构中。

CoffeeScript支持在插值结构中放入多个有副作用的表达式,但建议大家不要这样做。因为只有表达式的最后一个值会被插入。

# 可以这样做,但不要这样做。否则,你会疯掉。square = (x) -> x * xmuppet = "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."

相关教程

《Ruby教程》

把字符串转换为小写形式

问题

你想把字符串转换成小写形式。

解决方案

使用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?

匹配字符串

问题

你想要匹配两个或多个字符串。

解决方案

计算把一个字符串转换成另一个字符串所需的编辑距离或操作数。

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中交换两个变量的语法。

重复字符串

问题

你想重复一个字符串。

解决方案

创建一个包含n+1个空元素的数组,然后用要重复的字符串作为连接字符将数组元素拼接到一起:

# 创建包含10个foo的字符串Array(11).join 'foo'# => "foofoofoofoofoofoofoofoofoofoo"

为字符串重复方法

你也可以在字符串的原型中为其创建方法。它十分简单:

# 为所有的字符串添加重复方法,这会重复返回 n 次字符串String::repeat = (n) -> Array(n+1).join(this)

讨论

JavaScript缺少字符串重复函数,CoffeeScript也没有提供。虽然在这里也可以使用列表推导( comprehensions ),但对于简单的字符串重复来说,还是像这样先构建一个包含n+1个空元素的数组,然后再把它拼接起来更方便。

拆分字符串

问题

你想拆分一个字符串。

解决方案

使用JavaScript字符串的split()方法:

"foo bar baz".split " "# => [ 'foo', 'bar', 'baz' ]

讨论

String的这个split()方法是标准的JavaScript方法。可以用来基于任何分隔符——包括正则表达式来拆分字符串。这个方法还可以接受第二个参数,用于指定返回的子字符串数目。

"foo-bar-baz".split "-"# => [ 'foo', 'bar', 'baz' ]
"foo   bar  	 baz".split /s+/# => [ 'foo', 'bar', 'baz' ]
"the sun goes down and I sit on the old broken-down river pier".split " ", 2# => [ 'the', 'sun' ]

清理字符串前后的空白符

问题

你想清理字符串前后的空白符。

解决方案

使用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的这篇博客文章

把字符串转换为大写形式

问题

你想把字符串转换成大写形式。

解决方案

使用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?

检查变量的类型是否为数组

问题

你希望检查一个变量是否为一个数组。

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' )

将数组连接

问题

你希望将两个数组连接到一起。

解决方案

在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, array2array1# => [1, 2, 3, 4, 5, 6]

在上面的例子中,Array.prototype.push.apply(a, b)方法修改了array1而没有产生一个新的数组对象。

在CoffeeScript中,我们可以简化上面的方式,通过给数组创建一个新方法merge():

Array::merge = (other) -> Array::push.apply @, otherarray1 = [1, 2, 3]array2 = [4, 5, 6]array1.merge array2array1# => [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方法。

由数组创建一个对象词典

问题

你有一组对象,例如:

cats = [  {    name: "Bubbles"    age: 1  },  {    name: "Sparkle"    favoriteFood: "tuna"  }]

但是你想让它像词典一样,可以通过关键字访问它,就像使用cats["Bubbles"]。

解决方案

你需要将你的数组转换为一个对象。通过这样使用reduce:

# key = The key by which to index the dictionaryArray::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" }

由数组创建一个字符串

问题

你想由数组创建一个字符串。

解决方案

使用JavaScript的数组方法toString():

["one", "two", "three"].toString()# => 'one,two,three'

讨论

toString()是一个标准的JavaScript方法。不要忘记圆括号。

定义数组范围

问题

你想定义一个数组的范围。

解决方案

在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 ]

讨论

包含范围以“..”运算符定义,包含最后一个值。 排除范围以 “...” 运算符定义,并且通常忽略最后一个值。

筛选数组

问题

你想要根据布尔条件来筛选数组。

解决方案

使用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 > 5filtered_array = array.filter gt_five# => [6,7,8,9,10]

讨论

这个方法与Ruby的Array的#select方法类似。

列表推导

问题

你有一个对象数组,想将它们映射到另一个数组,类似于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的语句时,他们会很好地起到作用。对于简单的映射,列表推导具有更好的可读性。但是对于复杂的转换或链式映射,映射数组可能更合适。

映射数组

问题

你有一个对象数组,想把这些对象映射到另一个数组中,就像 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) 看起来会清楚一些。

数组最大值

问题

你需要找出数组中包含的最大的值。

解决方案

你可以使用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。

归纳数组

问题

你有一个对象数组,想要把它们归纳为一个值,类似于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为匿名函数提供了简单自然的表达语法。二者配合使用,可以把集合的项合并为组合的结果。

删除数组中的相同元素

问题

你想从数组中删除相同元素。

解决方案

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

反转数组

问题

你想要反转数组元素。

解决方案

使用 JavaScript Array 的 reverse() 方法

["one", "two", "three"].reverse()# => ["three", "two", "one"]

讨论

reverse()是标准的JavaScript方法,别忘了带圆括号。

打乱数组中的元素

问题

你想打乱数组中的元素。

解决方案

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]]  sourceshuffle([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也是一个不错的选择。

检测每个元素

问题

你希望能够在特定的情况下检测出在数组中的每个元素。

解决方案

使用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的解构赋值语法:

a = 1b = 3[a, b] = [b, a]a# => 3b# => 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 bintersection ray1, ray2# => [ 5, 9 ]intersection ray2, ray1# => [ 5, 9 ]

对象数组

问题

你想要得到一个与你的某些属性匹配的数组对象。

你有一系列的对象,如:

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 falsecats.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 falsecats.where name:"bubbles"# => []# it's case sensitivecats.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” 。

类似 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 resultszip([0, 1, 2, 3], [0, -1, -2, -3])# => [[0, 0], [1, -1], [2, -2], [3, -3]]

计算复活节的日期

问题

你需要在给出的年份中找到复活节的月份和日期。

解决方案

下面的函数返回数组有两个要素:复活节的月份( 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]

计算(美国和加拿大的)感恩节日期

问题

你需要在给出的年份中找到感恩节的月份和日期。

解决方案

下面的函数返回给出年份的感恩节的日期。如果没有给出任何参数,给出的结果是当前年份。

美国的感恩节是十一月的第四个星期四。  

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)

这个想法很简单:

  1. 找出哪一天是以下各月份的第一天(美国十一月,加拿大十月)。
  2. 计算从那天起偏移到下一个工作日的量(美国星期四,加拿大星期一)。
  3. 将这个偏移量加上第一个可能的假期日期(第二十二个美国感恩节,第八个加拿大感恩节)。

计算两个日期中间的天数

问题

你需要找出两个日期间隔了几年,几个月,几天,几个小时,几分钟,几秒。

解决方案

利用JavaScript的日期计算函数getTime()。它提供了从1970年1月1日开始经过了多少毫秒。

DAY = 1000 * 60 * 60  * 24d1 = 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 * 60d1 = new Date('02/01/2011 02:20')d2 = new Date('02/06/2011 05:20')hour_passed = Math.round((d2.getTime() - d1.getTime()) / HOUR)

找到一个月中的最后一天

问题

你需要去找出一个月的最后一天,但是一年中的各月并没有一个固定时间表。

解决方案

利用JavaScript的日期下溢来找到给出月份的第一天:

now = new DatelastDayOfTheMonth = new Date(1900+now.getYear(), now.getMonth()+1, 0)

讨论

JavaScript的日期构造函数成功地处理溢出和下溢情况,使日期的计算变得很简单。鉴于这种简单操作,不需要担心一个给定的月份里有多少天;只需要用数学稍加推导。在十二月,以上的解决方案就是寻找当前年份的第十三个月的第0天日期,那么它就是下一年的一月一日,也计算出来今年十二月份31号的日期。

找到上一个月(或下一个月)

问题

你需要计算相关日期范围例如“上一个月”,“下一个月”。

解决方案

添加或减去当月的数字,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"

计算月球的相位

问题

你想找出月球的相位。

解决方案

以下代码提供了一种计算给出日期的月球相位计算方案:

# 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 @WebCoding4Funproper_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    tmpjdn = (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.4999999moonElong = (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 moonNumgetMoonPhase = (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    moonPhaseMoonPhase = 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'

数学常数

问题

你需要使用常见的数学常数,比如π或者e。

解决方案

使用Javascript的Math object来提供通常需要的数学常数。

Math.PI# => 3.141592653589793# Note: Capitalization matters! This produces no output, it's undefined.Math.Pi# =>Math.E# => 2.718281828459045Math.SQRT2# => 1.4142135623730951Math.SQRT1_2# => 0.7071067811865476# Natural log of 2. ln(2)Math.LN2# => 0.6931471805599453Math.LN10# => 2.302585092994046Math.LOG2E# => 1.4426950408889634Math.LOG10E# => 0.4342944819032518

讨论

另外一个例子是关于一个数学常数用于真实世界的问题,是数学章节有关[弧度和角度]( http://coffeescript-cookbook.github.io/chapters/math/radians-degrees)的部分

更快的 Fibonacci 算法

问题

你想计算出Fibonacci数列中的数值N ,但需迅速地算出结果。

解决方案

下面的方案(仍有需改进的地方)最初在Robin Houston的博客上被提出来。

这里给出一些关于该算法和改进方法的链接:

以下的代码来源于:https://gist.github.com/1032685

###Author: Jason Giedymin <jasong _a_t_ apache -dot- org>        http://www.jasongiedymin.com        https://github.com/JasonGiedyminCoffeeScript 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 = 1476fib_bits = (n) ->    #代表一个作为二进制数字阵列的整数    bits = []    while n > 0        [n, bit] = divmodBasic n, 2        bits.push bit    bits.reverse()    return bitsfibFast = (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 bdivmodNewton = (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."  

平方根倒数快速算法

问题

你想快速计算某数的平方根倒数。

解决方案

在QuakeⅢ Arena的源代码中,这个奇怪的算法对一个幻数进行整数运算,来计算平方根倒数的浮点近似值。

在CoffeeScript中,他使用经典原始的变量,以及由Chris Lomont发现的新的最优32位幻数。除此之外,还使用64位大小的幻数。

另一特征是可以通过控制牛顿迭代法的迭代次数来改变其精确度。

相比于传统的,该算法在性能上更胜一筹,这归功于使用的机器及其精确度。

运行的时候使用coffee -c script.coffee来编译script:

然后复制粘贴编译的JS代码到浏览器的JavaScript控制台。

注意:你需要一个支持类型数组的浏览器

参考:

  1. ftp://ftp.idsoftware.com/idstuff/source/quake3-1.32b-source.zip
  2. http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf
  3. http://en.wikipedia.org/wiki/Newton%27s_method
  4. https://developer.mozilla.org/en/JavaScripttypedarrays
  5. 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()

生成可预测的随机数

问题

你需要生成在一定范围内的随机数,但你也需要对发生器进行“生成种子”操作来提供可预测的值。

解决方案

编写你自己的随机数生成器。当然有很多方法可以做到这一点,这里给出一个简单的示例。 该发生器绝对不可以以加密为目的!

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)。

生成随机数

问题

你需要生成在一定范围内的随机数。

解决方案

使用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# => truedice = Math.floor(Math.random() * 6) + 11 <= dice <= 6# => truemax = 42min = -13range = Math.random() * (max - min) + min-13 <= range < 42# => true

讨论

对于JavaScript来说,它更直接更快。

需要注意到JavaScript的Math.random()不能通过发生器生成随机数种子来得到特定值。详情可参考产生可预测的随机数

产生一个从0到n(不包括在内)的数,乘以n。
产生一个从1到n(包含在内)的数,乘以n然后加上1。

转换弧度和度

问题

你需要实现弧度和度之间的转换。

解决方案

使用JavaScript的Math.PI和一个简单的公式来转换两者。

# 弧度转换成度radiansToDegrees = (radians) ->    degrees = radians * 180 / Math.PIradiansToDegrees(1)# => 57.29577951308232# 度转换成弧度degreesToRadians = (degrees) ->    radians = degrees * Math.PI / 180degreesToRadians(1)# => 0.017453292519943295

一个随机整数函数

问题

你想要获得两个整数(包含在内)之间的一个随机整数。

解决方案

使用以下的函数。

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]

指数对数运算

问题

你需要进行包含指数和对数的运算。

解决方案

使用JavaScript的Math对象来提供常用的数学函数。

# Math.pow(x, y) 返回 x^yMath.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) logMath.log(5)# => 1.6094379124341003Math.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 开发者网络上的文档。另可参阅数学常量关于数学对象中各种常量的讨论。

去抖动函数

问题

你想只执行某个函数一次,在开始或结束时把多个连续的调用合并成一个简单的操作。

解决方案

使用一个命名函数:

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 || 100mouseMoveHandler: (e) ->  @debounce((e) ->    # 只能在鼠标光标停止 300 毫秒后操作一次。  300)someOtherHandler: (e) ->  @debounce((e) ->    # 只能在初次执行 250 毫秒后操作一次。  250, true)

讨论

可参阅John Hann的博客文章,了解JavaScript 去抖动方法

当函数括号不可选

问题

你想要调用一个没有参数的函数,但不希望使用括号。

解决方案

不管怎样都使用括号。

另一个方法是使用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表示法是一个巧妙的方法来克服括号使用恐惧症。尽管有部分人宁愿在函数调用中写出所有括号。

递归函数

问题

你想在一个函数中调用相同的函数。

解决方案

使用一个命名函数:

ping = ->    console.log "Pinged"    setTimeout ping, 1000

若为未命名函数,则使用@arguments.callee@:

delay = 1000setTimeout((->    console.log "Pinged"    setTimeout arguments.callee, delay    ), delay)

讨论

虽然arguments.callee允许未命名函数的递归,在内存密集型应用中占有一定优势,但是命名函数相对来说目的更加明确,也更易于代码的维护。

提示参数

问题

你的函数将会被可变数量的参数所调用。

解决方案

使用splat

loadTruck = (firstDibs, secondDibs, tooSlow...) ->    truck:        driversSeat: firstDibs        passengerSeat: secondDibs        trunkBed: tooSlowloadTruck("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: leftAtHomeloadTruck("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能把所有不被其他命名参数采用的参数值整合进一个列表中。就算并没有提供命名参数,它也会制造一个空列表。

检测与构建丢失的函数

问题

你想要检测一个函数是否存在,如果不存在则构建该函数。(比如Internet Explorer 8的ECMAScript 5函数)。

解决方案

使用存在赋值运算符(?=)来把函数分配给类库的原型(使用::简写),然后把它放于一个立即执行函数表达式中(do ->)使其含有所有变量。

do -> Array::filter ?= (callback) ->  element for element in this when callback elementarray = [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 ->`})();

扩展内置对象

问题

你想要扩展一个类来增加新的函数或者替换旧的。

解决方案

使用 :: 把你的新函数分配到对象或者类的原型中。

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:不要修改你不拥有的对象扩展内置的本地对象。对还是错?。)

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

回调绑定

问题

你想要把一个回调与一个对象绑定在一起。

解决方案

$ ->  class Basket    constructor: () ->      @products = []      $('.product').click (event) =>        @add $(event.currentTarget).attr 'id'    add: (product) ->      @products.push product      console.log @products  new Basket()

讨论

通过使用等号箭头(=>)取代正常箭头(->),函数将会自动与对象绑定,并可以访问@-可变量。

创建 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

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

不使用 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.coffee2 loadDataFromServer = ->3   req = new XMLHttpRequest()4 5   req.addEventListener 'readystatechange', ->6     if req.readyState is 4                        # ReadyState Complete7       successResultCodes = [200, 304]8       if req.status in successResultCodes9         data = eval '(' + req.responseText + ')'10         console.log 'data message: ', data.message11       else12         console.log 'Error loading data...'13 14   req.open 'GET', 'data.json', false15   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对象在全局命名空间中可用。

使用 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空格。

使用 HTML 命名实体替换 HTML 标签

问题

你需要使用命名实体来替代HTML标签:

<br/> => &lt;br/&gt;

解决方案

htmlEncode = (str) ->  str.replace /[&<>"']/g, ($0) ->    "&" + {"&":"amp", "<":"lt", ">":"gt", '"':"quot", "'":"#39"}[$0] + ";"htmlEncode('<a href="http://bn.com" rel="external nofollow" target="_blank" >Barnes & Noble</a>')# => '&lt;a href=&quot;http://bn.com&quot;&gt;Barnes &amp; Noble&lt;/a&gt;'

讨论

可能有更好的途径去执行上述方法。

替换子字符串

问题

你需要用另一个值替换字符串的一部分。

解决方案

使用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!'

每次匹配需要调用回调函数,并且匹配值作为参数传给回调函数。

讨论

正则表达式是一种强有力的方式来匹配和替换字符串。

查找子字符串

问题

你需要搜索一个字符串,并返回匹配的起始位置或匹配值本身。

解决方案

有几种使用正则表达式的方法来实现这个功能。其中一些方法被称为RegExp模式或对象还有一些方法被称为 String 对象。

RegExp 对象

第一种方式是在RegExp模式或对象中调用test方法。test方法返回一个布尔值:

match = /sample/.test("Sample text")# => falsematch = /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

讨论

正则表达式是一种可用来测试和匹配子字符串的强大的方法。

客户端

问题

你想使用网络上提供的服务。

解决方案

创建一个基本的TCP客户机。

在 Node.js 中

net = require 'net'domain = 'localhost'port = 9001connection = net.createConnection port, domainconnection.on 'connect', () ->    console.log "Opened connection to #{domain}:#{port}."connection.on 'data', (data) ->    console.log "Received: #{data}"    connection.end()

使用示例

可访问Basic Server

$ coffee basic-client.coffeeOpened connection to localhost:9001Received: Hello, World!

讨论

最重要的工作发生在connection.on 'data'处理过程中,客户端接收到来自服务器的响应并最有可能安排对它的应答。

另请参阅Basic ServerBi-Directional ClientBi-Directional Server

练习

  • 根据命令行参数或配置文件为选定的目标域和端口添加支持。

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 httphttps文档页面。此外,HTTP spec可能派上用场。

练习

  • 为键值存储HTTP服务器创建一个客户端,使用基本的HTTP服务器方法。

基本的 HTTP 服务器

问题

你想在网络上创建一个HTTP服务器。在这个方法中,我们将逐步从最小的服务器成为一个功能键值存储。

解决方案

我们将使用node.js HTTP库并在Coffeescript中创建最简单的web服务器。

开始 'hi '

我们可以通过导入node.js HTTP模块开始。这会包含createServer,一个简单的请求处理程序返回HTTP服务器。我们可以使用该服务器监听TCP端口。

http = require 'http'server = http.createServer (req, res) -> res.end 'hi
'server.listen 8000

要运行这个例子,只需放在一个文件中并运行它。你可以用ctrl-c终止它。我们可以使用curl命令测试它,可用在大多数*nix平台:

$ curl -D - http://localhost:8000/HTTP/1.1 200 OKConnection: keep-aliveTransfer-Encoding: chunkedhi

发生什么了?

让我们一点点来反馈服务器上发生的事情。这时,我们可以友好的对待用户并提供他们一些HTTP头文件。

http = require 'http'server = http.createServer (req, res) ->    console.log req.method, req.url    data = 'hi
'    res.writeHead 200,        'Content-Type':     'text/plain'        'Content-Length':   data.length    res.end dataserver.listen 8000

再次尝试访问它,但是这一次使用不同的URL路径,比如http://localhost:8000/coffee。你会看到这样的服务器控制台:

$ coffee http-server.coffee GET /GET /coffeeGET /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 + '
'    res.end()server.listen 8000

我们可以试试几种url,看看它们如何回应:

$ curl -D - http://localhost:8000/coffeeHTTP/1.1 200 OKContent-Type: text/plainContent-Length: 7Connection: keep-alivescript$ curl -D - http://localhost:8000/oopsHTTP/1.1 404 Not FoundConnection: keep-aliveTransfer-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>
<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 + '
'    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/coffeeUnknown 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>
<value>#{ value }</value>", xml]        else throw 'Unknown format'# puts a value in the dbput = (store, key, value) ->    throw 'Invalid key' if not key or key is ''    store[key] = valuestore =    foo:    'bar'    coffee: 'script'# helper function that responds to the clientrespond = (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 + '
'        when 'POST'            value = ''            req.on 'data', (chunk) -> value += chunk            req.on 'end', () ->                try                    put store, key, value                    value = ''                    code = 200                catch error                    value = error + '
'                respond res, code, contentType, valueserver.listen 8000

在一个POST请求中注意数据是如何接收的。通过在“数据”和“结束”请求对象的事件中附上一些处理程序,我们最终能够从客户端缓冲和保存数据。

$ curl -D - http://localhost:8000/cookieHTTP/1.1 404 Not Found # ...Unknown key$ curl -D - -d "monster" http://localhost:8000/cookieHTTP/1.1 200 OK # ...$ curl -D - http://localhost:8000/cookieHTTP/1.1 200 OK # ...{"key":"cookie","value":"monster"}

讨论

给http.createServer一个函数 (request,response) - >…… 它将返回一个服务器对象,我们可以用它来监听一个端口。让服务器与request和response对象交互。使用server.listen 8000监听端口8000。

在这个问题上的API和整体信息,参考node.js httphttps文档页面。此外,HTTP spec可能派上用场。

练习

在服务器和开发人员之间创建一个层,允许开发人员做类似的事情:

server = layer.createServer    'GET /': (req, res) ->        ...    'GET /page': (req, res) ->        ...    'PUT /image': (req, res) ->        ...

服务器

问题

你想在网络上提供一个服务器。

解决方案

创建一个基本的TCP服务器。

在 Node.js 中

net = require 'net'domain = 'localhost'port = 9001server = net.createServer (socket) ->    console.log "Received connection from #{socket.remoteAddress}"    socket.write "Hello, World!
"    socket.end()console.log "Listening to #{domain}:#{port}"server.listen port, domain

使用示例

可访问Basic Client

$ coffee basic-server.coffeeListening to localhost:9001Received connection from 127.0.0.1Received connection from 127.0.0.1[...]

讨论

函数将为每个客户端新连接的新插口传递给@net.createServer@ 。基本的服务器与访客只进行简单地交互,但是复杂的服务器会将插口连上一个专用的处理程序,然后返回等待下一个用户的任务。

另请参阅Basic ClientBi-Directional ServerBi-Directional Client

练习

  • 为选定的目标域和基于命令行参数或配置文件的端口添加支持。

双向客户端

问题

你想通过网络提供持续的服务,与客户保持持续的联系。

解决方案

创建一个双向TCP客户机。

在 Node.js 中

net = require 'net'domain = 'localhost'port = 9001ping = (socket, delay) ->    console.log "Pinging server"    socket.write "Ping"    nextPing = -> ping(socket, delay)    setTimeout nextPing, delayconnection = net.createConnection port, domainconnection.on 'connect', () ->    console.log "Opened connection to #{domain}:#{port}"    ping connection, 2000connection.on 'data', (data) ->    console.log "Received: #{data}"connection.on 'end', (data) ->    console.log "Connection closed"    process.exit()

使用示例

可访问Bi-Directional Server

$ coffee bi-directional-client.coffeeOpened connection to localhost:9001Pinging serverReceived: You have 0 peers on this serverPinging serverReceived: You have 0 peers on this serverPinging serverReceived: You have 1 peer on this server[...]Connection closed

讨论

这个特殊示例发起与服务器联系并在@connection.on 'connect'@处理程序中开启对话。大量的工作在一个真正的用户中,然而@connection.on 'data'@处理来自服务器的输出。@ping@函数递归是为了说明连续与服务器通信可能被真实的用户移除。

另请参阅Bi-Directional ServerBasic ClientBasic Server

练习

  • 为选定的目标域和基于命令行参数或配置文件的端口添加支持。

双向服务器

问题

你想通过网络提供持续的服务,与客户保持持续的联系。

解决方案

创建一个双向TCP服务器。

在 Node.js 中

net = require 'net'domain = 'localhost'port = 9001server = 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

使用示例

可访问Bi-Directional Client

$ coffee bi-directional-server.coffeeListening to localhost:9001New connection from 127.0.0.1127.0.0.1 sent: Ping127.0.0.1 sent: Ping127.0.0.1 sent: Ping[...]

讨论

大部分工作在@socket.on 'data'@中 ,处理所有的输入端。真正的服务器可能会将数据传给另一个函数处理并生成任何响应以便源程序处理。

练习

  • 为选定的目标域和基于命令行参数或配置文件的端口添加支持。

适配器模式

问题

想象你去国外旅行,一旦你意识到你的电源线插座与酒店房间墙上的插座不兼容时,幸运的是你记得带你的电源适配器。它将一边连接你的电源线插座另一边连接墙壁插座,允许它们之间进行通信。

同样的情况也可能会出现在代码中,当两个 ( 或更多 ) 实例 ( 类、模块等 ) 想跟对方通信,但其通信协议 ( 例如,他们所使用的语言交流 ) 不同。在这种情况下,Adapter模式更方便。它会充当翻译,从一边到另一边。

解决方案

# a fragment of 3-rd party grid componentclass 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 orderclass NullSorter    sort: (data, order) -> # do nothing; it is just a stubclass 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 dataclass RandomSorterAdapter    constructor: (@sorter) ->    sort: (data, order) ->        @sorter.sort dataagrid = new AwesomeGrid ['a','b','c','d','e','f']agrid.setCustomSorter new RandomSorterAdapter(new RandomSorter)agrid.sort() # sort data with custom sorter through adapter

讨论

当你要组织两个具有不同接口的对象之间的交互时,适配器是有用的。它可以当你使用第三方库或者使用遗留代码时使用。在任何情况下小心使用适配器:它可以是有用的,但它也可以导致设计错误。

桥接模式

问题

你需要为代码保持一个可靠的接口,可以经常变化或者在多种实现间转换。

解决方案

使用桥接模式作为不同的实现和剩余代码的中间体。

假设你开发了一个浏览器的文本编辑器保存到云。然而,现在你需要通过独立客户端的端口将其在本地保存。

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://localhostelse if root?    new FileSaver filename, directory: './' # => Saved "Example data" to temp.txt in ./saver.save data

讨论

桥接模式可以帮助你将特定实现的代码置于看不见的地方,这样你就可以专注于你的程序中的具体代码。在上面的示例中,应用程序的其余部分可以称为saver.save data,不考虑文件的最终结束。

生成器模式

问题

你需要准备一个复杂的、多部分的对象,你希望操作不止一次或有不同的配置。

解决方案

创建一个生成器封装对象的产生过程。

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用户喜欢在任务描述中插入项目和上下文的标签。添加代码来识别这些标签和过滤器的结束标记。

命令模式

问题

你需要让另一个对象处理你自己的可执行的代码。

解决方案

使用Command pattern传递函数的引用。

# Using a private variable to simulate external scripts or modulesincrementers = (() ->    privateVar = 0    singleIncrementer = () ->        privateVar += 1    doubleIncrementer = () ->        privateVar += 2    commands =         single: singleIncrementer        double: doubleIncrementer        value: -> privateVar)()class RunsAll    constructor: (@commands...) ->    run: -> command() for command in @commandsrunner = 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!
"jqxhr.error -> logMessages += "Error!
"jqxhr.complete -> logMessages += "Completed!
"# On a valid AJAX request:# logMessages == "Success!
Completed!
"

修饰模式

问题

你有一组数据,需要在多个过程、可能变换的方式下处理。

解决方案

使用修饰模式来构造如何更改应用。

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 commentsclass 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("
")).join("
")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>
<p>A regular line</p>

<h2>A level 2 header</h2>
<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    lineprocessor.processors.unshift smilieExpanderprocessor.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>"

工厂方法模式

问题

直到开始运行你才知道需要的是什么种类的对象。

解决方案

使用工厂方法(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 JSONParserfactory = new ParserFactoryfactory.makeParser("example.html").type # => "HTML parser"factory.makeParser("example.md").type # => "Markdown parser"factory.makeParser("example.json").type # => "JSON parser"

讨论

在这个示例中,你可以关注解析的内容,忽略细节文件的格式。更先进的工厂方法,例如,搜索版本控制文件中的数据本身,然后返回一个更精确的解析器(例如,返回一个HTML5解析器而不是HTML v4解析器)。

解释器模式

问题

其他人需要以控制方式运行你的一部分代码。相对地,你选择的语言不能以一种简洁的方式表达问题域。

解决方案

使用解释器模式来创建一个你翻译为特定代码的领域特异性语言(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 resultcalc = new StackCalculatorcalc.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'    swwheat = (sw) ->    sw.bread = 'wheat'    swturkey = (sw) ->    sw.toppings.push 'turkey'    swham = (sw) ->    sw.toppings.push 'ham'    swswiss = (sw) ->    sw.toppings.push 'swiss'    swmayo = (sw) ->    sw.toppings.push 'mayo'    swtoasted = (sw) ->    sw.toasted = true    swsandwich = (customer) ->    new Sandwich customerto = (customer) ->    customersend = (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语言技能和你现有的语言技能可以帮助你关于捕捉代码的问题。

备忘录模式

问题

你想预测对一个对象做出改变后的反应。

解决方案

使用备忘录模式(Memento Pattern)来跟踪一个对象的变化。使用这个模式的类会输出一个存储在其他地方的备忘录对象。

如果你的应用程序可以让用户编辑文本文件,例如,他们可能想要撤销上一个动作。你可以在用户改变文件之前保存文件现有的状态,然后回滚到上一个位置。

class PreserveableText    class Memento        constructor: (@text) ->    constructor: (@text) ->    save: (newText) ->        memento = new Memento @text        @text = newText        memento    restore: (memento) ->        @text = memento.textpt = 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 mementopt.text # => "The original string"

讨论

备忘录对象由PreserveableText#save返回,为了安全保护,分别地存储着重要的状态信息。你可以序列化备忘录以便来保证硬盘中的“撤销”缓冲或者是那些被编辑的图片等数据密集型对象。

观察者模式

问题

当一个事件发生时你不得不向一些对象发布公告。

解决方案

使用观察者模式(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 "+itemclass NewspaperSubscriber    onNewNewspaper: (item) ->        alert "I've got new "+itempostOffice = new PostOffice()sub1 = new MagazineSubscriber()sub2 = new NewspaperSubscriber()postOffice.subscribe "Mens Health", sub1.onNewMagazinepostOffice.subscribe "Times", sub2.onNewNewspaperpostOffice.notifyNewItemReleased "Times"postOffice.notifyNewItemReleased "Mens Health"

讨论

这里你有一个观察者对象(PostOffice)和可观察对象(MagazineSubscriber, NewspaperSubscriber)。为了通报发布新的周期性可观察对象的事件,应该对 PostOffice进行订阅。每一个被订阅的对象都存储在PostOffice的内部订阅数组中。当新的实体周期发布时每一个订阅者都会收到通知。

单件模式

问题

许多时候你想要一个,并且只要一个类的实例。比如,你可能需要一个创建服务器资源的类,并且你想要保证使用一个对象就可以控制这些资源。但是使用时要小心,因为单件模式可以很容易被滥用来模拟不必要的全局变量。

解决方案

公有类只包含获得一个实例的方法。实例被保存在该公共对象的闭包中,并且总是有返回值。

这很奏效因为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 # => undefineda.instance # => undefinedSingleton.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: -> @messageclass 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 moduleroot.Singleton = Singleton

我们可以注意到coffeescript是如此简单地实现这个设计模式。为了更好地参考和讨论JavaScript的实现,请看初学者必备 JavaScript 设计模式

策略模式

问题

解决问题的方式有多种,但是你需要在程序运行时选择(或是转换)这些方法。

解决方案

在策略对象(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()    listreverseBubbleSort = (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 bubbleSortunsortedList = ['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 = reverseBubbleSortsorter.sort unsortedList# => ['a', 'b', 'c', 'd', 'e', 'w', 'x']

讨论

“没有作战计划在第一次接触敌人时便能存活下来。” 用户如是,但是我们可以运用从变化的情况中获得的知识来做出适应改变。在示例末尾,例如,数组中的最新项是乱序排列的,知道了这个细节,我们便可以通过切换算法来加速排序,只要简单地重赋值就可以了。

练习

  • 将StringSorter扩展为AlwaysSortedArray类来实现规则序列的所有功能,但是要基于插入方法自动分类新的项(例如push对比shift)。

模板方法模式

问题

定义一个算法的结构,作为一系列的高层次的步骤,使每一个步骤的行为可以指定,使属于一个族的算法都具有相同的结构但是有不同的行为。

解决方案

使用模板方法(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方法的问题。

MongoDB

问题

你需要与一个MongoDB数据库连接的接口。

解决方案

对于 Node.js

安装

保存记录

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 neededexampleSave = (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 模块可以帮助这样的异步调用。

SQLite

问题

你需要Node.js内部与SQLite数据库连接的接口。

解决方案

使用SQLite 模块

sqlite = require 'sqlite'db = new sqlite.Database# The module uses asynchronous methods,# so we chain the calls the db.executeexampleCreate = ->    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 diskdb.open ":memory:", (openErr) ->    throw openErr if openErr    exampleCreate()

讨论

你也可以提前准备你的SQL查询语句。

sqlite = require 'sqlite'async = require 'async' # Not required but added to make the example more concisedb = new sqlite.DatabasecreateSQL = "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模块文档提供了更完整的信息。

使用 Jasmine 测试

问题

假如你正在使用CoffeeScript写一个简单地计算器,并且想要验证其功能是否与预期一致。可以使用Jasmine测试框架。

讨论

在使用Jasmine测试框架时,你要在一个参数(spec)文档中写测试,文档描述的是代码需要测试的预期功能。

例如,我们希望计算器可以实现加法和减法的功能,并且可以正确进行正数和负数的运算。我们的spec文档如下列所示。

# calculatorSpec.coffeedescribe '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="https://atts.51coolma.cn/attachments/image/wk/coffeescript/jquery.min.js"></script> 9   <script src="https://atts.51coolma.cn/attachments/image/wk/coffeescript/jasmine.js"></script>10   <script src="https://atts.51coolma.cn/attachments/image/wk/coffeescript/jasmine-html.js"></script>11   <script src="https://atts.51coolma.cn/attachments/image/wk/coffeescript/jasmine-jquery-1.3.1.js"></script>12 13   <!-- include source files here... -->14   <script src="https://atts.51coolma.cn/attachments/image/wk/coffeescript/calculator.js"></script>15 16   <!-- include spec files here... -->17   <script src="https://atts.51coolma.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个失败情况(如下)。

Alt text

看来我们的测试是失败的,因为jasmine无法找到Calculator变量。那是因为它还没有被创建。现在让我们来创建一个新文件命名为js/calculator.coffee。

# calculator.coffeewindow.Calculator = class Calculator

编译calculator.coffee并刷新浏览器来重新运行测试组。

Alt text

现在我们还有4个失败而不是原来的8个了,只用一行代码便做出了50%的改进。

测试通过

实现我们的方法来看是否可以通过测试。

# calculator.coffeewindow.Calculator = class Calculator    add: (a, b) ->        a + b    subtract: (a, b) ->        a - b

当我们刷新页面时可以看到全部通过。

Alt text

重构测试

既然测试全部通过了,我们应看一看我们的代码或测试是否可以被重构。

在我们的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然后刷新浏览器,可以看到测试仍然全部通过。

Alt text

使用 Nodeunit 测试

问题

假如你正在使用CoffeeScript并且想要验证功能是否与预期一致,便可以决定使用Nodeunit测试框架。

讨论

Nodeunit是一种JavaScript对于单元测试库( Unit Testing libraries )中xUnit族的实现,Java, Python, Ruby, Smalltalk中均可以使用。

当使用xUnit族测试框架时,你需要将所需测试的描述预期功能的代码写在一个文件中。

例如,我们希望我们的计算器可以进行加法和减法,并且对于正负数均可以正确计算,我们的测试如下。

# test/calculator.test.coffeeCalculator = 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 testnpm 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.coffeeclass Calculatormodule.exports = Calculator

并且重新运行测试套件。

suki@Yuzuki:nodeunit_testing (master)$ npm testnpm 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 testcalculator.test✖ CalculatorTest - test can add two positive numbersTypeError: Object #<Calculator> has no method 'add'  ...✖ CalculatorTest - test can handle negative number additionTypeError: Object #<Calculator> has no method 'add'  ...✖ CalculatorTest - test can subtract two positive numbersTypeError: Object #<Calculator> has no method 'subtract'  ...✖ CalculatorTest - test can handle negative number subtractionTypeError: 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.coffeeclass Calculator  add: (a, b) ->    a + b  subtract: (a, b) ->    a - bmodule.exports = Calculator

当我们重新运行测试时可以看到全部通过:

suki@Yuzuki:nodeunit_testing (master)$ npm testnpm 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 testcalculator.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 subtractionOK: 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()

我们可以重新运行测试,仍然可以全部通过。

服务端和客户端的代码重用

问题

当你在CoffeeScript上创建了一个函数,并希望将它用在有网页浏览器的客户端和有Node.js的服务端时。

解决方案

以下列方法输出函数:

# simpleMath.coffee# these methods are privateadd = (a, b) ->    a + bsubtract = (a, b) ->    a - bsquare = (x) ->    x * x# create a namespace to export our public methodsSimpleMath = exports? and exports or @SimpleMath = {}# items attached to our namespace are available in Node.js as well as client browsersclass 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 =  11undefined>

在网页中,我们可以通过将模块作为一个脚本嵌入其中。

<!DOCTYPE HTML><html lang="en-US"><head>    <meta charset="UTF-8">    <title>SimpleMath Module Example</title>    <script src="https://atts.51coolma.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的连缀比较语法。

maxDwarfism = 147minAcromegaly = 213height = 180normalHeight = maxDwarfism < height < minAcromegaly# => true

讨论

这是从Python中借鉴过来的一个很棒的特性。利用这个特性,不必像下面这样写出完整的比较:

normalHeight = height > maxDwarfism && height < minAcromegaly

CoffeeScript支持像写数学中的比较表达式一样连缀两个比较,这样更直观。

相关教程

《python基础教程》

嵌入 JavaScript

问题

你想在CoffeeScript中嵌入找到的或预先编写的JavaScript代码。

解决方案

把JavaScript包装到撇号中:

`function greet(name) {return "Hello "+name;}`# Back to CoffeeScriptgreet "Coffee"# => "Hello Coffee"

讨论

这是在CoffeeScript代码中集成少量JavaScript而不必用CoffeeScript语法转换它们的最简单的方法。正如CoffeeScript Language Reference中展示的,可以在一定范围内混合这两种语言的代码:

hello = `function (name) {return "Hello "+name}`hello "Coffee"# => "Hello Coffee"

这里的变量"hello"还在CoffeeScript中,但赋给它的函数则是用JavaScript写的。

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循环。

对象的链式调用

问题

你想调用一个对象上的多个方法,但不想每次都引用该对象。

解决方案

在每次链式调用后返回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        thismorningCup = 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]            objclass TeaCup    constructor:  ->        @properties=            size: 'medium'            type: 'black'            sugar: false            cream: false        addChainedAttributeAccessor(this, 'properties', attr) for attr of @propertiesearlgrey = new TeaCup().size('small').type('Earl Grey').sugar('false')earlgrey.properties # => { size: 'small', type: 'Earl Grey', sugar: false }earlgrey.sugar trueearlgrey.sugar() # => true

类方法和实例方法

问题

你想创建类和实例的方法。

解决方案

类方法

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.titlesSongs.get_count()# => 0song = new Songs("Rick Astley", "Never Gonna Give You Up")Songs.get_count()# => 1song.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()# => 1Songs.get_count()# => TypeError: Object function Songs(artist, title) ... has no method 'get_count'

讨论

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# => 50Zoo.MAX_ZOOKEEPERS# => undefined (it is a prototype member)Zoo::MAX_ZOOKEEPERS# => 3zoo = new Zoozoo.MAX_ZOOKEEPERS# => 3zoo.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 namezoo = 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 Javascriptzoo = 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会将类变量的值保存在类中而不是它定义的原型中。这在定义类中的变量时是十分有用的,因为这不会被实体属性变量重写。

克隆对象(深度复制)

问题

你想复制一个对象,包含其所有子对象。

解决方案

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 newInstancex =  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 = xy.foo = 'test'console.log x.foo isnt y.foo, x.foo, y.foo# => false, test, test

显然,复制之后修改y也就修改了x。

类的混合

问题

你有一些通用方法,你想把他们包含到很多不同的类中。

解决方案

使用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: ->    42class PhilosopherMixin  pontificate: ->    console.log "hmm..."    @wise = yesclass DeeperThought extends mixOf DeepThought, PhilosopherMixin  answer: ->    @pontificate()    super()earth = new DeeperThoughtearth.answer()# hmm...# => 42

讨论

这适用于轻量级的混合。因此你可以从基类和基类的祖先中继承方法,也可以从混合类的基类和祖先中继承,但是不能从混合类的祖先中继承。与此同时,在声明了一个混合类后,此后的对这个混合类进行的改变是不会反应出来的。

创建一个不存在的对象字面值

问题

你想初始化一个对象字面值,但如果这个对象已经存在,你不想重写它。

解决方案

使用存在判断运算符(existential operator)。

window.MY_NAMESPACE ?= {} 

讨论

这行代码与下面的JavaScript代码等价:

window.MY_NAMESPACE = window.MY_NAMESPACE || {};

这是JavaScript中一个常用的技巧,即使用对象字面值来定义命名空间。这样先判断是否存在同名的命名空间然后再创建,可以避免重写已经存在的命名空间。

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

大写单词首字母

问题

你想把字符串中每个单词的首字母转换为大写形式。

解决方案

使用“拆分-映射-拼接”模式:先把字符串拆分成单词,然后通过映射来大写单词第一个字母小写其他字母,最后再将转换后的单词拼接成字符串。

("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'

但这样做又会导致第二个问题:在结果字符串中,原来的空白符串经过拼接就只剩下一个空格了。

不过,一般来说,这两个问题还是可以接受的。所以,“拆分-映射-拼接”仍然是一种有效的技术。

查找子字符串

问题

你想在一条消息中查找某个关键字第一次或最后一次出现的位置。

解决方案

分别使用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 parametermessage.indexOf "This", 5# => 23message.lastIndexOf "This"# => 49

讨论

还需要想办法统计出给定字符串在一条消息中出现的次数。

生成唯一ID

问题

你想随机生成一个唯一的标识符。

解决方案

可以根据一个随机数值生成一个Base 36编码的字符串。

uniqueId = (length=8) ->  id = ""  id += Math.random().toString(36).substr(2) while id.length < length  id.substr 0, lengthuniqueId()    # => n5yjla3buniqueId(2)   # => 0duniqueId(20)  # => ox9eo7rt3ej0pb9kqlkeuniqueId(40)  # => xu2vo4xjn4g0t3xr74zmndshrqlivn291d584alj

讨论

使用其他技术也可以,但这种方法相对来说性能更高,也更灵活。

字符串插值

问题

你想创建一个字符串,让它包含体现某个CoffeeScript变量的文本。

解决方案

使用CoffeeScript中类似Ruby的字符串插值,而不是JavaScript的字符串拼接。

插值:

muppet = "Beeker"favorite = "My favorite muppet is #{muppet}!"# => "My favorite muppet is Beeker!"
square = (x) -> x * xmessage = "The square of 7 is #{square 7}."# => "The square of 7 is 49."

讨论

CoffeeScript的插值与Ruby类似,多数表达式都可以用在#{ ... }插值结构中。

CoffeeScript支持在插值结构中放入多个有副作用的表达式,但建议大家不要这样做。因为只有表达式的最后一个值会被插入。

# 可以这样做,但不要这样做。否则,你会疯掉。square = (x) -> x * xmuppet = "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."

相关教程

《Ruby教程》

把字符串转换为小写形式

问题

你想把字符串转换成小写形式。

解决方案

使用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?

匹配字符串

问题

你想要匹配两个或多个字符串。

解决方案

计算把一个字符串转换成另一个字符串所需的编辑距离或操作数。

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中交换两个变量的语法。

重复字符串

问题

你想重复一个字符串。

解决方案

创建一个包含n+1个空元素的数组,然后用要重复的字符串作为连接字符将数组元素拼接到一起:

# 创建包含10个foo的字符串Array(11).join 'foo'# => "foofoofoofoofoofoofoofoofoofoo"

为字符串重复方法

你也可以在字符串的原型中为其创建方法。它十分简单:

# 为所有的字符串添加重复方法,这会重复返回 n 次字符串String::repeat = (n) -> Array(n+1).join(this)

讨论

JavaScript缺少字符串重复函数,CoffeeScript也没有提供。虽然在这里也可以使用列表推导( comprehensions ),但对于简单的字符串重复来说,还是像这样先构建一个包含n+1个空元素的数组,然后再把它拼接起来更方便。

拆分字符串

问题

你想拆分一个字符串。

解决方案

使用JavaScript字符串的split()方法:

"foo bar baz".split " "# => [ 'foo', 'bar', 'baz' ]

讨论

String的这个split()方法是标准的JavaScript方法。可以用来基于任何分隔符——包括正则表达式来拆分字符串。这个方法还可以接受第二个参数,用于指定返回的子字符串数目。

"foo-bar-baz".split "-"# => [ 'foo', 'bar', 'baz' ]
"foo   bar  	 baz".split /s+/# => [ 'foo', 'bar', 'baz' ]
"the sun goes down and I sit on the old broken-down river pier".split " ", 2# => [ 'the', 'sun' ]

清理字符串前后的空白符

问题

你想清理字符串前后的空白符。

解决方案

使用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的这篇博客文章

把字符串转换为大写形式

问题

你想把字符串转换成大写形式。

解决方案

使用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?

检查变量的类型是否为数组

问题

你希望检查一个变量是否为一个数组。

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' )

将数组连接

问题

你希望将两个数组连接到一起。

解决方案

在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, array2array1# => [1, 2, 3, 4, 5, 6]

在上面的例子中,Array.prototype.push.apply(a, b)方法修改了array1而没有产生一个新的数组对象。

在CoffeeScript中,我们可以简化上面的方式,通过给数组创建一个新方法merge():

Array::merge = (other) -> Array::push.apply @, otherarray1 = [1, 2, 3]array2 = [4, 5, 6]array1.merge array2array1# => [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方法。

由数组创建一个对象词典

问题

你有一组对象,例如:

cats = [  {    name: "Bubbles"    age: 1  },  {    name: "Sparkle"    favoriteFood: "tuna"  }]

但是你想让它像词典一样,可以通过关键字访问它,就像使用cats["Bubbles"]。

解决方案

你需要将你的数组转换为一个对象。通过这样使用reduce:

# key = The key by which to index the dictionaryArray::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" }

由数组创建一个字符串

问题

你想由数组创建一个字符串。

解决方案

使用JavaScript的数组方法toString():

["one", "two", "three"].toString()# => 'one,two,three'

讨论

toString()是一个标准的JavaScript方法。不要忘记圆括号。

定义数组范围

问题

你想定义一个数组的范围。

解决方案

在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 ]

讨论

包含范围以“..”运算符定义,包含最后一个值。 排除范围以 “...” 运算符定义,并且通常忽略最后一个值。

筛选数组

问题

你想要根据布尔条件来筛选数组。

解决方案

使用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 > 5filtered_array = array.filter gt_five# => [6,7,8,9,10]

讨论

这个方法与Ruby的Array的#select方法类似。

列表推导

问题

你有一个对象数组,想将它们映射到另一个数组,类似于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的语句时,他们会很好地起到作用。对于简单的映射,列表推导具有更好的可读性。但是对于复杂的转换或链式映射,映射数组可能更合适。

映射数组

问题

你有一个对象数组,想把这些对象映射到另一个数组中,就像 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) 看起来会清楚一些。

数组最大值

问题

你需要找出数组中包含的最大的值。

解决方案

你可以使用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。

归纳数组

问题

你有一个对象数组,想要把它们归纳为一个值,类似于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为匿名函数提供了简单自然的表达语法。二者配合使用,可以把集合的项合并为组合的结果。

删除数组中的相同元素

问题

你想从数组中删除相同元素。

解决方案

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

反转数组

问题

你想要反转数组元素。

解决方案

使用 JavaScript Array 的 reverse() 方法

["one", "two", "three"].reverse()# => ["three", "two", "one"]

讨论

reverse()是标准的JavaScript方法,别忘了带圆括号。

打乱数组中的元素

问题

你想打乱数组中的元素。

解决方案

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]]  sourceshuffle([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也是一个不错的选择。

检测每个元素

问题

你希望能够在特定的情况下检测出在数组中的每个元素。

解决方案

使用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的解构赋值语法:

a = 1b = 3[a, b] = [b, a]a# => 3b# => 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 bintersection ray1, ray2# => [ 5, 9 ]intersection ray2, ray1# => [ 5, 9 ]

对象数组

问题

你想要得到一个与你的某些属性匹配的数组对象。

你有一系列的对象,如:

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 falsecats.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 falsecats.where name:"bubbles"# => []# it's case sensitivecats.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” 。

类似 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 resultszip([0, 1, 2, 3], [0, -1, -2, -3])# => [[0, 0], [1, -1], [2, -2], [3, -3]]

计算复活节的日期

问题

你需要在给出的年份中找到复活节的月份和日期。

解决方案

下面的函数返回数组有两个要素:复活节的月份( 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]

计算(美国和加拿大的)感恩节日期

问题

你需要在给出的年份中找到感恩节的月份和日期。

解决方案

下面的函数返回给出年份的感恩节的日期。如果没有给出任何参数,给出的结果是当前年份。

美国的感恩节是十一月的第四个星期四。  

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)

这个想法很简单:

  1. 找出哪一天是以下各月份的第一天(美国十一月,加拿大十月)。
  2. 计算从那天起偏移到下一个工作日的量(美国星期四,加拿大星期一)。
  3. 将这个偏移量加上第一个可能的假期日期(第二十二个美国感恩节,第八个加拿大感恩节)。

计算两个日期中间的天数

问题

你需要找出两个日期间隔了几年,几个月,几天,几个小时,几分钟,几秒。

解决方案

利用JavaScript的日期计算函数getTime()。它提供了从1970年1月1日开始经过了多少毫秒。

DAY = 1000 * 60 * 60  * 24d1 = 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 * 60d1 = new Date('02/01/2011 02:20')d2 = new Date('02/06/2011 05:20')hour_passed = Math.round((d2.getTime() - d1.getTime()) / HOUR)

找到一个月中的最后一天

问题

你需要去找出一个月的最后一天,但是一年中的各月并没有一个固定时间表。

解决方案

利用JavaScript的日期下溢来找到给出月份的第一天:

now = new DatelastDayOfTheMonth = new Date(1900+now.getYear(), now.getMonth()+1, 0)

讨论

JavaScript的日期构造函数成功地处理溢出和下溢情况,使日期的计算变得很简单。鉴于这种简单操作,不需要担心一个给定的月份里有多少天;只需要用数学稍加推导。在十二月,以上的解决方案就是寻找当前年份的第十三个月的第0天日期,那么它就是下一年的一月一日,也计算出来今年十二月份31号的日期。

找到上一个月(或下一个月)

问题

你需要计算相关日期范围例如“上一个月”,“下一个月”。

解决方案

添加或减去当月的数字,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"

计算月球的相位

问题

你想找出月球的相位。

解决方案

以下代码提供了一种计算给出日期的月球相位计算方案:

# 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 @WebCoding4Funproper_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    tmpjdn = (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.4999999moonElong = (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 moonNumgetMoonPhase = (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    moonPhaseMoonPhase = 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'

数学常数

问题

你需要使用常见的数学常数,比如π或者e。

解决方案

使用Javascript的Math object来提供通常需要的数学常数。

Math.PI# => 3.141592653589793# Note: Capitalization matters! This produces no output, it's undefined.Math.Pi# =>Math.E# => 2.718281828459045Math.SQRT2# => 1.4142135623730951Math.SQRT1_2# => 0.7071067811865476# Natural log of 2. ln(2)Math.LN2# => 0.6931471805599453Math.LN10# => 2.302585092994046Math.LOG2E# => 1.4426950408889634Math.LOG10E# => 0.4342944819032518

讨论

另外一个例子是关于一个数学常数用于真实世界的问题,是数学章节有关[弧度和角度]( http://coffeescript-cookbook.github.io/chapters/math/radians-degrees)的部分

更快的 Fibonacci 算法

问题

你想计算出Fibonacci数列中的数值N ,但需迅速地算出结果。

解决方案

下面的方案(仍有需改进的地方)最初在Robin Houston的博客上被提出来。

这里给出一些关于该算法和改进方法的链接:

以下的代码来源于:https://gist.github.com/1032685

###Author: Jason Giedymin <jasong _a_t_ apache -dot- org>        http://www.jasongiedymin.com        https://github.com/JasonGiedyminCoffeeScript 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 = 1476fib_bits = (n) ->    #代表一个作为二进制数字阵列的整数    bits = []    while n > 0        [n, bit] = divmodBasic n, 2        bits.push bit    bits.reverse()    return bitsfibFast = (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 bdivmodNewton = (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."  

平方根倒数快速算法

问题

你想快速计算某数的平方根倒数。

解决方案

在QuakeⅢ Arena的源代码中,这个奇怪的算法对一个幻数进行整数运算,来计算平方根倒数的浮点近似值。

在CoffeeScript中,他使用经典原始的变量,以及由Chris Lomont发现的新的最优32位幻数。除此之外,还使用64位大小的幻数。

另一特征是可以通过控制牛顿迭代法的迭代次数来改变其精确度。

相比于传统的,该算法在性能上更胜一筹,这归功于使用的机器及其精确度。

运行的时候使用coffee -c script.coffee来编译script:

然后复制粘贴编译的JS代码到浏览器的JavaScript控制台。

注意:你需要一个支持类型数组的浏览器

参考:

  1. ftp://ftp.idsoftware.com/idstuff/source/quake3-1.32b-source.zip
  2. http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf
  3. http://en.wikipedia.org/wiki/Newton%27s_method
  4. https://developer.mozilla.org/en/JavaScripttypedarrays
  5. 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()

生成可预测的随机数

问题

你需要生成在一定范围内的随机数,但你也需要对发生器进行“生成种子”操作来提供可预测的值。

解决方案

编写你自己的随机数生成器。当然有很多方法可以做到这一点,这里给出一个简单的示例。 该发生器绝对不可以以加密为目的!

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)。

生成随机数

问题

你需要生成在一定范围内的随机数。

解决方案

使用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# => truedice = Math.floor(Math.random() * 6) + 11 <= dice <= 6# => truemax = 42min = -13range = Math.random() * (max - min) + min-13 <= range < 42# => true

讨论

对于JavaScript来说,它更直接更快。

需要注意到JavaScript的Math.random()不能通过发生器生成随机数种子来得到特定值。详情可参考产生可预测的随机数

产生一个从0到n(不包括在内)的数,乘以n。
产生一个从1到n(包含在内)的数,乘以n然后加上1。

转换弧度和度

问题

你需要实现弧度和度之间的转换。

解决方案

使用JavaScript的Math.PI和一个简单的公式来转换两者。

# 弧度转换成度radiansToDegrees = (radians) ->    degrees = radians * 180 / Math.PIradiansToDegrees(1)# => 57.29577951308232# 度转换成弧度degreesToRadians = (degrees) ->    radians = degrees * Math.PI / 180degreesToRadians(1)# => 0.017453292519943295

一个随机整数函数

问题

你想要获得两个整数(包含在内)之间的一个随机整数。

解决方案

使用以下的函数。

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]

指数对数运算

问题

你需要进行包含指数和对数的运算。

解决方案

使用JavaScript的Math对象来提供常用的数学函数。

# Math.pow(x, y) 返回 x^yMath.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) logMath.log(5)# => 1.6094379124341003Math.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 开发者网络上的文档。另可参阅数学常量关于数学对象中各种常量的讨论。

去抖动函数

问题

你想只执行某个函数一次,在开始或结束时把多个连续的调用合并成一个简单的操作。

解决方案

使用一个命名函数:

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 || 100mouseMoveHandler: (e) ->  @debounce((e) ->    # 只能在鼠标光标停止 300 毫秒后操作一次。  300)someOtherHandler: (e) ->  @debounce((e) ->    # 只能在初次执行 250 毫秒后操作一次。  250, true)

讨论

可参阅John Hann的博客文章,了解JavaScript 去抖动方法

当函数括号不可选

问题

你想要调用一个没有参数的函数,但不希望使用括号。

解决方案

不管怎样都使用括号。

另一个方法是使用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表示法是一个巧妙的方法来克服括号使用恐惧症。尽管有部分人宁愿在函数调用中写出所有括号。

递归函数

问题

你想在一个函数中调用相同的函数。

解决方案

使用一个命名函数:

ping = ->    console.log "Pinged"    setTimeout ping, 1000

若为未命名函数,则使用@arguments.callee@:

delay = 1000setTimeout((->    console.log "Pinged"    setTimeout arguments.callee, delay    ), delay)

讨论

虽然arguments.callee允许未命名函数的递归,在内存密集型应用中占有一定优势,但是命名函数相对来说目的更加明确,也更易于代码的维护。

提示参数

问题

你的函数将会被可变数量的参数所调用。

解决方案

使用splat

loadTruck = (firstDibs, secondDibs, tooSlow...) ->    truck:        driversSeat: firstDibs        passengerSeat: secondDibs        trunkBed: tooSlowloadTruck("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: leftAtHomeloadTruck("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能把所有不被其他命名参数采用的参数值整合进一个列表中。就算并没有提供命名参数,它也会制造一个空列表。

检测与构建丢失的函数

问题

你想要检测一个函数是否存在,如果不存在则构建该函数。(比如Internet Explorer 8的ECMAScript 5函数)。

解决方案

使用存在赋值运算符(?=)来把函数分配给类库的原型(使用::简写),然后把它放于一个立即执行函数表达式中(do ->)使其含有所有变量。

do -> Array::filter ?= (callback) ->  element for element in this when callback elementarray = [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 ->`})();

扩展内置对象

问题

你想要扩展一个类来增加新的函数或者替换旧的。

解决方案

使用 :: 把你的新函数分配到对象或者类的原型中。

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:不要修改你不拥有的对象扩展内置的本地对象。对还是错?。)

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

回调绑定

问题

你想要把一个回调与一个对象绑定在一起。

解决方案

$ ->  class Basket    constructor: () ->      @products = []      $('.product').click (event) =>        @add $(event.currentTarget).attr 'id'    add: (product) ->      @products.push product      console.log @products  new Basket()

讨论

通过使用等号箭头(=>)取代正常箭头(->),函数将会自动与对象绑定,并可以访问@-可变量。

创建 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

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

不使用 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.coffee2 loadDataFromServer = ->3   req = new XMLHttpRequest()4 5   req.addEventListener 'readystatechange', ->6     if req.readyState is 4                        # ReadyState Complete7       successResultCodes = [200, 304]8       if req.status in successResultCodes9         data = eval '(' + req.responseText + ')'10         console.log 'data message: ', data.message11       else12         console.log 'Error loading data...'13 14   req.open 'GET', 'data.json', false15   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对象在全局命名空间中可用。

使用 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空格。

使用 HTML 命名实体替换 HTML 标签

问题

你需要使用命名实体来替代HTML标签:

<br/> => &lt;br/&gt;

解决方案

htmlEncode = (str) ->  str.replace /[&<>"']/g, ($0) ->    "&" + {"&":"amp", "<":"lt", ">":"gt", '"':"quot", "'":"#39"}[$0] + ";"htmlEncode('<a href="http://bn.com" rel="external nofollow" target="_blank" >Barnes & Noble</a>')# => '&lt;a href=&quot;http://bn.com&quot;&gt;Barnes &amp; Noble&lt;/a&gt;'

讨论

可能有更好的途径去执行上述方法。

替换子字符串

问题

你需要用另一个值替换字符串的一部分。

解决方案

使用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!'

每次匹配需要调用回调函数,并且匹配值作为参数传给回调函数。

讨论

正则表达式是一种强有力的方式来匹配和替换字符串。

查找子字符串

问题

你需要搜索一个字符串,并返回匹配的起始位置或匹配值本身。

解决方案

有几种使用正则表达式的方法来实现这个功能。其中一些方法被称为RegExp模式或对象还有一些方法被称为 String 对象。

RegExp 对象

第一种方式是在RegExp模式或对象中调用test方法。test方法返回一个布尔值:

match = /sample/.test("Sample text")# => falsematch = /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

讨论

正则表达式是一种可用来测试和匹配子字符串的强大的方法。

客户端

问题

你想使用网络上提供的服务。

解决方案

创建一个基本的TCP客户机。

在 Node.js 中

net = require 'net'domain = 'localhost'port = 9001connection = net.createConnection port, domainconnection.on 'connect', () ->    console.log "Opened connection to #{domain}:#{port}."connection.on 'data', (data) ->    console.log "Received: #{data}"    connection.end()

使用示例

可访问Basic Server

$ coffee basic-client.coffeeOpened connection to localhost:9001Received: Hello, World!

讨论

最重要的工作发生在connection.on 'data'处理过程中,客户端接收到来自服务器的响应并最有可能安排对它的应答。

另请参阅Basic ServerBi-Directional ClientBi-Directional Server

练习

  • 根据命令行参数或配置文件为选定的目标域和端口添加支持。

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 httphttps文档页面。此外,HTTP spec可能派上用场。

练习

  • 为键值存储HTTP服务器创建一个客户端,使用基本的HTTP服务器方法。

基本的 HTTP 服务器

问题

你想在网络上创建一个HTTP服务器。在这个方法中,我们将逐步从最小的服务器成为一个功能键值存储。

解决方案

我们将使用node.js HTTP库并在Coffeescript中创建最简单的web服务器。

开始 'hi '

我们可以通过导入node.js HTTP模块开始。这会包含createServer,一个简单的请求处理程序返回HTTP服务器。我们可以使用该服务器监听TCP端口。

http = require 'http'server = http.createServer (req, res) -> res.end 'hi
'server.listen 8000

要运行这个例子,只需放在一个文件中并运行它。你可以用ctrl-c终止它。我们可以使用curl命令测试它,可用在大多数*nix平台:

$ curl -D - http://localhost:8000/HTTP/1.1 200 OKConnection: keep-aliveTransfer-Encoding: chunkedhi

发生什么了?

让我们一点点来反馈服务器上发生的事情。这时,我们可以友好的对待用户并提供他们一些HTTP头文件。

http = require 'http'server = http.createServer (req, res) ->    console.log req.method, req.url    data = 'hi
'    res.writeHead 200,        'Content-Type':     'text/plain'        'Content-Length':   data.length    res.end dataserver.listen 8000

再次尝试访问它,但是这一次使用不同的URL路径,比如http://localhost:8000/coffee。你会看到这样的服务器控制台:

$ coffee http-server.coffee GET /GET /coffeeGET /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 + '
'    res.end()server.listen 8000

我们可以试试几种url,看看它们如何回应:

$ curl -D - http://localhost:8000/coffeeHTTP/1.1 200 OKContent-Type: text/plainContent-Length: 7Connection: keep-alivescript$ curl -D - http://localhost:8000/oopsHTTP/1.1 404 Not FoundConnection: keep-aliveTransfer-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>
<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 + '
'    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/coffeeUnknown 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>
<value>#{ value }</value>", xml]        else throw 'Unknown format'# puts a value in the dbput = (store, key, value) ->    throw 'Invalid key' if not key or key is ''    store[key] = valuestore =    foo:    'bar'    coffee: 'script'# helper function that responds to the clientrespond = (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 + '
'        when 'POST'            value = ''            req.on 'data', (chunk) -> value += chunk            req.on 'end', () ->                try                    put store, key, value                    value = ''                    code = 200                catch error                    value = error + '
'                respond res, code, contentType, valueserver.listen 8000

在一个POST请求中注意数据是如何接收的。通过在“数据”和“结束”请求对象的事件中附上一些处理程序,我们最终能够从客户端缓冲和保存数据。

$ curl -D - http://localhost:8000/cookieHTTP/1.1 404 Not Found # ...Unknown key$ curl -D - -d "monster" http://localhost:8000/cookieHTTP/1.1 200 OK # ...$ curl -D - http://localhost:8000/cookieHTTP/1.1 200 OK # ...{"key":"cookie","value":"monster"}

讨论

给http.createServer一个函数 (request,response) - >…… 它将返回一个服务器对象,我们可以用它来监听一个端口。让服务器与request和response对象交互。使用server.listen 8000监听端口8000。

在这个问题上的API和整体信息,参考node.js httphttps文档页面。此外,HTTP spec可能派上用场。

练习

在服务器和开发人员之间创建一个层,允许开发人员做类似的事情:

server = layer.createServer    'GET /': (req, res) ->        ...    'GET /page': (req, res) ->        ...    'PUT /image': (req, res) ->        ...

服务器

问题

你想在网络上提供一个服务器。

解决方案

创建一个基本的TCP服务器。

在 Node.js 中

net = require 'net'domain = 'localhost'port = 9001server = net.createServer (socket) ->    console.log "Received connection from #{socket.remoteAddress}"    socket.write "Hello, World!
"    socket.end()console.log "Listening to #{domain}:#{port}"server.listen port, domain

使用示例

可访问Basic Client

$ coffee basic-server.coffeeListening to localhost:9001Received connection from 127.0.0.1Received connection from 127.0.0.1[...]

讨论

函数将为每个客户端新连接的新插口传递给@net.createServer@ 。基本的服务器与访客只进行简单地交互,但是复杂的服务器会将插口连上一个专用的处理程序,然后返回等待下一个用户的任务。

另请参阅Basic ClientBi-Directional ServerBi-Directional Client

练习

  • 为选定的目标域和基于命令行参数或配置文件的端口添加支持。

双向客户端

问题

你想通过网络提供持续的服务,与客户保持持续的联系。

解决方案

创建一个双向TCP客户机。

在 Node.js 中

net = require 'net'domain = 'localhost'port = 9001ping = (socket, delay) ->    console.log "Pinging server"    socket.write "Ping"    nextPing = -> ping(socket, delay)    setTimeout nextPing, delayconnection = net.createConnection port, domainconnection.on 'connect', () ->    console.log "Opened connection to #{domain}:#{port}"    ping connection, 2000connection.on 'data', (data) ->    console.log "Received: #{data}"connection.on 'end', (data) ->    console.log "Connection closed"    process.exit()

使用示例

可访问Bi-Directional Server

$ coffee bi-directional-client.coffeeOpened connection to localhost:9001Pinging serverReceived: You have 0 peers on this serverPinging serverReceived: You have 0 peers on this serverPinging serverReceived: You have 1 peer on this server[...]Connection closed

讨论

这个特殊示例发起与服务器联系并在@connection.on 'connect'@处理程序中开启对话。大量的工作在一个真正的用户中,然而@connection.on 'data'@处理来自服务器的输出。@ping@函数递归是为了说明连续与服务器通信可能被真实的用户移除。

另请参阅Bi-Directional ServerBasic ClientBasic Server

练习

  • 为选定的目标域和基于命令行参数或配置文件的端口添加支持。

双向服务器

问题

你想通过网络提供持续的服务,与客户保持持续的联系。

解决方案

创建一个双向TCP服务器。

在 Node.js 中

net = require 'net'domain = 'localhost'port = 9001server = 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

使用示例

可访问Bi-Directional Client

$ coffee bi-directional-server.coffeeListening to localhost:9001New connection from 127.0.0.1127.0.0.1 sent: Ping127.0.0.1 sent: Ping127.0.0.1 sent: Ping[...]

讨论

大部分工作在@socket.on 'data'@中 ,处理所有的输入端。真正的服务器可能会将数据传给另一个函数处理并生成任何响应以便源程序处理。

练习

  • 为选定的目标域和基于命令行参数或配置文件的端口添加支持。

适配器模式

问题

想象你去国外旅行,一旦你意识到你的电源线插座与酒店房间墙上的插座不兼容时,幸运的是你记得带你的电源适配器。它将一边连接你的电源线插座另一边连接墙壁插座,允许它们之间进行通信。

同样的情况也可能会出现在代码中,当两个 ( 或更多 ) 实例 ( 类、模块等 ) 想跟对方通信,但其通信协议 ( 例如,他们所使用的语言交流 ) 不同。在这种情况下,Adapter模式更方便。它会充当翻译,从一边到另一边。

解决方案

# a fragment of 3-rd party grid componentclass 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 orderclass NullSorter    sort: (data, order) -> # do nothing; it is just a stubclass 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 dataclass RandomSorterAdapter    constructor: (@sorter) ->    sort: (data, order) ->        @sorter.sort dataagrid = new AwesomeGrid ['a','b','c','d','e','f']agrid.setCustomSorter new RandomSorterAdapter(new RandomSorter)agrid.sort() # sort data with custom sorter through adapter

讨论

当你要组织两个具有不同接口的对象之间的交互时,适配器是有用的。它可以当你使用第三方库或者使用遗留代码时使用。在任何情况下小心使用适配器:它可以是有用的,但它也可以导致设计错误。

桥接模式

问题

你需要为代码保持一个可靠的接口,可以经常变化或者在多种实现间转换。

解决方案

使用桥接模式作为不同的实现和剩余代码的中间体。

假设你开发了一个浏览器的文本编辑器保存到云。然而,现在你需要通过独立客户端的端口将其在本地保存。

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://localhostelse if root?    new FileSaver filename, directory: './' # => Saved "Example data" to temp.txt in ./saver.save data

讨论

桥接模式可以帮助你将特定实现的代码置于看不见的地方,这样你就可以专注于你的程序中的具体代码。在上面的示例中,应用程序的其余部分可以称为saver.save data,不考虑文件的最终结束。

生成器模式

问题

你需要准备一个复杂的、多部分的对象,你希望操作不止一次或有不同的配置。

解决方案

创建一个生成器封装对象的产生过程。

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用户喜欢在任务描述中插入项目和上下文的标签。添加代码来识别这些标签和过滤器的结束标记。

命令模式

问题

你需要让另一个对象处理你自己的可执行的代码。

解决方案

使用Command pattern传递函数的引用。

# Using a private variable to simulate external scripts or modulesincrementers = (() ->    privateVar = 0    singleIncrementer = () ->        privateVar += 1    doubleIncrementer = () ->        privateVar += 2    commands =         single: singleIncrementer        double: doubleIncrementer        value: -> privateVar)()class RunsAll    constructor: (@commands...) ->    run: -> command() for command in @commandsrunner = 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!
"jqxhr.error -> logMessages += "Error!
"jqxhr.complete -> logMessages += "Completed!
"# On a valid AJAX request:# logMessages == "Success!
Completed!
"

修饰模式

问题

你有一组数据,需要在多个过程、可能变换的方式下处理。

解决方案

使用修饰模式来构造如何更改应用。

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 commentsclass 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("
")).join("
")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>
<p>A regular line</p>

<h2>A level 2 header</h2>
<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    lineprocessor.processors.unshift smilieExpanderprocessor.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>"

工厂方法模式

问题

直到开始运行你才知道需要的是什么种类的对象。

解决方案

使用工厂方法(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 JSONParserfactory = new ParserFactoryfactory.makeParser("example.html").type # => "HTML parser"factory.makeParser("example.md").type # => "Markdown parser"factory.makeParser("example.json").type # => "JSON parser"

讨论

在这个示例中,你可以关注解析的内容,忽略细节文件的格式。更先进的工厂方法,例如,搜索版本控制文件中的数据本身,然后返回一个更精确的解析器(例如,返回一个HTML5解析器而不是HTML v4解析器)。

解释器模式

问题

其他人需要以控制方式运行你的一部分代码。相对地,你选择的语言不能以一种简洁的方式表达问题域。

解决方案

使用解释器模式来创建一个你翻译为特定代码的领域特异性语言(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 resultcalc = new StackCalculatorcalc.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'    swwheat = (sw) ->    sw.bread = 'wheat'    swturkey = (sw) ->    sw.toppings.push 'turkey'    swham = (sw) ->    sw.toppings.push 'ham'    swswiss = (sw) ->    sw.toppings.push 'swiss'    swmayo = (sw) ->    sw.toppings.push 'mayo'    swtoasted = (sw) ->    sw.toasted = true    swsandwich = (customer) ->    new Sandwich customerto = (customer) ->    customersend = (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语言技能和你现有的语言技能可以帮助你关于捕捉代码的问题。

备忘录模式

问题

你想预测对一个对象做出改变后的反应。

解决方案

使用备忘录模式(Memento Pattern)来跟踪一个对象的变化。使用这个模式的类会输出一个存储在其他地方的备忘录对象。

如果你的应用程序可以让用户编辑文本文件,例如,他们可能想要撤销上一个动作。你可以在用户改变文件之前保存文件现有的状态,然后回滚到上一个位置。

class PreserveableText    class Memento        constructor: (@text) ->    constructor: (@text) ->    save: (newText) ->        memento = new Memento @text        @text = newText        memento    restore: (memento) ->        @text = memento.textpt = 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 mementopt.text # => "The original string"

讨论

备忘录对象由PreserveableText#save返回,为了安全保护,分别地存储着重要的状态信息。你可以序列化备忘录以便来保证硬盘中的“撤销”缓冲或者是那些被编辑的图片等数据密集型对象。

观察者模式

问题

当一个事件发生时你不得不向一些对象发布公告。

解决方案

使用观察者模式(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 "+itemclass NewspaperSubscriber    onNewNewspaper: (item) ->        alert "I've got new "+itempostOffice = new PostOffice()sub1 = new MagazineSubscriber()sub2 = new NewspaperSubscriber()postOffice.subscribe "Mens Health", sub1.onNewMagazinepostOffice.subscribe "Times", sub2.onNewNewspaperpostOffice.notifyNewItemReleased "Times"postOffice.notifyNewItemReleased "Mens Health"

讨论

这里你有一个观察者对象(PostOffice)和可观察对象(MagazineSubscriber, NewspaperSubscriber)。为了通报发布新的周期性可观察对象的事件,应该对 PostOffice进行订阅。每一个被订阅的对象都存储在PostOffice的内部订阅数组中。当新的实体周期发布时每一个订阅者都会收到通知。

单件模式

问题

许多时候你想要一个,并且只要一个类的实例。比如,你可能需要一个创建服务器资源的类,并且你想要保证使用一个对象就可以控制这些资源。但是使用时要小心,因为单件模式可以很容易被滥用来模拟不必要的全局变量。

解决方案

公有类只包含获得一个实例的方法。实例被保存在该公共对象的闭包中,并且总是有返回值。

这很奏效因为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 # => undefineda.instance # => undefinedSingleton.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: -> @messageclass 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 moduleroot.Singleton = Singleton

我们可以注意到coffeescript是如此简单地实现这个设计模式。为了更好地参考和讨论JavaScript的实现,请看初学者必备 JavaScript 设计模式

策略模式

问题

解决问题的方式有多种,但是你需要在程序运行时选择(或是转换)这些方法。

解决方案

在策略对象(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()    listreverseBubbleSort = (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 bubbleSortunsortedList = ['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 = reverseBubbleSortsorter.sort unsortedList# => ['a', 'b', 'c', 'd', 'e', 'w', 'x']

讨论

“没有作战计划在第一次接触敌人时便能存活下来。” 用户如是,但是我们可以运用从变化的情况中获得的知识来做出适应改变。在示例末尾,例如,数组中的最新项是乱序排列的,知道了这个细节,我们便可以通过切换算法来加速排序,只要简单地重赋值就可以了。

练习

  • 将StringSorter扩展为AlwaysSortedArray类来实现规则序列的所有功能,但是要基于插入方法自动分类新的项(例如push对比shift)。

模板方法模式

问题

定义一个算法的结构,作为一系列的高层次的步骤,使每一个步骤的行为可以指定,使属于一个族的算法都具有相同的结构但是有不同的行为。

解决方案

使用模板方法(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方法的问题。

MongoDB

问题

你需要与一个MongoDB数据库连接的接口。

解决方案

对于 Node.js

安装

保存记录

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 neededexampleSave = (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 模块可以帮助这样的异步调用。

SQLite

问题

你需要Node.js内部与SQLite数据库连接的接口。

解决方案

使用SQLite 模块

sqlite = require 'sqlite'db = new sqlite.Database# The module uses asynchronous methods,# so we chain the calls the db.executeexampleCreate = ->    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 diskdb.open ":memory:", (openErr) ->    throw openErr if openErr    exampleCreate()

讨论

你也可以提前准备你的SQL查询语句。

sqlite = require 'sqlite'async = require 'async' # Not required but added to make the example more concisedb = new sqlite.DatabasecreateSQL = "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模块文档提供了更完整的信息。

使用 Jasmine 测试

问题

假如你正在使用CoffeeScript写一个简单地计算器,并且想要验证其功能是否与预期一致。可以使用Jasmine测试框架。

讨论

在使用Jasmine测试框架时,你要在一个参数(spec)文档中写测试,文档描述的是代码需要测试的预期功能。

例如,我们希望计算器可以实现加法和减法的功能,并且可以正确进行正数和负数的运算。我们的spec文档如下列所示。

# calculatorSpec.coffeedescribe '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="https://atts.51coolma.cn/attachments/image/wk/coffeescript/jquery.min.js"></script> 9   <script src="https://atts.51coolma.cn/attachments/image/wk/coffeescript/jasmine.js"></script>10   <script src="https://atts.51coolma.cn/attachments/image/wk/coffeescript/jasmine-html.js"></script>11   <script src="https://atts.51coolma.cn/attachments/image/wk/coffeescript/jasmine-jquery-1.3.1.js"></script>12 13   <!-- include source files here... -->14   <script src="https://atts.51coolma.cn/attachments/image/wk/coffeescript/calculator.js"></script>15 16   <!-- include spec files here... -->17   <script src="https://atts.51coolma.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个失败情况(如下)。

Alt text

看来我们的测试是失败的,因为jasmine无法找到Calculator变量。那是因为它还没有被创建。现在让我们来创建一个新文件命名为js/calculator.coffee。

# calculator.coffeewindow.Calculator = class Calculator

编译calculator.coffee并刷新浏览器来重新运行测试组。

Alt text

现在我们还有4个失败而不是原来的8个了,只用一行代码便做出了50%的改进。

测试通过

实现我们的方法来看是否可以通过测试。

# calculator.coffeewindow.Calculator = class Calculator    add: (a, b) ->        a + b    subtract: (a, b) ->        a - b

当我们刷新页面时可以看到全部通过。

Alt text

重构测试

既然测试全部通过了,我们应看一看我们的代码或测试是否可以被重构。

在我们的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然后刷新浏览器,可以看到测试仍然全部通过。

Alt text

使用 Nodeunit 测试

问题

假如你正在使用CoffeeScript并且想要验证功能是否与预期一致,便可以决定使用Nodeunit测试框架。

讨论

Nodeunit是一种JavaScript对于单元测试库( Unit Testing libraries )中xUnit族的实现,Java, Python, Ruby, Smalltalk中均可以使用。

当使用xUnit族测试框架时,你需要将所需测试的描述预期功能的代码写在一个文件中。

例如,我们希望我们的计算器可以进行加法和减法,并且对于正负数均可以正确计算,我们的测试如下。

# test/calculator.test.coffeeCalculator = 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 testnpm 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.coffeeclass Calculatormodule.exports = Calculator

并且重新运行测试套件。

suki@Yuzuki:nodeunit_testing (master)$ npm testnpm 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 testcalculator.test✖ CalculatorTest - test can add two positive numbersTypeError: Object #<Calculator> has no method 'add'  ...✖ CalculatorTest - test can handle negative number additionTypeError: Object #<Calculator> has no method 'add'  ...✖ CalculatorTest - test can subtract two positive numbersTypeError: Object #<Calculator> has no method 'subtract'  ...✖ CalculatorTest - test can handle negative number subtractionTypeError: 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.coffeeclass Calculator  add: (a, b) ->    a + b  subtract: (a, b) ->    a - bmodule.exports = Calculator

当我们重新运行测试时可以看到全部通过:

suki@Yuzuki:nodeunit_testing (master)$ npm testnpm 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 testcalculator.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 subtractionOK: 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()

我们可以重新运行测试,仍然可以全部通过。

不使用 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文件来保存页面逻辑。此文件中的代码创建了一个函数,当点击加载数据按钮时将会调用该函数。

# XMLHttpRequest.coffeeloadDataFromServer = ->  req = new XMLHttpRequest()  req.addEventListener 'readystatechange', ->    if req.readyState is 4                        # ReadyState Complete      successResultCodes = [200, 304]      if req.status in successResultCodes        data = eval '(' + req.responseText + ')'         console.log 'data message: ', data.message       else         console.log 'Error loading data...'    req.open 'GET', 'data.json', false   req.send()  loadDataButton = document.getElementById 'loadDataButton' 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对象在全局命名空间中可用。