CoffeeScript袖珍手册

类在javascript中发挥的功效很大,就像对付吸血鬼的大蒜头一样,不过话说回来,如果你有这种想法,你很可能不会去看一本Coffeescript的书。然而,类在javascript无非只是表现的和它在别的语言里面一样的能力,而在这方面Coffeescript提供了更好的抽象.

CoffeeScript使用了Javascript原生的原形来创建类,并且添加了一些静态属性和保持作用域上的语法糖。而这一切都通过 class 关键字提供给开发者.

class Animal

在上面的例子中, Animal 是类名,我们可以用这个类目来创建变量.CoffeeScript支持构造函数,这意味着你可以通过new操作符来生成实例.

animal = new Animal

定义构造函数非常简单,只需定义一个 constructor 函数.这就像我们使用Ruby的 initialize 或者 Python 的 __init__.

class Animal
  constructor: (name) ->
    @name = name

事实上,CoffeeScript提供了一种设定实例属性的缩写方式。只需要在参数前加上@, CoffeeScript会自动的在构造函数中设定实例的属性,事实上,这个缩写方式也适用于普通的类之外的函数。下面的这个例子和我们之前手动设定实例属性的例子是等价的.

class Animal
  constructor: (@name) ->

正如你期望的,任何在初始化过程中传入的参数都会被传入构造函数.

animal = new Animal("Parrot")
alert "Animal is a #{animal.name}"

实例属性

给一个类添加额外的实例方法非常的简单. 它与对一个对象添加属性的语法一样. 只是把方法添加在class之上.

class Animal
  price: 5

  sell: (customer) ->

animal = new Animal
animal.sell(new Customer)

作用域的改变在JavaScript中非常普遍,在之前的语法章节,我们谈及了CoffeeScript可以通过 => 锁定 this 的值到一个固定的执行上下文上面。这就确保了不论函数在什么作用域下运行,它总会在它创建时的执行上下文下执行.

class Animal
  price: 5

  sell: =>
    alert "Give me #{@price} shillings!"

animal = new Animal
$("#sell").click(animal.sell)

上面的例子已经说明,this在事件回调中非常有用。通常来说, sell()函数只会在 #sell 的元素的作用域下面执行。不过通过对sell()函数使用=> 符,我们可以保证它的作用域一直不变。并且 this.price 始终等于 5.

静态属性

那么如何定义类的方法(静态方法)呢?很简单,在一个类的定义体中,this指向这个类对象.换句话说,你可以直接在this上面设置静态属性(类属性).

class Animal
  this.find = (name) ->      

Animal.find("Parrot")

你可能还记得,CoffeeScript通过@符来引用 this,这样你能更简洁的编写静态方法:

class Animal
  @find: (name) ->

Animal.find("Parrot")

继承和超类

如果没有继承的机制,类的存在也就没有真正的意义, CoffeeScript自然也提供了这方面的语法.你可以使用 extends关键词从一个类继承自另一个类. 在下面的这个例子中, Parrot 类就继承自 Animal 类, 包括了所有的实例方法, 例如 alive() 方法。

class Animal
  constructor: (@name) ->

  alive: ->
    false

class Parrot extends Animal
  constructor: ->
    super("Parrot")

  dead: ->
    not @alive()

从上一个例子中你可以看到,我们还使用了 super() 关键字. 如此做, this就指向了当前类“父类”的原型, 并且使用当前的作用域来执行. 在对应的js中, 就会生成 Parrot.__super__.constructor.call(this, "Parrot"); 这样的一段申明. 实际运用中, 这样就好比是在 Ruby 或者 Python中使用 super, 执行被继承的函数.

通常来说当实例被创建的时候CoffeeScript会执行父类的 constructor 构造函数,除非你自己修改了构造函数.

CoffeeScript使用原形继承来继承一个类的所有实例方法.这保证了所有的类都是动态的; 即便是一个子类被创建后,在父类添加了实例方法,继承于它的子类依然能够使用这个方法.

class Animal
  constructor: (@name) ->

class Parrot extends Animal

Animal::rip = true

parrot = new Parrot("Macaw")
alert("This parrot is no more") if parrot.rip

值得指出的是静态的属性都被拷贝到子类中, 而不是像实例属性那样通过原形来继承. 这主要源于Javascript原型架构的原因.

混合(Mixins)

混合(Mixins) 并非CoffeeScript原生支持的特性, 但基于他们的好处,你可以自己写一个. 举例来说,混合包含两个函数, extend()include() 分别可以向一个类中添加类属性和实例属性.

extend = (obj, mixin) ->
  obj[name] = method for name, method of mixin        
  obj

include = (klass, mixin) ->
  extend klass.prototype, mixin

# Usage
include Parrot,
  isDeceased: true

(new Parrot).isDeceased

当继承的方式并不适用,但又需要在模块之间共享一些常用的逻辑的时候,混合就是很好的方式。混合的优势在于, 相比于继承的来源只有一个父类,你可以对自己的类加入很多的不同的特性或属性。

扩展类

混合(Mixin)非常的简洁优雅, 但是他们的写法并没有面向对象. 我们就把Mixin整合进CoffeeScript的类之中. 我们先定义一个叫 Module 的类,通过这个类我们可以继承Mixin的特性. Module 拥有两个静态的方法, @extend()@include(), 通过这两个方法,我们可以分别扩展一个类的静态和实例属性.

moduleKeywords = ['extended', 'included']

class Module
  @extend: (obj) ->
    for key, value of obj when key not in moduleKeywords
      @[key] = value

    obj.extended?.apply(@)
    this

  @include: (obj) ->
    for key, value of obj when key not in moduleKeywords
      # Assign properties to the prototype
      @::[key] = value

    obj.included?.apply(@)
    this

moduleKeywords变量所做的工作是确保我们在mixin继承了一个类之后能支持回调. 让我们看看 Module 类如何实际的使用:

classProperties = 
  find: (id) ->
  create: (attrs) ->

instanceProperties =
  save: -> 

class User extends Module
  @extend classProperties
  @include instanceProperties

# Usage:
user = User.find(1)

user = new User
user.save()

你可以看到, 我们添加了静态方法 find()create()User 这个类, 另外添加了实例方法 save(). 因为我们有模块扩展的回调, 我们可以通过接受静态和动态的属性来简化这一过程:

ORM = 
  find: (id) ->
  create: (attrs) ->
  extended: ->
    @include
      save: -> 

class User extends Module
  @extend ORM

非常简单和高效吧!