Last active
December 15, 2015 10:58
-
-
Save iangreenleaf/5249148 to your computer and use it in GitHub Desktop.
Ruby-style mixins in JavaScript.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
test: | |
for f in test-*.coffee; do coffee "$$f"; done | |
.PHONY: test |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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