From 9f401d2fec2ca53802169393c549343cb10e423b Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Sat, 26 Apr 2014 10:00:01 -0400 Subject: [PATCH] Class#asPrototype support This is an interop feature: allows using ease.js classes as part of a prototype chain. --- lib/ClassBuilder.js | 12 +++++ test/Class/InteropTest.js | 94 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index 343f513..69c426b 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -299,6 +299,8 @@ function _instChk( type, instance ) */ exports.prototype.build = function extend( _, __ ) { + var build = this; + // ensure we'll be permitted to instantiate abstract classes for the base this._extending = true; @@ -445,6 +447,16 @@ exports.prototype.build = function extend( _, __ ) attachAbstract( new_class, abstract_methods ); attachId( new_class, this._classId ); + // returns a new instance of the class without invoking the constructor + // (intended for use in prototype chains) + new_class.asPrototype = function() + { + build._extending = true; + var inst = new_class(); + build._extending = false; + return inst; + }; + // we're done with the extension process this._extending = false; diff --git a/test/Class/InteropTest.js b/test/Class/InteropTest.js index c7cd0be..3be7ebe 100644 --- a/test/Class/InteropTest.js +++ b/test/Class/InteropTest.js @@ -307,5 +307,99 @@ require( 'common' ).testCase( // should return itself; we should not have modified that behavior this.assertStrictEqual( inst.pub(), inst ); }, + + + /** + * When prototypally extending a class, it is not wise to invoke the + * constructor (just like ease.js does not invoke the constructor of + * subtypes until the supertype is instantiated), as the constructor may + * validate its arguments, or may even have side-effects. Expose this + * internal deferral functionality for our prototypal friends. + * + * It is incredibly unwise to use this function purely to circumvent the + * constructor, as classes will use the constructor to ensure that the + * inststance is in a consistent and expected state. + * + * This may also have its uses for stubbing/mocking. + */ + 'Can defer invoking __construct': function() + { + var expected = {}; + + var C = this.Class( + { + __construct: function() + { + throw Error( "__construct called!" ); + }, + + foo: function() { return expected; }, + } ); + + var inst; + this.assertDoesNotThrow( function() + { + inst = C.asPrototype(); + } ); + + // should have instantiated C without invoking its constructor + this.assertOk( this.Class.isA( C, inst ) ); + + // we should be able to invoke methods even though the ctor has not + // yet run + this.assertStrictEqual( expected, inst.foo() ); + }, + + + /** + * Ensure that the prototype is able to invoke the deferred constructor. + * Let's hope they actually do. This should properly bind the context to + * whatever was provided; it should not be overridden. But see the test + * case below. + */ + 'Can invoke constructor within context of prototypal subtype': + function() + { + var expected = {}; + + var C = this.Class( + { + foo: null, + __construct: function() { this.foo = expected; }, + } ); + + function SubC() { this.__construct.call( this ); } + SubC.prototype = C.asPrototype(); + + this.assertStrictEqual( + ( new SubC() ).foo, + expected + ); + }, + + + /** + * Despite being used as part of a prototype, it's important that + * ease.js' context switching between visibility objects remains active. + */ + 'Deferred constructor still has access to private context': function() + { + var expected = {}; + + var C = this.Class( + { + 'private _foo': null, + __construct: function() { this._foo = expected; }, + getFoo: function() { return this._foo }, + } ); + + function SubC() { this.__construct.call( this ); } + SubC.prototype = C.asPrototype(); + + this.assertStrictEqual( + ( new SubC() ).getFoo(), + expected + ); + }, } );