1
0
Fork 0
easejs/lib/class.js

508 lines
14 KiB
JavaScript
Raw Normal View History

/**
* Contains basic inheritance mechanism
*
* Copyright (C) 2010, 2011, 2012, 2013 Mike Gerwitz
*
* This file is part of GNU ease.js.
*
* ease.js is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Mike Gerwitz
*/
var util = require( __dirname + '/util' ),
ClassBuilder = require( __dirname + '/ClassBuilder' ),
warn = require( __dirname + '/warn' ),
Warning = warn.Warning,
MethodWrapperFactory = require( __dirname + '/MethodWrapperFactory' ),
wrappers = require( __dirname + '/MethodWrappers' ).standard,
class_builder = ClassBuilder(
require( __dirname + '/MemberBuilder' )(
MethodWrapperFactory( wrappers.wrapNew ),
MethodWrapperFactory( wrappers.wrapOverride ),
Added `proxy' keyword support The concept of proxy methods will become an important, core concept in ease.js that will provide strong benefits for creating decorators and proxies, removing boilerplate code and providing useful metadata to the system. Consider the following example: Class( 'Foo', { // ... 'public performOperation': function( bar ) { this._doSomethingWith( bar ); return this; }, } ); Class( 'FooDecorator', { 'private _foo': null, // ... 'public performOperation': function( bar ) { return this._foo.performOperation( bar ); }, } ); In the above example, `FooDecorator` is a decorator for `Foo`. Assume that the `getValueOf()` method is undecorated and simply needs to be proxied to its component --- an instance of `Foo`. (It is not uncommon that a decorator, proxy, or related class will alter certain functionality while leaving much of it unchanged.) In order to do so, we can use this generic, boilerplate code return this.obj.func.apply( this.obj, arguments ); which would need to be repeated again and again for *each method that needs to be proxied*. We also have another problem --- `Foo.getValueOf()` returns *itself*, which `FooDecorator` *also* returns. This breaks encapsulation, so we instead need to return ourself: 'public performOperation': function( bar ) { this._foo.performOperation( bar ); return this; }, Our boilerplate code then becomes: var ret = this.obj.func.apply( this.obj, arguments ); return ( ret === this.obj ) ? this : ret; Alternatively, we could use the `proxy' keyword: Class( 'FooDecorator2', { 'private _foo': null, // ... 'public proxy performOperation': '_foo', } ); `FooDecorator2.getValueOf()` and `FooDecorator.getValueOf()` both perform the exact same task --- proxy the entire call to another object and return its result, unless the result is the component, in which case the decorator itself is returned. Proxies, as of this commit, accomplish the following: - All arguments are forwarded to the destination - The return value is forwarded to the caller - If the destination returns a reference to itself, it will be replaced with a reference to the caller's context (`this`). - If the call is expected to fail, either because the destination is not an object or because the requested method is not a function, a useful error will be immediately thrown (rather than the potentially cryptic one that would otherwise result, requiring analysis of the stack trace). N.B. As of this commit, static proxies do not yet function properly.
2012-05-02 13:26:47 -04:00
MethodWrapperFactory( wrappers.wrapProxy ),
require( __dirname + '/MemberBuilderValidator' )(
function( warning )
{
warn.handle( Warning( warning ) );
}
)
),
require( __dirname + '/VisibilityObjectFactoryFactory' )
.fromEnvironment()
)
;
/**
* This module may be invoked in order to provide a more natural looking class
* definition mechanism
*
* This may not be used to extend existing classes. To extend an existing class,
* use the class's extend() method. If unavailable (or extending a non-ease.js
* class/object), use the module's extend() method.
*
* @param {string|Object} namedef optional name or definition
* @param {Object=} def class definition if first argument is name
*
* @return {Function|Object} new class or staging object
*/
module.exports = function( namedef, def )
{
var type = ( typeof namedef ),
result = null
2011-03-04 00:24:42 -05:00
;
2011-03-04 00:24:42 -05:00
switch ( type )
{
2011-03-04 00:24:42 -05:00
// anonymous class
case 'object':
result = createAnonymousClass.apply( null, arguments );
2011-03-04 00:24:42 -05:00
break;
2011-03-04 00:24:42 -05:00
// named class
case 'string':
result = createNamedClass.apply( null, arguments );
2011-03-04 00:24:42 -05:00
break;
default:
// we don't know what to do!
throw TypeError(
"Expecting anonymous class definition or named class definition"
);
}
return result;
};
/**
* Creates a class, inheriting either from the provided base class or the
* default base class
*
* @param {Function|Object} baseordfn parent or definition object
* @param {Object=} dfn definition object if parent provided
*
* @return {Function} extended class
*/
module.exports.extend = function( baseordfn, dfn )
{
return extend.apply( this, arguments );
};
/**
* Implements an interface or set of interfaces
*
* @param {...Function} interfaces interfaces to implement
*
* @return {Object} intermediate interface object
*/
module.exports.implement = function( interfaces )
{
// implement on empty base
return createImplement(
null,
Array.prototype.slice.call( arguments )
);
};
/**
* Determines whether the provided object is a class created through ease.js
*
* @param {Object} obj object to test
*
* @return {boolean} true if class (created through ease.js), otherwise false
*/
module.exports.isClass = function( obj )
{
obj = obj || {};
return ( obj.prototype instanceof ClassBuilder.ClassBase )
? true
: false
;
};
/**
* Determines whether the provided object is an instance of a class created
* through ease.js
*
* @param {Object} obj object to test
*
* @return {boolean} true if instance of class (created through ease.js),
* otherwise false
*/
module.exports.isClassInstance = function( obj )
{
obj = obj || {};
return ( obj instanceof ClassBuilder.ClassBase )
? true
: false;
};
/**
* Determines if the class is an instance of the given type
*
* The given type can be a class, interface, trait or any other type of object.
* It may be used in place of the 'instanceof' operator and contains additional
* enhancements that the operator is unable to provide due to prototypal
* restrictions.
*
* @param {Object} type expected type
* @param {Object} instance instance to check
*
* @return {boolean} true if instance is an instance of type, otherwise false
*/
module.exports.isInstanceOf = ClassBuilder.isInstanceOf;
/**
* Alias for isInstanceOf()
*
* May read better in certain situations (e.g. Cat.isA( Mammal )) and more
* accurately conveys the act of inheritance, implementing interfaces and
* traits, etc.
*/
module.exports.isA = module.exports.isInstanceOf;
/**
* Creates a new anonymous Class from the given class definition
*
* @param {Object} def class definition
*
* @return {Function} new anonymous class
*/
function createAnonymousClass( def )
{
// ensure we have the proper number of arguments (if they passed in
// too many, it may signify that they don't know what they're doing,
// and likely they're not getting the result they're looking for)
if ( arguments.length > 1 )
{
throw Error(
"Expecting one argument for anonymous Class definition; " +
arguments.length + " given."
);
}
return extend( def );
}
/**
* Creates a new named Class from the given class definition
*
* @param {string} name class name
* @param {Object} def class definition
*
* @return {Function|Object} new named class or staging object if definition
* was not provided
*/
function createNamedClass( name, def )
{
// if too many arguments were provided, it's likely that they're
// expecting some result that they're not going to get
if ( arguments.length > 2 )
{
throw Error(
"Expecting at most two arguments for definition of named Class '" +
name + "'; " + arguments.length + " given."
);
}
// if no definition was given, return a staging object, to apply the name to
// the class once it is actually created
if ( def === undefined )
{
return createStaging( name );
}
// the definition must be an object
else if ( typeof def !== 'object' )
{
throw TypeError(
"Unexpected value for definition of named Class '" + name +
"'; object expected"
);
}
// add the name to the definition
def.__name = name;
return extend( def );
}
2010-11-14 22:07:04 -05:00
/**
* Creates a staging object to stage a class name
*
* The class name will be applied to the class generated by operations performed
* on the staging object. This allows applying names to classes that need to be
* extended or need to implement interfaces.
*
* @param {string} cname desired class name
*
* @return {Object} object staging the given class name
*/
function createStaging( cname )
{
return {
extend: function()
{
var args = Array.prototype.slice.apply( arguments );
// extend() takes a maximum of two arguments. If only one
// argument is provided, then it is to be the class definition.
// Otherwise, the first argument is the supertype and the second
// argument is the class definition. Either way you look at it,
// the class definition is always the final argument.
//
// We want to add the name to the definition.
args[ args.length - 1 ].__name = cname;
return extend.apply( null, args );
},
implement: function()
{
// implement on empty base, providing the class name to be used once
// extended
return createImplement(
null,
Array.prototype.slice.call( arguments ),
cname
);
},
};
}
/**
* Creates an intermediate object to permit implementing interfaces
*
* This object defers processing until extend() is called. This intermediate
* object ensures that a usable class is not generated until after extend() is
* called, as it does not make sense to create a class without any
* body/definition.
*
* @param {Object} base base class to implement atop of, or null
* @param {Array} ifaces interfaces to implement
* @param {string=} cname optional class name once extended
*
* @return {Object} intermediate implementation object
*/
function createImplement( base, ifaces, cname )
{
// Defer processing until after extend(). This also ensures that implement()
// returns nothing usable.
return {
extend: function()
{
var args = Array.prototype.slice.call( arguments ),
def = args.pop(),
ext_base = args.pop()
;
// if any arguments remain, then they likely misunderstood what this
// method does
if ( args.length > 0 )
{
throw Error(
"Expecting no more than two arguments for extend()"
);
}
// if a base was already provided for extending, don't allow them to
// give us yet another one (doesn't make sense)
if ( base && ext_base )
{
throw Error(
"Cannot override parent " + base.toString() + " with " +
ext_base.toString() + " via extend()"
);
}
// if a name was provided, use it
if ( cname )
{
def.__name = cname;
}
// If a base was provided when createImplement() was called, use
// that. Otherwise, use the extend() base passed to this function.
// If neither of those are available, extend from an empty class.
ifaces.push( base || ext_base || extend( {} ) );
return extend.call( null,
implement.apply( this, ifaces ),
def
);
},
};
}
2010-11-14 22:07:04 -05:00
/**
* Mimics class inheritance
*
* This method will mimic inheritance by setting up the prototype with the
* provided base class (or, by default, Class) and copying the additional
* properties atop of it.
*
* The class to inherit from (the first argument) is optional. If omitted, the
* first argument will be considered to be the properties list.
*
* @param {Function|Object} _ parent or definition object
* @param {Object=} __ definition object if parent was provided
*
* @return {Function} extended class
*/
function extend( _, __ )
{
// set up the new class
var new_class = class_builder.build.apply( class_builder, arguments );
2011-03-29 00:15:16 -04:00
// set up some additional convenience props
setupProps( new_class );
// lock down the new class (if supported) to ensure that we can't add
// members at runtime
util.freeze( new_class );
return new_class;
}
/**
* Implements interface(s) into an object
*
2011-01-10 19:56:09 -05:00
* This will copy all of the abstract methods from the interface and merge it
* into the given object.
*
* @param {Object} baseobj base object
* @param {...Function} interfaces interfaces to implement into dest
*
* @return {Object} destination object with interfaces implemented
*/
var implement = function( baseobj, interfaces )
{
var args = Array.prototype.slice.call( arguments ),
2011-01-10 19:56:09 -05:00
dest = {},
base = args.pop(),
len = args.length,
arg = null,
implemented = [],
make_abstract = false
;
// add each of the interfaces
for ( var i = 0; i < len; i++ )
{
arg = args[ i ];
// copy all interface methods to the class (does not yet deep copy)
2011-01-24 23:38:27 -05:00
util.propParse( arg.prototype, {
method: function( name, func, is_abstract, keywords )
{
dest[ 'abstract ' + name ] = func.definition;
make_abstract = true;
2011-01-24 23:38:27 -05:00
},
} );
implemented.push( arg );
}
// xxx: temporary
if ( make_abstract )
{
dest.___$$abstract$$ = true;
}
2011-01-10 19:56:09 -05:00
// create a new class with the implemented abstract methods
var class_new = module.exports.extend( base, dest );
ClassBuilder.getMeta( class_new ).implemented = implemented;
return class_new;
}
/**
* Sets up common properties for the provided function (class)
*
2011-03-29 00:15:16 -04:00
* @param {function()} func function (class) to set up
*
* @return {undefined}
*/
2011-03-29 00:15:16 -04:00
function setupProps( func )
{
2010-12-28 22:08:30 -05:00
attachExtend( func );
2011-01-10 19:56:09 -05:00
attachImplement( func );
}
/**
* Attaches extend method to the given function (class)
*
* @param {Function} func function (class) to attach method to
*
* @return {undefined}
*/
2010-12-28 22:08:30 -05:00
function attachExtend( func )
{
/**
* Shorthand for extending classes
*
2010-12-01 23:19:59 -05:00
* This method can be invoked on the object, rather than having to call
* Class.extend( this ).
*
* @param {Object} props properties to add to extended class
*
* @return {Object} extended class
*/
util.defineSecureProp( func, 'extend', function( props )
2010-11-10 23:28:20 -05:00
{
return extend( this, props );
2010-11-14 22:07:04 -05:00
});
}
2011-01-10 19:56:09 -05:00
/**
* Attaches implement method to the given function (class)
*
* Please see the implement() export of this module for more information.
*
2011-01-10 19:56:09 -05:00
* @param {function()} func function (class) to attach method to
*
* @return {undefined}
*/
function attachImplement( func )
{
util.defineSecureProp( func, 'implement', function()
{
return createImplement(
func,
Array.prototype.slice.call( arguments )
);
2011-01-10 19:56:09 -05:00
});
}