ES6-style constructors
Included with this change is a simple "wrapper" implementation: ``` // equivalent easejs( Foo ); Class.extend( Foo, {} ); ```master
commit
cbf98cccf1
20
index.js
20
index.js
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Provides ease of access to all submodules
|
* Provides ease of access to all submodules
|
||||||
*
|
*
|
||||||
* Copyright (C) 2010, 2011, 2013, 2014 Free Software Foundation, Inc.
|
* Copyright (C) 2010, 2011, 2013, 2014, 2015 Free Software Foundation, Inc.
|
||||||
*
|
*
|
||||||
* This file is part of GNU ease.js.
|
* This file is part of GNU ease.js.
|
||||||
*
|
*
|
||||||
|
@ -19,10 +19,26 @@
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a prototype using ease.js
|
||||||
|
*
|
||||||
|
* This function is the entry point for ease.js; its fields expose all of
|
||||||
|
* its core features. When invoked, it wraps the given prototype using
|
||||||
|
* ease.js, producing an ease.js Class. This is more natural when using the
|
||||||
|
* ECMAScript 6 `class` syntax to define prototypes.
|
||||||
|
*
|
||||||
|
* @param {Function} proto prototype to wrap
|
||||||
|
*
|
||||||
|
* @return {Function} ease.js Class wrapping PROTO
|
||||||
|
*/
|
||||||
|
var exports = module.exports = function( proto )
|
||||||
|
{
|
||||||
|
return exports.Class.extend( proto, {} );
|
||||||
|
};
|
||||||
|
|
||||||
exports.Class = require( './lib/class' );
|
exports.Class = require( './lib/class' );
|
||||||
exports.AbstractClass = require( './lib/class_abstract' );
|
exports.AbstractClass = require( './lib/class_abstract' );
|
||||||
exports.FinalClass = require( './lib/class_final' );
|
exports.FinalClass = require( './lib/class_final' );
|
||||||
exports.Interface = require( './lib/interface' );
|
exports.Interface = require( './lib/interface' );
|
||||||
exports.Trait = require( './lib/Trait' );
|
exports.Trait = require( './lib/Trait' );
|
||||||
exports.version = require( './lib/version' );
|
exports.version = require( './lib/version' );
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,8 @@ var util = require( './util' ),
|
||||||
Warning = require( './warn' ).Warning,
|
Warning = require( './warn' ).Warning,
|
||||||
Symbol = require( './util/Symbol' ),
|
Symbol = require( './util/Symbol' ),
|
||||||
|
|
||||||
|
parseKeywords = require( './prop_parser' ).parseKeywords,
|
||||||
|
|
||||||
hasOwn = Object.prototype.hasOwnProperty,
|
hasOwn = Object.prototype.hasOwnProperty,
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,15 +52,27 @@ var util = require( './util' ),
|
||||||
/**
|
/**
|
||||||
* Hash of reserved members
|
* Hash of reserved members
|
||||||
*
|
*
|
||||||
* These methods cannot be defined in the class. They are for internal use
|
* These methods cannot be defined in the class; they are for internal
|
||||||
* only. We must check both properties and methods to ensure that neither is
|
* use only. We must check both properties and methods to ensure that
|
||||||
* defined.
|
* neither is defined.
|
||||||
*
|
*
|
||||||
* @type {Object.<string,boolean>}
|
* @type {Object.<string,boolean>}
|
||||||
*/
|
*/
|
||||||
reserved_members = {
|
reserved_members = {
|
||||||
'__initProps': true,
|
'__initProps': true,
|
||||||
'constructor': true,
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of aliased members
|
||||||
|
*
|
||||||
|
* These are members that alias to another. Ideally, this should be a
|
||||||
|
* very small list. It is useful for introducing features without
|
||||||
|
* deprecating old.
|
||||||
|
*
|
||||||
|
* @type {Object.<string,string>}
|
||||||
|
*/
|
||||||
|
aliased_members = {
|
||||||
|
'constructor': '__construct',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -641,12 +655,56 @@ exports.prototype.buildMembers = function buildMembers(
|
||||||
parser.method && hjoin( 'method', handlers.method );
|
parser.method && hjoin( 'method', handlers.method );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlers.keywordParser = _keywordParser;
|
||||||
|
|
||||||
// parse members and process accumulated member state
|
// parse members and process accumulated member state
|
||||||
util.propParse( props, handlers, context );
|
util.propParse( props, handlers, context );
|
||||||
this._memberBuilder.end( context.state );
|
this._memberBuilder.end( context.state );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Member keyword parser
|
||||||
|
*
|
||||||
|
* In reality, this parser is simply intended to override names where there
|
||||||
|
* are applicable aliases; all keyword parsing is kept to the original
|
||||||
|
* implementation.
|
||||||
|
*
|
||||||
|
* @param {string} prop property to parse
|
||||||
|
*
|
||||||
|
* @return {{name: string, bitwords: number, keywords: Object.<string, boolean>}}
|
||||||
|
*/
|
||||||
|
function _keywordParser( prop )
|
||||||
|
{
|
||||||
|
var result = parseKeywords( prop ),
|
||||||
|
alias = _getMemberAlias( result.name );
|
||||||
|
|
||||||
|
if ( alias !== undefined )
|
||||||
|
{
|
||||||
|
result.name = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a member alias for NAME
|
||||||
|
*
|
||||||
|
* If NAME has no alias, then the result is `undefined`.
|
||||||
|
*
|
||||||
|
* @param {string} name member name
|
||||||
|
*
|
||||||
|
* @return {string|undefined}
|
||||||
|
*/
|
||||||
|
function _getMemberAlias( name )
|
||||||
|
{
|
||||||
|
return ( hasOwn.call( aliased_members, name ) )
|
||||||
|
? aliased_members[ name ]
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function _parseEach( name, value, keywords )
|
function _parseEach( name, value, keywords )
|
||||||
{
|
{
|
||||||
var defs = this.defs;
|
var defs = this.defs;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Property keyword parser module
|
* Property keyword parser module
|
||||||
*
|
*
|
||||||
* Copyright (C) 2010, 2011, 2012, 2013, 2014 Free Software Foundation, Inc.
|
* Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015
|
||||||
|
* Free Software Foundation, Inc.
|
||||||
*
|
*
|
||||||
* This file is part of GNU ease.js.
|
* This file is part of GNU ease.js.
|
||||||
*
|
*
|
||||||
|
@ -60,7 +61,7 @@ exports.kmasks = _kmasks;
|
||||||
*
|
*
|
||||||
* @param {string} prop property string, which may contain keywords
|
* @param {string} prop property string, which may contain keywords
|
||||||
*
|
*
|
||||||
* @return {{name: string, keywords: Object.<string, boolean>}}
|
* @return {{name: string, bitwords: number, keywords: Object.<string, boolean>}}
|
||||||
*/
|
*/
|
||||||
exports.parseKeywords = function ( prop )
|
exports.parseKeywords = function ( prop )
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Tests class module constructor creation
|
* Tests class module constructor creation
|
||||||
*
|
*
|
||||||
* Copyright (C) 2014 Free Software Foundation, Inc.
|
* Copyright (C) 2014, 2015 Free Software Foundation, Inc.
|
||||||
*
|
*
|
||||||
* This file is part of GNU ease.js.
|
* This file is part of GNU ease.js.
|
||||||
*
|
*
|
||||||
|
@ -21,6 +21,24 @@
|
||||||
|
|
||||||
require( 'common' ).testCase(
|
require( 'common' ).testCase(
|
||||||
{
|
{
|
||||||
|
caseSetUp: function()
|
||||||
|
{
|
||||||
|
// ease.js was written long before ES6 drafts began providing class
|
||||||
|
// support. Now that they do, we should support their constructor
|
||||||
|
// decision as well as our own.
|
||||||
|
this.ctors = [ '__construct', 'constructor' ];
|
||||||
|
|
||||||
|
// we only use ES3 features, thus this
|
||||||
|
this.mkctor = function( name, f )
|
||||||
|
{
|
||||||
|
var o = {};
|
||||||
|
o[ name ] = f;
|
||||||
|
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
setUp: function()
|
setUp: function()
|
||||||
{
|
{
|
||||||
this.Sut = this.require( 'class' );
|
this.Sut = this.require( 'class' );
|
||||||
|
@ -32,10 +50,15 @@ require( 'common' ).testCase(
|
||||||
* defining the class. (Note that the case of ensuring that it is not
|
* defining the class. (Note that the case of ensuring that it is not
|
||||||
* called when creating a subtype is handled by the ExtendTest case.)
|
* called when creating a subtype is handled by the ExtendTest case.)
|
||||||
*/
|
*/
|
||||||
'Constructor should not be invoked before instantiation': function()
|
'@each(ctors) Should not be invoked before instantiation':
|
||||||
|
function( name )
|
||||||
{
|
{
|
||||||
var called = false;
|
var called = false,
|
||||||
this.Sut.extend( { __construct: function() { called = true; } } );
|
dfn = {};
|
||||||
|
|
||||||
|
this.Sut.extend(
|
||||||
|
this.mkctor( name, function() { called = true; } )
|
||||||
|
);
|
||||||
|
|
||||||
this.assertNotEqual( called, true );
|
this.assertNotEqual( called, true );
|
||||||
},
|
},
|
||||||
|
@ -47,14 +70,14 @@ require( 'common' ).testCase(
|
||||||
* is instantiated. Further, it should only be called a single time,
|
* is instantiated. Further, it should only be called a single time,
|
||||||
* which is particularly important if it produces side-effects.
|
* which is particularly important if it produces side-effects.
|
||||||
*/
|
*/
|
||||||
'Constructor should be invoked once upon instantiation': function()
|
'@each(ctors) Should be invoked once upon instantiation':
|
||||||
|
function( name )
|
||||||
{
|
{
|
||||||
var called = 0;
|
var called = 0;
|
||||||
|
|
||||||
var Foo = this.Sut.extend(
|
var Foo = this.Sut.extend(
|
||||||
{
|
this.mkctor( name, function() { called++; } )
|
||||||
__construct: function() { called++; }
|
);
|
||||||
} );
|
|
||||||
|
|
||||||
// note that we're not yet testing the more consise new-less
|
// note that we're not yet testing the more consise new-less
|
||||||
// invocation style
|
// invocation style
|
||||||
|
@ -67,17 +90,20 @@ require( 'common' ).testCase(
|
||||||
* Once invoked, the __construct method should be bound to the newly
|
* Once invoked, the __construct method should be bound to the newly
|
||||||
* created instance.
|
* created instance.
|
||||||
*/
|
*/
|
||||||
'Constructor should be invoked within context of new instance':
|
'@each(ctors) Should be invoked within context of new instance':
|
||||||
function()
|
function( name )
|
||||||
{
|
{
|
||||||
var expected = Math.random();
|
var expected = Math.random();
|
||||||
|
|
||||||
var Foo = this.Sut.extend(
|
var dfn = this.mkctor( name, function()
|
||||||
{
|
{
|
||||||
val: null,
|
this.val = expected;
|
||||||
__construct: function() { this.val = expected; }
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
dfn.val = null;
|
||||||
|
|
||||||
|
var Foo = this.Sut.extend( dfn );
|
||||||
|
|
||||||
// if `this' was bound to the instance, then __construct should set
|
// if `this' was bound to the instance, then __construct should set
|
||||||
// VAL to EXPECTED
|
// VAL to EXPECTED
|
||||||
var inst = new Foo();
|
var inst = new Foo();
|
||||||
|
@ -90,19 +116,18 @@ require( 'common' ).testCase(
|
||||||
* ``class'') should be passed to __construct, unchanged and
|
* ``class'') should be passed to __construct, unchanged and
|
||||||
* uncopied---that is, references should be retained.
|
* uncopied---that is, references should be retained.
|
||||||
*/
|
*/
|
||||||
'Constructor arguments should be passed unchanged to __construct':
|
'@each(ctors) Arguments should be passed unchanged to __construct':
|
||||||
function()
|
function( name )
|
||||||
{
|
{
|
||||||
var args = [ "foo", { bar: 'baz' }, [ 'moo', 'cow' ] ],
|
var args = [ "foo", { bar: 'baz' }, [ 'moo', 'cow' ] ],
|
||||||
given = null;
|
given = null;
|
||||||
|
|
||||||
var Foo = this.Sut.extend(
|
var Foo = this.Sut.extend(
|
||||||
{
|
this.mkctor( name, function()
|
||||||
__construct: function()
|
|
||||||
{
|
{
|
||||||
given = Array.prototype.slice.call( arguments, 0 );
|
given = Array.prototype.slice.call( arguments, 0 );
|
||||||
}
|
} )
|
||||||
} );
|
);
|
||||||
|
|
||||||
new Foo( args[ 0 ], args[ 1 ], args[ 2 ] );
|
new Foo( args[ 0 ], args[ 1 ], args[ 2 ] );
|
||||||
|
|
||||||
|
@ -125,15 +150,16 @@ require( 'common' ).testCase(
|
||||||
* the name __construct---is modelled after PHP; Java classes, for
|
* the name __construct---is modelled after PHP; Java classes, for
|
||||||
* instance, do not inherit their parents' constructors.
|
* instance, do not inherit their parents' constructors.
|
||||||
*/
|
*/
|
||||||
'Parent constructor should be invoked for subtype if not overridden':
|
'@each(ctors)Parent constructor invoked for subtype if not overridden':
|
||||||
function()
|
function( name )
|
||||||
{
|
{
|
||||||
var called = false;
|
var called = false;
|
||||||
|
|
||||||
var Sub = this.Sut.extend(
|
var dfn = {};
|
||||||
{
|
dfn[ name ] = function() { called = true; };
|
||||||
__construct: function() { called = true; }
|
|
||||||
} ).extend( {} );
|
var Sub = this.Sut.extend( dfn )
|
||||||
|
.extend( {} );
|
||||||
|
|
||||||
new Sub();
|
new Sub();
|
||||||
this.assertOk( called );
|
this.assertOk( called );
|
||||||
|
@ -172,18 +198,22 @@ require( 'common' ).testCase(
|
||||||
* __construct, since public is the default and there is no other
|
* __construct, since public is the default and there is no other
|
||||||
* option.)
|
* option.)
|
||||||
*/
|
*/
|
||||||
'__construct must be public': function()
|
'@each(ctors) Constructor must be public': function( name )
|
||||||
{
|
{
|
||||||
var Sut = this.Sut;
|
var Sut = this.Sut;
|
||||||
|
|
||||||
this.assertThrows( function()
|
this.assertThrows( function()
|
||||||
{
|
{
|
||||||
Sut( { 'protected __construct': function() {} } );
|
var dfn = {};
|
||||||
|
dfn[ 'protected ' + name ] = function() {};
|
||||||
|
Sut( dfn );
|
||||||
}, TypeError, "Constructor should not be able to be protected" );
|
}, TypeError, "Constructor should not be able to be protected" );
|
||||||
|
|
||||||
this.assertThrows( function()
|
this.assertThrows( function()
|
||||||
{
|
{
|
||||||
Sut( { 'private __construct': function() {} } );
|
var dfn = {};
|
||||||
|
dfn[ 'private ' + name ] = function() {};
|
||||||
|
Sut( dfn );
|
||||||
}, TypeError, "Constructor should not be able to be private" );
|
}, TypeError, "Constructor should not be able to be private" );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -200,4 +230,35 @@ require( 'common' ).testCase(
|
||||||
var Foo = this.Sut.extend( {} );
|
var Foo = this.Sut.extend( {} );
|
||||||
this.assertStrictEqual( Foo().constructor, Foo );
|
this.assertStrictEqual( Foo().constructor, Foo );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We support multiple constructor styles; only one may be provided.
|
||||||
|
*
|
||||||
|
* This error should happen as a consequence of other method checks that
|
||||||
|
* prohibit redefinitions.
|
||||||
|
*/
|
||||||
|
'Cannot provide multiple constructor styles': function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut,
|
||||||
|
len = this.ctors.length;
|
||||||
|
|
||||||
|
// this will not test every permutation, but will hopefully be a
|
||||||
|
// reasonable test in the event that any additional constructors are
|
||||||
|
// added in the future (we start at 1 because it'll wrap modulo LEN)
|
||||||
|
for ( var i = 1; i < len; i++ )
|
||||||
|
{
|
||||||
|
var dfn = {},
|
||||||
|
a = this.ctors[ i ],
|
||||||
|
b = this.ctors[ ( i + 1 ) % len ];
|
||||||
|
|
||||||
|
dfn[ a ] = function() {};
|
||||||
|
dfn[ b ] = function() {};
|
||||||
|
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
Sut( dfn );
|
||||||
|
}, Error, "Multiple constructors should not be permitted" );
|
||||||
|
}
|
||||||
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Tests class builder member restrictions
|
* Tests class builder member restrictions
|
||||||
*
|
*
|
||||||
* Copyright (C) 2014 Free Software Foundation, Inc.
|
* Copyright (C) 2014, 2015 Free Software Foundation, Inc.
|
||||||
*
|
*
|
||||||
* This file is part of GNU ease.js.
|
* This file is part of GNU ease.js.
|
||||||
*
|
*
|
||||||
|
@ -88,7 +88,7 @@ require( 'common' ).testCase(
|
||||||
*/
|
*/
|
||||||
'Proper members are reserved': function()
|
'Proper members are reserved': function()
|
||||||
{
|
{
|
||||||
var chk = [ '__initProps', 'constructor' ],
|
var chk = [ '__initProps' ],
|
||||||
i = chk.length,
|
i = chk.length,
|
||||||
reserved = this.Sut.getReservedMembers();
|
reserved = this.Sut.getReservedMembers();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Tests index.js
|
* Tests index.js
|
||||||
*
|
*
|
||||||
* Copyright (C) 2014 Free Software Foundation, Inc.
|
* Copyright (C) 2014, 2015 Free Software Foundation, Inc.
|
||||||
*
|
*
|
||||||
* This file is part of GNU ease.js.
|
* This file is part of GNU ease.js.
|
||||||
*
|
*
|
||||||
|
@ -63,4 +63,27 @@ require( 'common' ).testCase(
|
||||||
{
|
{
|
||||||
this.exportedAs( 'version', 'version' );
|
this.exportedAs( 'version', 'version' );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since ECMAScript 6 introduces the ability to define prototypes using
|
||||||
|
* the `class` keyword and some syntatic sugar, it is awkward to use the
|
||||||
|
* traditional ease.js class abstraction with it. Instead, if a user
|
||||||
|
* wishes to wrap a prototype defined in this manner (to take advantage
|
||||||
|
* of ease.js' features), it would be more appropriate to make it look
|
||||||
|
* like we're doing just that.
|
||||||
|
*
|
||||||
|
* This is a shorthand for Class.extend( proto, {} ).
|
||||||
|
*/
|
||||||
|
'Invoking is equivalent to extending with empty definition': function()
|
||||||
|
{
|
||||||
|
var proto = function() {},
|
||||||
|
result = this.Sut( proto );
|
||||||
|
|
||||||
|
this.assertOk( this.Sut.Class.isClass( result ) );
|
||||||
|
|
||||||
|
// this is not a comprehensive test (once we add reflection of some
|
||||||
|
// sort, verify that nothing is added to the prototype)
|
||||||
|
this.assertOk( this.Sut.Class.isA( proto, result() ) );
|
||||||
|
},
|
||||||
} );
|
} );
|
||||||
|
|
Loading…
Reference in New Issue