stamp-this-sensitive

2.1.1 • Public • Published

stampit

Travis-CIGitter

Create objects from reusable, composable behaviors. Stampit uses three different kinds of prototypal OO to let you inherit behavior in a way that is much more powerful and flexible than classical OO.

Stampit was written as an example for the book, "Programming JavaScript Applications" (O'Reilly).

Looking for a deep dive into prototypal OO, stamps, and the Two Pillars of JavaScript? Learn JavaScript with Eric Elliott.

React Users. Stampit loves React. Check out react-stampit for composable components.

Status

v1, stable, in production use with millions of monthly users. There will be no breaking changes in the 1.x line.

v2, current stable. Breaking changes:

  • stampit() now receives options object ({methods,refs,init,props,static}) instead of multiple arguments.
  • All chaining methods return new stamps instead of self-mutating this stamp.
  • state() always shallow merge properties. It was not doing so in a single rare case.
  • Instead of factory arguments the enclose() functions now receives the following object { instance, stamp, args }.

There is a slight chance these changes affect your current codebase. If so, we would recommend you to update to v2 because it is more powerful. See advances examples.

Install

Stampit can be installed via npm

npm install stampit

or the unofficial gem

gem install stampitjs-rails

or by downloading the latest release.

Features

  • Create factory functions (called stamps) which stamp out new objects. All of the new objects inherit all of the prescribed behavior.

  • Assign properties by passing a references object to the stamp (factory function).

  • Compose stamps together to create new stamps.

  • Inherit methods and default state.

  • Supports composable private state and privileged methods.

  • References are copied across for each instance.

  • Properties are deeply merged and cloned for each instance, so it won't be accidentally shared.

  • Initializers are called for each new instance. Provides wide extensibility to stamp behavior.

  • For the curious - it's great for learning about prototypal OO. It mixes three major types of prototypes:

    1. differential inheritance, aka delegation (for methods),
    2. cloning, aka concatenation/exemplar prototypes (for state),
    3. functional / closure inheritance (for privacy / encapsulation)

What is a Stamp?

A stamp is a composable factory function. Stamps allow you to inherit easily from multiple ancestors by composing multiple source stamps. You can combine properties, methods, and initializers (with closures) from any number of stamps to produce a new stamp. Stamps are more flexible than traditional factory functions or classical multiple inheritance. Traditional factory functions can't be composed together to produce new factory functions. Class inheritance does not provide a standardized mechanism for class composition.

Stamp composition takes advantage of three different kinds of prototypal inheritance:

  • Differential inheritance, aka delegation (e.g., JavaScript's [[Prototype]]),
  • Mixins/cloning with optional deep merge, aka concatenative inheritance (e.g., JavaScript's Object.assign()),
  • Functional / closure inheritance (for initialization or privacy/encapsulation)

When invoked the stamp factory function creates and returns object instances assigning:

var DbAuthStamp = stampit().
  methods({ authorize: function(){} }). // methods each new object instance will have
  refs({user: {name: 'guest', pwd: ''}}). // properties to be set by reference to object instances
  init(function(context){ }). // init function(s) to be called when an object instance is created
  props({db: {host: 'localhost'}}); // properties to be deeply merged to object instances
 
var dbAuthorizer = DbAuthStamp({ user: adminUserCredentials });

How are Stamps Different from Classes?

  • It's easy to combine multiple stamps to create a new stamp with all of the source stamp capabilities
  • Stamps are factory functions, so they don't need to be invoked with new (which couples callers to the implementation of object instantiation)
  • Stamps don't create parent-child class hierarchies. Class hierarchies create "is-a" relationships between classes. Stamp composition creates "has-a" or "uses-a" relationships, instead. For that reason, stamp inheritance is less brittle than class inheritance.

All of these stampit methods may be called multiple times to add more elements to the factory.

What's the Point?

Prototypal OO is great, and JavaScript's capabilities give us some really powerful tools to explore it, but it could be easier to use.

Basic questions like "how do I inherit privileged methods and private data?" and "what are some good alternatives to inheritance hierarchies?" are stumpers for many JavaScript users.

Let's answer both of these questions at the same time. First, we'll use a closure to create data privacy:

var a = stampit().init(function () {
  var priv = 'a';
  this.getA = function () {
    return priv;
  };
});

It uses function scope to encapsulate private data. Note that the getter must be defined inside the function in order to access the closure variables.

Let's see if that worked:

a(); // Object -- so far so good.
a().getA(); // "a"

Yes. Got it. In both of these instances, we actually created a brand new object, and then immediately threw it away, because we didn't assign it to anything. Don't worry about that.

Here's another:

var b = stampit().init(function () {
  var priv = 'b';
  this.getB = function () {
    return priv;
  };
});

Those priv's are not a typo. The point is to demonstrate that a and b's private variables won't clash.

But here's the real treat:

var c = stampit.compose(a, b);
 
var foo = c(); // we won't throw this one away...
 
foo.getA(); // "a"
foo.getB(); // "b"

WAT? Yeah. You just inherited privileged methods and private data from two sources at the same time.

But that's boring. Let's see what else is on tap:

// Some more privileged methods, with some private data.
// Use stampit.mixin() to make this feel declarative:
var availability = stampit().init(function () {
  var isOpen = false; // private
 
  return stampit.mixin(this, {
    open: function open() {
      isOpen = true;
      return this;
    },
    close: function close() {
      isOpen = false;
      return this;
    },
    isOpen: function isOpenMethod() {
      return isOpen;
    }
  });
});
 
// Here's a mixin with public methods, and some state:
var membership = stampit({
  methods: {
    add: function (member) {
      this.members[member.name] = member;
      return this;
    },
    getMember: function (name) {
      return this.members[name];
    }
  },
  refs: {
    members: {}
  }
});
 
// Let's set some defaults:
var defaults = stampit().refs({
      name: 'The Saloon',
      specials: 'Whisky, Gin, Tequila'
    });
 
// Classical inheritance has nothing on this. No parent/child coupling. No deep inheritance hierarchies.
// Just good, clean code reusability.
var bar = stampit.compose(defaults, availability, membership);
 
// Note that you can override references on instantiation:
var myBar = bar({name: 'Moe\'s'});
 
// Silly, but proves that everything is as it should be.
myBar.add({name: 'Homer' }).open().getMember('Homer');

Statics

Stamps have a static method. This method applies passed object properties to the calling stamp's object. static is a convenience method. The old school way to apply statics to a stamp is by using stampit's mixIn/extend method.

stampit.extend(stamp, {
  foo: 'foo'
});
 
This can now be written as:
 
stamp.static({
  foo: 'foo'
});

Chaining methods

Chaining stamps always creates new stamps.

Chain .methods() ...

var myStamp = stampit().methods({
  fooMethod: function () {
    return 'foo';
  },
  methodOverride: function () {
    return false;
  }
}).methods({
  barMethod: function () {
    return 'bar'
  },
  methodOverride: function () {
    return true;
  }
});

And .refs() ...

myStamp = myStamp.refs({
  foo: {bar: 'bar'},
  stateOverride: false
}).refs({
  bar: 'bar',
  stateOverride: true
});

And .props() ...

myStamp = myStamp.props({
  name: { first: 'John' }
}).props({
  name: { last: 'Doe' }
});

And .static() ...

myStamp.static({
  foo: {bar: 'bar'},
  staticOverride: false
}).static({
  bar: 'bar',
  staticOverride: true
});

And .init() ...

myStamp = myStamp.init(function () {
  var secret = 'foo';
 
  this.getSecret = function () {
    return secret;
  };
}).init(function () {
  this.a = true;
}).init({
  foo: function bar() {
    this.b = true;
  }
}, {
  bar: function baz() {
    this.c = true;
  }
});
 
var obj = myStamp.create();
obj.fooMethod && obj.barMethod && obj.methodOverride; // true
obj.foo && obj.bar && obj.stateOverride; // true
obj.name.first && obj.name.last; // true
obj.getSecret && obj.a && obj.b && obj.c; // true

And .compose().

var newStamp = baseStamp.compose(myStamp);

Pass multiple objects into .methods(), .refs(), .init(), props(), .static(), or .compose().

Stampit mimics the behavior of _.extend(), $.extend() when you pass multiple objects into one of the stamp methods. In other words, it will copy all of the properties from those objects to the .methods, .refs, .init or .props of the stamp. The properties from later arguments in the list will override the same named properties of previously passed in objects. refs will be copied by reference. props will be deeply merged.

  var obj = stampit().methods({
    a: function () { return 'a'; }
  }, {
    b: function () { return 'b'; }
  }).create();

Or .refs() ...

  var obj = stampit().refs({
    a: 'a'
  }, {
    b: 'b'
  }).create();

Or .init() ...

  var obj = stampit().init(function () {
    console.log(this);
  }, function () {
    console.log(this); // same as above
  }).create();

Or .props() ...

  var obj = stampit().props({
    name: { first: 'John' }
  }, {
    name: { last: 'Doe' }
  }).create();

Or .static() ...

  var obj = stampit().static({
    foo: 'foo'
  }, {
    bar: 'bar'
  }).create();

Or even .compose() ...

  var obj = abstractStamp.compose(concreteStamp, additionalStamp, utilityStamp).create();

Stampit API

Source: stampit.js

stampit()

Return a factory function (called a stamp) that will produce new objects using the components that are passed in or composed.

  • @param {Object} [options] Options to build stamp from: { methods, refs, init, props }
  • @param {Object} [options.methods] A map of method names and bodies for delegation.
  • @param {Object} [options.refs] A map of property names and values to be mixed into each new object.
  • @param {Object} [options.init] A closure (function) used to create private data and privileged methods.
  • @param {Object} [options.props] An object to be deeply cloned into each newly stamped object.
  • @return {Function} factory A factory to produce objects.
  • @return {Function} factory.create Just like calling the factory function.
  • @return {Object} factory.fixed An object map containing the stamp metadata.
  • @return {Function} factory.methods Add methods to the stamp. Chainable.
  • @return {Function} factory.refs Add references to the stamp. Chainable.
  • @return {Function} factory.init Add a closure which called on object instantiation. Chainable.
  • @return {Function} factory.props Add deeply cloned properties to the produced objects. Chainable.
  • @return {Function} factory.compose Add stamp to stamp. Chainable.
  • @return {Function} factory.static Add properties to the factory object. Chainable.

The stamp object

stamp.methods()

Take n objects and add them to the methods list of a new stamp. Creates new stamp.

  • @return {Object} stamp The new stamp based on the original this stamp.

stamp.refs()

Take n objects and add them to the references list of a new stamp. Creates new stamp.

  • @return {Object} stamp The new stamp based on the original this stamp.

It has an alias - stamp.state(). Deprecated.

stamp.init([arg1] [,arg2] [,arg3...])

Take n functions, an array of functions, or n objects and add the functions to the initializers list of a new stamp. Creates new stamp.

  • @return {Object} stamp The new stamp based on the original this stamp.

If any of the init() functions return a promise then the stamp will always be creating promises which resolve to the expected object instance.

It has an alias - stamp.enclose(). Deprecated.

Functions passed into .init() are called any time an object is instantiated. That happens when the stamp function is invoked, or when the .create() method is called.

Each function receives the following object as the first argument:

{
  instance,
  stamp,
  args
}

Examples (ES6).

Make any stamp cloneable.

let Cloneable = stampit().init(({instance, stamp, args}) =>
  instance.clone = () => stamp(instance);
});
 
let MyStamp = stampit().refs({x: 42}).compose(Cloneable); // composing with the "Cloneable" behavior
MyStamp.create().clone().clone().clone().x === 42; // true

Teach any object to return original stamp:

let SelfKnowlegeable = stampit().init(({instance, stamp, args}) =>
  this.originalStamp = stamp;
});
 
let MyStamp = stampit().refs({x: 42}).compose(SelfKnowlegeable); // composing with the "SelfKnowlegeable" behavior
MyStamp.create().originalStamp === MyStamp; // true

stamp.props()

Take n objects and deep merge them safely to the properties. Creates new stamp. Note: the merge algorithm will not change any existing refs data of a resulting object instance.

  • @return {Object} stamp The new stamp based on the original this stamp.

stamp.compose([arg1] [,arg2] [,arg3...])

Take one or more factories produced from stampit() and combine them with this to produce and return a new factory object. Combining overrides properties with last-in priority.

  • @return {Function} A new stampit factory composed from arguments.

stamp.create([properties] [,arg2] [,arg3...])

Alias to stamp([properties] [,arg2] [,arg3...]).

Just like calling stamp(), stamp.create() invokes the stamp and returns a new object instance. The first argument is an object containing properties you wish to set on the new objects. The properties are copied by reference using standard mixin/extend/assign algorithm.

The remaining arguments are passed to all .init() functions. WARNING Avoid using two different .init() functions that expect different arguments. .init() functions that take arguments should not be considered safe to compose with other .init() functions that also take arguments. Taking arguments with an .init() function is an anti-pattern that should be avoided, when possible.

stamp.static()

Take n objects and add all props to the factory object.

  • @return {Object} stamp The factory in question (this).

Utility methods

stampit.methods()

Shortcut for stampit().methods()

stampit.refs()

Shortcut for stampit().refs()

stampit.init()

Shortcut for stampit().init()

stampit.props()

Shortcut for stampit().props()

stampit.compose()

Take two or more stamps produced from stampit() and combine them to produce a new stamp. Combining overrides properties with last-in priority.

  • @param {...Function|Function[]} stamp any number of stamps.
  • @return {Function} A new stamp composed from arguments.

stampit.mixin(destObj, source1 [, sourc2] [, source3]...)

Same as Object.assign(). Take a destination object followed by one or more source objects, and copy the source object properties to the destination object, with last in priority overrides.

  • @param {Object} destination An object to copy properties to.
  • @param {...Object} source An object to copy properties from.
  • @returns {Object}

stampit.extend(), .mixIn(), .assign()

Aliases for stampit.mixin().

stampit.isStamp(obj)

Take an object and return true if it's a stamp, false otherwise.

stampit.convertConstructor()

Take an old-fashioned JS constructor and return a stamp that you can freely compose with other stamps. It is possible to use constructors that take arguments. Simply pass the arguments into the returned stamp after the properties object: var myInstance = myStamp(props, arg1, arg2);

Note that if you use this feature, it is not safe to compose the resulting stamp with other stamps willy-nilly, because if two different stamps depend on the argument passing feature, the arguments will probably clash with each other, producing very unexpected results.

  • @param {Function} Constructor
  • @return {Function} A composable stampit factory (aka stamp).
  // The old constructor / class thing...
  var Constructor = function Constructor() {
    this.thing = 'initialized';
  };
  Constructor.prototype.foo = function foo() { return 'foo'; };
 
  // The conversion
  var oldskool = stampit.convertConstructor(Constructor);
 
  // A new stamp to compose with...
  var newskool = stampit({
    methods: {
      bar: function bar() { return 'bar'; }
     // your methods here...
    },
    init: function () {
      this.baz = 'baz';
    }
  });
 
  // Now you can compose those old constructors just like you could
  // with any other stamp...
  var myThing = stampit.compose(oldskool, newskool);
 
  var t = myThing();
 
  t.thing; // 'initialized',
 
  t.foo(); // 'foo',
 
  t.bar(); // 'bar'

Dependents (0)

Package Sidebar

Install

npm i stamp-this-sensitive

Weekly Downloads

4

Version

2.1.1

License

MIT

Last publish

Collaborators

  • rektide