Skip to content

Instantly share code, notes, and snippets.

@iangreenleaf
Last active December 15, 2015 10:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iangreenleaf/5249148 to your computer and use it in GitHub Desktop.
Save iangreenleaf/5249148 to your computer and use it in GitHub Desktop.
Ruby-style mixins in JavaScript.
test:
for f in test-*.coffee; do coffee "$$f"; done
.PHONY: test
module.exports = class Mixin
@mixInto: (target) ->
# newProto is the mixin's link in the prototype chain.
# It points at the original prototype chain.
newProto = ->
newProto:: = target::
p = new newProto
p.__mixedSuper__ = target::
for k,v of @::
p[k] = v
# delegatorProto is the new prototype for this class.
# It points at newProto.
delegatorProto = ->
@constructor = target
return
delegatorProto:: = p
d = new delegatorProto
# Make mixedSuper available in the child object.
d.mixedSuper = (name, args) ->
p[name].apply p, args
target:: = d
# Sadly, it is impossible to correctly use CoffeeScript's `super` in
# mixins, so instead we provide @mixedSuper. It behaves much the same,
# but must be passed the name of the method and any arguments.
# @mixedSuper is available in two places: within the mixin, and on the
# child object. It should behave as expected in both.
mixedSuper: (name, args) ->
@__mixedSuper__[name].apply @, args
Mixin = require './mixin'
assert = require 'assert'
class M extends Mixin
mTrue: ->
true
class A
M.mixInto @
aTrue: ->
true
class B
M.mixInto @
mTrue: ->
false
assert (new A).mTrue(), "class should respond to mixin methods"
assert (new A).aTrue(), "class should respond to own methods"
assert.equal (new B).mTrue(), false, "class may override mixed-in methods"
assert.equal (new A).constructor, A, "should have old class constructor"
assert (new A) instanceof A, "should be instance of class"
# Sadly I don't think this is possible
#assert (new A) instanceof M, "should be instance of mixin"
Mixin = require './mixin'
assert = require 'assert'
called = []
class MNoConstructor extends Mixin
class MConstructor extends Mixin
constructor: ->
called.push "m"
class MSuperConstructor extends Mixin
constructor: ->
@mixedSuper "constructor", arguments
called.push "m"
class A
MNoConstructor.mixInto @
constructor: ->
called.push "a"
class B
constructor: ->
called.push "b"
class C extends B
MNoConstructor.mixInto @
class Q
MConstructor.mixInto @
class R extends Q
class S
MConstructor.mixInto @
constructor: ->
called.push "s"
class X
constructor: ->
called.push "x"
class Y extends X
MSuperConstructor.mixInto @
class Z extends Y
constructor: ->
@mixedSuper "constructor", arguments
called.push "z"
called = []
new A
assert.deepEqual called, ["a"], "should call constructor"
called = []
new C
assert.deepEqual called, ["b"], "should call parent constructor"
called = []
new Q
assert.deepEqual called, ["m"], "should call mixin constructor"
called = []
new R
assert.deepEqual called, ["m"], "should call parent mixin constructor"
called = []
new S
assert.deepEqual called, ["s"], "should override mixin constructor"
called = []
new Y
assert.deepEqual called, ["x", "m"], "super should work in mixed-in constructor"
called = []
new Z
assert.deepEqual called, ["x", "m", "z"], "super should call mixed-in constructor"
Mixin = require './mixin'
assert = require 'assert'
class M extends Mixin
mTrue: ->
true
class A
aTrue: ->
true
class B extends A
M.mixInto @
class C extends B
class Q
mTrue: ->
false
class R extends Q
M.mixInto @
class S extends R
class X
M.mixInto @
class Y extends X
mTrue: ->
false
class Z extends Y
assert (new B).mTrue(), "should respond to mixin method at top of chain"
assert (new C).mTrue(), "should respond to mixin method deeper in chain"
assert (new R).mTrue(), "mixin should override inherited method"
assert (new S).mTrue(), "inherited mixin should override inherited method"
assert.equal (new Y).mTrue(), false, "method should override inherited mixin"
assert.equal (new Z).mTrue(), false, "inherited method should override inherited mixin"
assert (new B).aTrue(), "non-mixin methods are passed through"
assert (new C).aTrue(), "non-mixin methods are passed through from further down chain"
Mixin = require './mixin'
assert = require 'assert'
called = []
class M extends Mixin
doCall: ->
called.push "m"
doSuperCall: ->
@mixedSuper "doSuperCall", arguments
called.push "m"
class A
doCall: ->
called.push "a"
doSuperCall: ->
called.push "a"
class B extends A
M.mixInto @
doCall: ->
@mixedSuper "doCall", arguments
called.push "b"
doSuperCall: ->
@mixedSuper "doSuperCall", arguments
called.push "b"
class C extends B
doCall: ->
called.push "c"
doSuperCall: ->
super
called.push "c"
called = []
(new A).doCall()
assert.deepEqual called, ["a"], "no mixin further up chain"
called = []
(new B).doCall()
assert.deepEqual called, ["m", "b"], "super should call mixin"
called = []
(new B).doSuperCall()
assert.deepEqual called, ["a", "m", "b"], "super from mixin should call parent"
called = []
(new C).doCall()
assert.deepEqual called, ["c"], "no mixin further down chain"
called = []
(new C).doSuperCall()
assert.deepEqual called, ["a", "m", "b", "c"], "regular super reaches mixin secondhand"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment