在 JavaScript 實作 Mixin / Concern

有這樣的需求:

CoffeeScript 也沒能很好解決這個問題。不過在 CoffeeScript Cookbook » Mixins for classes 這裡提了一個很有趣的做法,就是先混成兩個要繼承的 classes,再給新的 Class 繼承。缺點是 mixin 有更動的時候,不會反映在之前 include 過的 class 裡面。

土砲 Mixin

於是找到了 Addy Osmani 大師的 Learning JavaScript Design Patterns > Mixin Pattern 這篇文章,裡面是借用了 Underscore / Lo-Dash 的 _.extend 來實作的,看來非得這樣做不可了。

寫起來會像這樣:

# concern.js.coffee
Function::include = (mixin) ->
  _.extend(this.prototype, mixin)
# duck.js.coffee
this.Duck =
  quake: ->
    "quake quake"

  walk: ->
    "walks like a duck"
# yazi.js.coffee
class Yazi
  @include Duck
# kamo.js.coffee
class Kamo # "duck" in Japanese
  @include Duck
# app.js.coffee
yazi = new Yazi()
kamo = new Kamo()

yazi.quake() #=> "quake"
kamo.quake() #=> "quake"

此外也可以在新的 class 裡面覆寫 function / variable 就是了。

然而這種做法跟第一種做法一樣,mixin 有更動的時候,不會反映在之前 include 過的 class 裡面。

要注意的是,因為我們專案用的是 CoffeeScript,所以寫起來像 @include 這樣子看起來很簡潔有力,如果換成 JavaScript 的話會變得比較醜一點,以下是 CoffeScript 編出來的結果:

// kamo.js
var Kamo = (function() {
  var Kamo = function() {};

  Kamo.include(Duck);

  return Kamo;
})();

var kamo = new Kamo();
kamo.quake(); //=> "quake"

ActiveSupport::Concern 化

當然也可以弄得像 ActiveSupport::Concern,我是說 included callback:

# concern.js.coffee
Function::include = (mixin) ->
  functions = _.omit(mixin, "included", "classFunctions")

  _.extend(this.prototype, functions)
  _.extend(this, mixin.classFunctions) # insert class functions in class itself

  # Call "included" callback function if available
  if typeof mixin.included is "function"
    # call included(mixin, base)
    # this = the mixin included, base = the class which called `include`
    mixin.included.call(mixin, this)
# duck.js.coffee
this.Duck =
  quake: ->
    "quake quake"

  walk: ->
    "walks like a duck"

  included: (base) ->
    console.log "mixing this class as a duck: #{base}"

  classFunctions:
    kindOfDuck: -> true
# kamo.js.coffee
class Kamo
  @include Duck
// app.js
// Console prints "mixing this class as a duck: function Kamo() { ..."
Kamo.kindOfDuck() //=> true

var animal = new Kamo()
animal.quake() //=> "quake quake"

至於怎麼弄到可以像 Rails 4.1 的 Concerning 我還沒想出來……。