Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active March 20, 2022 03:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfkaye/04e13542abd4bf06abc9 to your computer and use it in GitHub Desktop.
Save dfkaye/04e13542abd4bf06abc9 to your computer and use it in GitHub Desktop.
Function#create ~ fiercely idiomatic alternatives to JavaScript object construction ~ with variation for inheritance
// Note 16 DEC 2014
// see tweet re ES6 classes ~ https://twitter.com/Keithamus/status/543409270029844480
//////
// July 23-24, 2014
// TODO: more examples - prototypes, composition, aggregation
// 3 June 2015 ~ fixed examples and Object.create, added Object.keys
// this version does not "inherit"
// We have Object.create() - or if not we use this:
Object.create || (function() {
function F() {}
Object.create = function(o){
F.prototype = o;
return new F;
};
}());
// but Object.create doesn't create a Function, and it doesn't execute one if passed in.
// A factory or create method on each function would enable us to stop using 'new', write
// bare-bones 'constructor' functions that don't need argument lists, and avoid the need
// for an additional init() method being attached to a prototype.
// create() would instead take an initializer function that sets or overrides properties
// on a newly created object.
// 26 July ~ create() could take a second options argument for setting defaults that the initializer
// would override.
// 26 July ~ create() could also allow zero arguments to be passed in, and simply return the simplest
// instance of the constructable object.
// first, an Object.keys shim:
Object.keys || (Object.keys = function(o) {
if (o == null) {
throw new TypeError("can't convert " + o + " to object");
}
var keys = [];
for (var k in o) {
o.hasOwnProperty(k) && (keys.push(k));
}
return keys;
});
// now, here's an under-development implementation of that method:
Function.prototype.create ||
(Function.prototype.create = function create(fn, opts) {
var o = Object.create(this.prototype);
// support default assignments
if (opts && typeof opts == 'object') {
Object.keys(opts).forEach(function(k) {
o[k] = opts[k];
});
}
// support zero-arg calls
fn && typeof fn == 'function' && fn.call(o, o);
return o;
});
// Now, every function has its own .create() method which takes an initializer function
// which in turn takes an instance created from the prototype of the original function.
//////
// simple object with a default name property
function simple(fn) {
return simple.create(fn, { name: 'default' });
}
// zero-arg call returns bare instance of the simple "type"
var anon = simple();
// call with initializer fn to override default name
var named = simple(function(simple){
// EITHER
simple.name = 'overridden';
// OR
this.name = 'overridden';
});
console.log(anon); // simple {name: "default"}
console.log(named); // simple {name: "overridden"}
//////
// if you need something really anonymous like an IIFE or a sandbox,
// create a one-off function to use as the anonymous context instead,
// that you run once, sandbox forever
(function (){}).create(function(context) {
context.name = 'sandbox';
console.log(this); // Object { name="sandbox" }
});
// you can name your one-off fn for logging purposes:
(function sandbox(){}).create(function(context) {
context.name = 'sandbox';
console.log(this); // sandbox { name="sandbox" }
});
//////
// you can combine objects made in this way via their initializers
function A(fn) { return A.create(fn); }
function B(fn) { return B.create(fn); }
var b = B(function() {
this.a = A(function(a) { a.name = 'b->a'; });
console.warn(b.a.name == 'b->a');
});
//////
// prototypes are still supported
B.prototype.log = function (msg) {
console.log('log called on ' + this.constructor.name + ': ' + (msg || ''));
};
B.create(function(bbbbb) {
bbbbb.log(bbbbb === this); // log called on B: true
});
// instead of passing dependencies via constructor, we can
// declare them EXTREMELY LATE, after object creation, and mix in
// or refer to dependencies from within the initializer function
// notice the boilerplate that arises, though, arguing for a Function.create()
// factory method....
//////
// this version supports inheritance (pretty much but not "perfect" ~ Date is still fairly intractable)
// July 30, 2014 ~ start on inheritance
// August 1, 2014 ~ working ! (pretty much)
//Function.prototype.create ||
(Function.prototype.create = function create(fn, opts) {
var o, __parent__, thisp;
// inheritance fork
if (opts && opts['extend']) {
thisp = this.prototype;
this.prototype = __parent__ = new opts['extend']();
}
o = Object.create(this.prototype);
// inheritance fork
__parent__ && (function () {
for (var i in thisp) {
o[i] = thisp[i];
}
o.__parent__ = __parent__;
// Date needs these
__parent__ instanceof Date && (
o.toString = function() {
return this.__parent__.toString();
},
o.valueOf = function () {
return this.__parent__.valueOf();
});
// Array and String need these to return a copy
'concat' in __parent__ && (
o.slice = function () {
return this.__parent__.slice.apply(this, arguments);
},
o.concat = function () {
return this.__parent__.concat.apply(this.slice(), arguments);
});
}());
// set default assignments
opts && typeof opts == 'object' && (function() {
for (var k in opts) {
if (opts.hasOwnProperty(k)) {
o[k] = opts[k];
}
}
}());
// support zero-arg calls (no fn arg)
fn && typeof fn == 'function' && fn.call(o, o);
return o;
});
// Now, every function has its own .create() method which takes an initializer
// function, which in turn takes an instance created from the prototype of the
// original function.
//////
// simple object with a default name property
function simple(fn) {
return simple.create(fn, { name: 'default-simple' });
}
// zero-arg call returns bare instance of the simple "type"
var anon = simple();
// call with initializer fn to override default name
var named = simple(function(simple){
// EITHER
simple.name = 'overridden';
// OR
this.name = 'overridden';
});
console.log(anon); // simple {name: "default"}
console.log(named); // simple {name: "overridden"}
console.log(anon instanceof simple); // true
// compound inherits simple
function compound(fn) {
return compound.create(fn, { extend: simple });
}
var comp = compound(function() {
console.log('compound created');
});
console.log(comp instanceof simple);
console.log(comp instanceof compound);
// deep inherits compound
function deep(fn) {
return deep.create(fn, { extend: compound });
}
deep.prototype.log = function(s) {
console.log(s);
};
// return nothing, just do some work
deep(function() {
console.log('deep: ' + this.name);
console.log(this);
this.log('deep created with name: ' + this.name);
this.log(this instanceof deep);
this.log(this instanceof simple);
this.log(this instanceof compound);
this.log(this.__parent__ instanceof compound);
this.log(this.__parent__ instanceof simple);
this.log(this.__parent__.__parent__ instanceof simple);
});
// inheritance with native Array ~ pretty close
function array(fn) {
array.create(fn, { extend: Array });
}
array(function(array) {
console.log(array); // Object[]
console.log('__parent__');
console.log(array.__parent__); // [ ]
array.push(1,2,3);
console.log('array: ' + array); // array: 1,2,3
// CUTTING LENGTH DIRECTLY...
console.log(array.length); // 3
array.length = 1;
console.log(array.length); // 1
console.log(array); // Object[1]
console.log('__parent__');
console.log(array.__parent__); // [ ]
console.log('Array instance: ' + (this instanceof Array)); // true
});
// deep inheritance with native Date ~ demonstrates the issues when inheriting
// Date instance methods
function date(fn) {
return date.create(fn, { extend: Date });
};
function subdate(fn) {
return subdate.create(fn, { extend: date });
};
var s = subdate(function(s) {
console.log(s === this);
console.log(s instanceof Date);
console.log(s instanceof date);
console.log(s instanceof subdate);
console.log(s.__parent__ instanceof date);
console.log(s.__parent__ instanceof Date);
console.log(s.__parent__.__parent__ instanceof Date);
// HOWEVER, to use real Date methods, we have to call them on Date instances:
console.log('time: ' + s.__parent__.__parent__.getTime()); // works
var msg = 'calling s.getTime() should fail: ';
try {
s.getTime();
} catch (e) {
msg += e.message;
} finally {
console.error(msg);
// calling s.getTime() should fail: Date.prototype.getTime called on incompatible Object
}
// 18 March 2022
// could make local methods that walk the parent prototypes
s.getDate = function() { return s.__parent__.__parent__.getTime() }
});
///////
// August 5, 2014 ~ Function.create factory
// first define the newInstance or create method for any function...
/*
//Function.prototype.create ||
(Function.prototype.create = function create(fn, opts) {
var o = Object.create(this.prototype);
// support default assignments
if (opts && typeof opts == 'object') {
for (var k in opts) {
if (opts.hasOwnProperty(k)) {
o[k] = opts[k];
}
}
}
// support zero-arg calls
fn && typeof fn == 'function' && fn.call(o, o);
return o;
});
*/
// 19 March 2022
// Far better implementation of function.create
//Function.prototype.create ||
(Function.prototype.create = function create(fn, opts) {
// support default assignments
var o = Object.assign({}, Object(opts))
// support zero-arg calls
typeof fn == 'function' && fn.call(o, o);
return o;
});
// now define a constructor factory for passing in prototypes
//Function.create ||
(Function.create = function (prototype) {
function constructor(fn) {
return constructor.create(
fn,
Object.assign(
{},
constructor.prototype,
constructor.__default__
)
);
}
constructor.__default__ = prototype.__default__; // closure avoidance
constructor.prototype = prototype;
return prototype.constructor = constructor;
});
// ...so, instead of writing
function Model(fn) {
return Model.create(fn, {name: 'default-name;'});
}
Model.prototype.get = function() {};
Model.prototype.set = function() {};
Model.prototype.clear = function() {};
Model.prototype.toString = function() {
return this.name;
};
// ...now we can write
var Model = Function.create({
__default__ : { name: 'default-name' },
get: function () { return this.name },
set: function (name) { this.name = name },
clear: function () { delete this.name },
toString: function () { return this.name }
});
// and call it with
var m = Model(function(m) {
console.log(this === m)
console.warn('%s', this); // 'default-name'
m.name = 'custom-name';
});
console.warn('%s', m); // custom-name
console.log(JSON.stringify(m)); // {"name":"custom-name"}
console.log(m);
/*
constructor { name="custom-name",
__default__={ name="default-name" },
get=function(),
set=function(),
clear=function(),
toString=function(),
constructor=constructor(fn) }
*/
// 18 March 2022
// Turns out we get
// Instance cloning for free!
var o = m.constructor(function(o) {
o.name = "clone"
});
console.log(o == m);
// false
console.log(o.constructor === m.constructor)
// true
console.log(`${o.name}, ${m.name}`);
// clone, custom-name

Function.prototype.create

Why another constructor pattern

It's not really "another" pattern ~ it's borrowed from Java's instance initializer block pattern, particularly on anonymous classes ~ cf., Paul Holser, "Concisions, Concisions...or, (De-) Constructing an Idiom".

Mainly I wanted to write one-off scripts like this, where all is enclosed:

model(function(m) {
  // m === this
  ...
});

task(function(t) {
  // t === this
  ...
});

Benefits

  • no new globals added
  • shows use of monkey-patching on a native built-in
  • avoid using new for constructors (externally)
  • avoid logic and assignments in constructors
  • avoid instanceof checks in constructors (if not this instanceof x then call new x...)
  • avoid parameter lists for constructors
  • instance initializers are callbacks that support:
    • very late binding ~ enabling mocks
    • encapsulated state
    • built-in default option-setting
  • avoid assignments directly on the prototype outside a safe pattern ~e.g., no x.prototype.something = somethingElse

Yes, but why the inheritance version?

  • show how it's possible to extend natives and inherit from them
  • show what's broken or unfinished in JavaScript ~ Array has some [[class]] identifier that makes it special to the interpreter ~ Date is a mishmash of strings and a timestamp...
@ericelliott
Copy link

A couple serious issues -- as the original prototype library famously pointed out, it's not safe to extend natives for two reasons: 1) other code relies on unmodified natives, and 2) if the natives ever add a method with the same name, either your code breaks, or the code that relies on the native implementation breaks (depending on how you've written your code).

Next, have a look at Prototypal Inheritance with Stamps: http://ericleads.com/2014/02/prototypal-inheritance-with-stamps/

@ericelliott
Copy link

var stampit = require('stampit'),
  arrayStamp = stampit.convertConstructor(Array),
  arr = arrayStamp();

arr.push('a');
arr.push('b');
arr.push('c');

arr.forEach(function (item) {
  console.log(item);
});

@dfkaye
Copy link
Author

dfkaye commented Nov 6, 2014

@ericelliott ~ see the commented lines, like

// Function.prototype.create || 

which should be uncommented in order to protect against overwriting those method names if they already exist.

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