From 56c13b757bfa2a784fc71526f05b627ad2faa3aa Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 4 Jan 2011 00:37:54 -0500 Subject: [PATCH] isInstanceOf() now works with interfaces --- lib/class.js | 72 ++++++++++++++++++++++++++++-------- test/test-class-implement.js | 18 +++++++++ test/test-class.js | 12 ++++++ 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/lib/class.js b/lib/class.js index 185f40d..cebf28d 100644 --- a/lib/class.js +++ b/lib/class.js @@ -27,10 +27,9 @@ var util = require( './util' ); /** * Stores class metadata internally (ensures data is encapsulated) * - * The data in this object is hashed on the class object itself. Therefore, to - * look up an item, you would provide the class itself as the property. + * The data in this object is hashed a class id. * - * @type {Object. }>} + * @type {Object. }>} */ var class_meta = {}; @@ -76,7 +75,7 @@ exports.implement = function() } var class_new = exports.extend( dest ); - getMeta( class_new ).implemented = implemented; + getMeta( class_new.__cid ).implemented = implemented; return class_new; }; @@ -149,8 +148,9 @@ exports.isInstanceOf = function( type, instance ) // if no metadata is available, then our remaining checks cannot be // performed - if ( !( meta = getMeta( type ) ) ) + if ( !instance.__cid || !( meta = getMeta( instance.__cid ) ) ) { + console.log( instance ); return false; } @@ -203,6 +203,8 @@ function Class() {}; */ var extend = ( function( extending ) { + var class_id = 0; + /** * Mimics class inheritance * @@ -274,18 +276,20 @@ var extend = ( function( extending ) // set up the new class var new_class = createCtor( abstract_methods ); - setupProps( new_class, abstract_methods ); attachPropInit( prototype, properties ); new_class.prototype = prototype; new_class.constructor = new_class; + // important: call after setting prototype + setupProps( new_class, abstract_methods, ++class_id ); + // lock down the new class (if supported) to ensure that we can't add // members at runtime util.freeze( new_class ); // create internal metadata for the new class - createMeta( new_class ); + createMeta( new_class, base.prototype.__cid ); // we're done with the extension process extending = false; @@ -360,13 +364,15 @@ var extend = ( function( extending ) * * @param {Function} func function (class) to set up * @param {Array.} abstract_methods list of abstract method names + * @param {number} class_id unique id to assign to class * * @return {undefined} */ -function setupProps( func, abstract_methods ) +function setupProps( func, abstract_methods, class_id ) { attachAbstract( func, abstract_methods ); attachExtend( func ); + attachId( func, class_id ); } @@ -466,6 +472,25 @@ function attachExtend( func ) } +/** + * Attaches the unique id to the class and its prototype + * + * The unique identifier is used internally to match a class and its instances + * with the class metadata. Exposing the id breaks encapsulation to a degree, + * but is a lesser evil when compared to exposing all metadata. + * + * @param {Function} func function (class) to attach method to + * @param {number} id id to assign + * + * @return {undefined} + */ +function attachId( func, id ) +{ + util.defineSecureProp( func, '__cid', id ); + util.defineSecureProp( func.prototype, '__cid', id ); +} + + /** * Attaches partially applied isInstanceOf() method to class instance * @@ -488,15 +513,30 @@ function attachInstanceOf( instance ) /** * Initializes class metadata for the given class * - * @param {Class} obj class to initialize metadata for + * @param {Class} func class to initialize metadata for * * @return {undefined} */ -function createMeta( obj ) +function createMeta( func, parent_id ) { - class_meta[ obj ] = { - implemented: [], - }; + var id = func.__cid, + parent_meta = ( ( parent_id ) ? getMeta( parent_id) : undefined ); + + // copy the parent prototype's metadata if it exists (inherit metadata) + if ( parent_meta ) + { + // todo: deep (1) util.clone() when available + class_meta[ id ] = { + implemented: util.clone( parent_meta.implemented ), + }; + } + else + { + // create empty + class_meta[ id ] = { + implemented: [], + }; + } } @@ -506,12 +546,12 @@ function createMeta( obj ) * Since a reference is returned (rather than a copy), the returned object can * be modified to alter the metadata. * - * @param {Class} obj class to retrieve metadata for + * @param {number} id id of class to retrieve metadata for * * @return {Object} */ -function getMeta( obj ) +function getMeta( id ) { - return class_meta[ obj ]; + return class_meta[ id ]; } diff --git a/test/test-class-implement.js b/test/test-class-implement.js index 06aab33..8487619 100644 --- a/test/test-class-implement.js +++ b/test/test-class-implement.js @@ -77,3 +77,21 @@ assert.ok( "Abstract methods list contains names of implemented methods" ); + +// concrete implementation so that we can instantiate it +var ConcreteFoo = Foo.extend( + { + foo: function() {}, + foo2: function() {}, + }), + + concrete_inst = new ConcreteFoo(); + +assert.ok( + ( concrete_inst.isInstanceOf( Type ) + && concrete_inst.isInstanceOf( Type2 ) + ), + "Instances of classes implementing interfaces are considered to be " + + "instances of the implemented interfaces" +); + diff --git a/test/test-class.js b/test/test-class.js index bffd6da..5c76b4f 100644 --- a/test/test-class.js +++ b/test/test-class.js @@ -132,3 +132,15 @@ assert.equal( "Class instance contains isA() alias for isInstanceOf() partially applied function" ); + + +assert.ok( + ( Foo.__cid !== undefined ), + "Class id available via class" +); + +assert.ok( + ( Foo.prototype.__cid !== undefined ), + "Class id available via class prototype" +); +