Skip to content

Instantly share code, notes, and snippets.

@parasyte
Last active July 3, 2019 19:55
Show Gist options
  • Save parasyte/9712366 to your computer and use it in GitHub Desktop.
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
/*
* 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
);
};
@parasyte
Copy link
Author

parasyte commented Jul 3, 2019

This gist is deprecated in favor of the full git repo: https://github.com/parasyte/jay-extend

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment