Last active
July 3, 2019 19:55
-
-
Save parasyte/9712366 to your computer and use it in GitHub Desktop.
Jay inheritance : A *really fast* implementation of JavaScript single inheritance with mixins and a little syntactic sugar to make it go down smoothly. http://blog.kodewerx.org/2014/03/melonjs-should-be-all-about-speed.html || DEPRECATED, See: https://github.com/parasyte/jay-extend
This file contains 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
/* | |
* Copyright (c) 2014, Jay Oster | |
* All rights reserved. | |
* | |
* Redistribution and use in source and binary forms, with or without | |
* modification, are permitted provided that the following conditions are met: | |
* | |
* 1. Redistributions of source code must retain the above copyright notice, | |
* this list of conditions and the following disclaimer. | |
* 2. Redistributions in binary form must reproduce the above copyright notice, | |
* this list of conditions and the following disclaimer in the documentation | |
* and/or other materials provided with the distribution. | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
* POSSIBILITY OF SUCH DAMAGE. | |
*/ | |
/** | |
* Extend a class prototype with the provided mixin descriptors. | |
* Designed as a faster replacement for John Resig's Simple Inheritance. | |
* @name extend | |
* @memberOf external:Object# | |
* @function | |
* @param {Object[]} mixins... Each mixin is a dictionary of functions, or a | |
* previously extended class whose methods will be applied to the target class | |
* prototype. | |
* @return {Object} | |
* @example | |
* var Person = Object.extend({ | |
* "init" : function(isDancing) { | |
* this.dancing = isDancing; | |
* }, | |
* "dance" : function() { | |
* return this.dancing; | |
* } | |
* }); | |
* | |
* var Ninja = Person.extend({ | |
* "init" : function() { | |
* // Call the super constructor, passing a single argument | |
* this._super(Person, "init", [ false ]); | |
* }, | |
* "dance" : function() { | |
* // Call the overridden dance() method | |
* return this._super(Person, "dance"); | |
* }, | |
* "swingSword" : function() { | |
* return true; | |
* } | |
* }); | |
* | |
* var Pirate = Person.extend(Ninja, { | |
* "init" : function() { | |
* // Call the super constructor, passing a single argument | |
* this._super(Person, "init", [ true ]); | |
* } | |
* }); | |
* | |
* var p = new Person(true); | |
* console.log(p.dance()); // => true | |
* | |
* var n = new Ninja(); | |
* console.log(n.dance()); // => false | |
* console.log(n.swingSword()); // => true | |
* | |
* var r = new Pirate(); | |
* console.log(r.dance()); // => true | |
* console.log(r.swingSword()); // => true | |
* | |
* console.log( | |
* p instanceof Person && | |
* n instanceof Ninja && | |
* n instanceof Person && | |
* r instanceof Pirate && | |
* r instanceof Person | |
* ); // => true | |
* | |
* console.log(r instanceof Ninja); // => false | |
*/ | |
(function () { | |
Object.defineProperty(Object.prototype, "extend", { | |
"value" : function () { | |
var methods = {}; | |
var mixins = Array.prototype.slice.call(arguments, 0); | |
/** | |
* The class constructor which creates the `_super` shortcut method | |
* and calls the user `init` constructor if defined. | |
* @ignore | |
*/ | |
function Class() { | |
// Call the user constructor | |
this.init.apply(this, arguments); | |
return this; | |
} | |
// Apply superClass | |
Class.prototype = Object.create(this.prototype); | |
// Apply all mixin methods to the class prototype | |
mixins.forEach(function (mixin) { | |
apply_methods(Class, methods, mixin.__methods__ || mixin); | |
}); | |
// Verify constructor exists | |
if (!("init" in Class.prototype)) { | |
throw "Object.extend: Class is missing a constructor named `init`"; | |
} | |
// Apply syntactic sugar for accessing methods on super classes | |
Object.defineProperty(Class.prototype, "_super", { | |
"value" : _super | |
}); | |
// Create a hidden property on the class itself | |
// List of methods, used for applying classes as mixins | |
Object.defineProperty(Class, "__methods__", { | |
"value" : methods | |
}); | |
return Class; | |
} | |
}); | |
/** | |
* Apply methods to the class prototype. | |
* @ignore | |
*/ | |
function apply_methods(Class, methods, descriptor) { | |
Object.keys(descriptor).forEach(function (method) { | |
methods[method] = descriptor[method]; | |
if (typeof(descriptor[method]) !== "function") { | |
throw "Object.extend: Method `" + method + "` is not a function!"; | |
} | |
Object.defineProperty(Class.prototype, method, { | |
"configurable" : true, | |
"value" : descriptor[method] | |
}); | |
}); | |
} | |
/** | |
* Special method that acts as a proxy to the super class. | |
* @name _super | |
* @ignore | |
*/ | |
function _super(superClass, method, args) { | |
return superClass.prototype[method].apply(this, args); | |
}; | |
})(); | |
/* Inheritance tests */ | |
var Person = Object.extend({ | |
"init" : function(isDancing) { | |
this.dancing = isDancing; | |
}, | |
"dance" : function() { | |
return this.dancing; | |
} | |
}); | |
var Ninja = Person.extend({ | |
"init" : function() { | |
// Call the super constructor, passing a single argument | |
this._super(Person, "init", [ false ]); | |
}, | |
"dance" : function() { | |
// Call the overridden dance() method | |
return this._super(Person, "dance"); | |
}, | |
"swingSword" : function() { | |
return true; | |
} | |
}); | |
var Pirate = Person.extend(Ninja, { | |
"init" : function() { | |
// Call the super constructor, passing a single argument | |
this._super(Person, "init", [ true ]); | |
} | |
}); | |
console.clear(); | |
var p = new Person(true); | |
console.log("Person dances, expect true:", p.dance()); | |
var n = new Ninja(); | |
console.log("Ninja dances, expect false:", n.dance()); | |
console.log("Ninja swings sword, expect true:", n.swingSword()); | |
var r = new Pirate(); | |
console.log("Pirate dances, expect true:", r.dance()); | |
console.log("Pirate swings sword, expect true:", r.swingSword()); | |
console.log( | |
"People prototype chain is accurate:", | |
p instanceof Person && | |
n instanceof Ninja && | |
n instanceof Person && | |
r instanceof Pirate && | |
r instanceof Person | |
); | |
console.log("Pirate is a Ninja, expect false:", r instanceof Ninja); | |
console.log("-----"); | |
/* Complex inheritance tests */ | |
var A = Object.extend({ | |
"init" : function () {}, | |
"foo" : function () { | |
console.log("A.foo"); | |
}, | |
"bar" : function () { | |
console.log("A.bar"); | |
} | |
}); | |
var B = A.extend({ | |
"foo" : function () { | |
console.log("B.foo"); | |
this.bar(); | |
} | |
}); | |
var C = B.extend({ | |
"bar" : function () { | |
console.log("C.bar"); | |
this._super(B, "bar"); | |
} | |
}); | |
var D = C.extend({ | |
"foo" : function () { | |
console.log("D.foo"); | |
this._super(C, "foo"); | |
}, | |
"bar" : function () { | |
console.log("D.bar"); | |
this._super(C, "bar"); | |
} | |
}); | |
console.log("Expected output:"); | |
console.log("D.foo"); | |
console.log("B.foo"); | |
console.log("D.bar"); | |
console.log("C.bar"); | |
console.log("A.bar"); | |
console.log("Actual output:"); | |
var d = new D(); | |
d.foo(); | |
console.log("-----"); | |
/* Complex inheritance tests with sibling calls */ | |
var X = Object.extend({ | |
"init" : function () {}, | |
"foo" : function () { | |
console.log("X.foo"); | |
}, | |
"bar" : function () { | |
console.log("X.bar"); | |
} | |
}); | |
var Y = X.extend({ | |
"foo" : function () { | |
console.log("Y.foo"); | |
this._super(Y, "bar"); | |
}, | |
"bar" : function () { | |
console.log("Y.bar"); | |
} | |
}); | |
var Z = Y.extend({ | |
"foo" : function () { | |
console.log("Z.foo"); | |
this._super(Y, "foo"); | |
}, | |
"bar" : function () { | |
console.log("Z.bar"); | |
} | |
}); | |
console.log("Expected output:"); | |
console.log("Z.foo"); | |
console.log("Y.foo"); | |
console.log("Y.bar"); | |
console.log("Actual output:"); | |
var z = new Z(); | |
z.foo(); | |
console.log("-----"); | |
/** 2D vector class */ | |
var Vector2d = Object.extend({ | |
"init" : function (x, y) { | |
this.set(x, y); | |
}, | |
"set" : function (x, y) { | |
this.x = x; | |
this.y = y; | |
return this; | |
}, | |
"scale" : function (v) { | |
this.x *= v.x; | |
this.y *= v.y; | |
return this; | |
} | |
}); | |
/** Rectangle class */ | |
var Rect = Object.extend({ | |
"init" : function (x, y, w, h) { | |
this.pos = new Vector2d(x, y); | |
this.resize(w, h); | |
}, | |
"resize" : function (w, h) { | |
this.w = w; | |
this.h = h; | |
return this; | |
}, | |
"move" : function (x, y) { | |
this.pos.set(x, y); | |
return this; | |
} | |
}); | |
/** Sprite class */ | |
var Sprite = Rect.extend({ | |
"init" : function (image, x, y) { | |
this._super(Rect, "init", [ x, y, image.width, image.height ]); | |
this.image = image; | |
}, | |
"draw" : function (context) { | |
context.drawImage(this.image, this.pos.x, this.pos.y, this.w, this.h); | |
return this; | |
} | |
}); | |
/** Animation sheet class */ | |
var Animation = Sprite.extend({ | |
"init" : function (image, x, y, w, h, frames, delay) { | |
this._super(Sprite, "init", [ image, x, y ]); | |
this.resize(w, h); | |
this.frames = frames; | |
this.delay = delay; | |
this.index = 0; | |
this.lastDelta = 0; | |
}, | |
"update" : function (dt) { | |
this.lastDelta += dt; | |
if (this.lastDelta >= this.delay) { | |
this.lastDelta = 0; | |
this.index++; | |
if (this.index >= this.frames.length) { | |
this.index = 0; | |
} | |
} | |
return this; | |
}, | |
"draw" : function (context) { | |
var frame = this.frames[this.index]; | |
context.drawImage( | |
this.image, | |
this.w * frame, this.h * frame, // sx,sy | |
this.w, this.h, // sw,sh | |
this.pos.x, this.pos.y, // dx,dy | |
this.w, this.h // dw,dh | |
); | |
return this; | |
} | |
}); | |
/** Animation atlas class */ | |
var AnimationAtlas = Sprite.extend(Animation, { | |
"init" : function (image, x, y, frames, delay) { | |
this._super(Sprite, "init", [ image, x, y ]); | |
this.frames = frames; | |
this.delay = delay; | |
this.index = 0; | |
this.lastDelta = 0; | |
}, | |
"draw" : function (context) { | |
var frame = this.frames[this.index]; | |
context.drawImage( | |
this.image, | |
frame.x, frame.y, // sx,sy | |
frame.w, frame.h, // sw,sh | |
this.pos.x, this.pos.y, // dx,dy | |
frame.w, frame.h // dw,dh | |
); | |
return this; | |
} | |
}); | |
/* Correctness tests */ | |
try { | |
console.log("Missing constructor must throw exception:"); | |
var MissingConstructor = Object.extend({}); | |
console.error("Did not throw"); | |
} | |
catch (e) { | |
console.log("Success!", e); | |
} | |
try { | |
console.log("Non-methods must throw exception:"); | |
var BadMethod = Object.extend({ | |
"init" : function () {}, | |
"bad" : 0 | |
}); | |
console.error("Did not throw"); | |
} | |
catch (e) { | |
console.log("Success!", e); | |
} | |
console.log("-----"); | |
var img = document.createElement("img"); | |
img.src = "http://www.google.com/images/srpr/logo11w.png"; | |
img.onload = function () { | |
/* Deep prototype chain test */ | |
var a = new Animation(img, 20, 20, 50, 50, [ 0, 1, 2 ], 1000); | |
console.log( | |
"Animation prototype chain is accurate:", | |
a instanceof Animation && | |
a instanceof Sprite && | |
a instanceof Rect | |
); | |
console.log("-----"); | |
/* Mixin test */ | |
var b = new AnimationAtlas(img, 20, 20, [ | |
{ "x" : 0, "y" : 0, "w" : 50, "h" : 50 }, | |
{ "x" : 50, "y" : 0, "w" : 50, "h" : 50 }, | |
{ "x" : 100, "y" : 0, "w" : 50, "h" : 50 } | |
], 1000); | |
console.log( | |
"AnimationAtlas updates to index 1:", | |
b.update(1000).index | |
); | |
console.log( | |
"AnimationAtlas prototype chain is accurate:", | |
b instanceof AnimationAtlas && | |
b instanceof Sprite && | |
b instanceof Rect | |
); | |
console.log( | |
"AnimationAtlas is an instance of Animation, expect false:", | |
b instanceof Animation | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This gist is deprecated in favor of the full git repo: https://github.com/parasyte/jay-extend