Cookie Consent by FreePrivacyPolicy.com TIBET
TIBET Logo

TIBET

TIBET

JavaScript: The TIBET Parts™


I want to thank the people I worked with at Electric Communities and State Software who helped me discover that deep down there was goodness in this language, especially Chip Morningstar, Randy Farmer, John La, Mark Miller, Scott Shattuck, and Bill Edney.

-- Douglas Crockford, JavaScript: The Good Parts

Wins

  • Avoid problems with JS/DOM changes, browser bugs, etc. via common APIs.
  • Avoid limiting boilerplate syntax for modules, types, and other declarations.
  • Avoid common pitfalls due to type conversion, poor reflection, etc.
  • Avoid compiler and source map overhead and leverage HTML5 browsers.
  • Leverage "standard library" APIs for collections, comparisons, etc.

Contents

Concepts

Cookbook

Modules

Types

Instances

Collections

Comparisons

summary

Concepts

We sometimes joke that "TIBET isn't JavaScript, it's just written in it". While that sounds extreme, it's better to start with this standpoint and work our way back to reality.

In reality, TIBET is just JavaScript, there's no TIBET compiler or interpreter, no build or reload delays, no fussing with source maps. TIBET is "100% Pure JavaScript" optimized for ease of development, ease of tooling, and ease of maintenance.

To achieve its goals of easier, faster development, TIBET relies on a set of APIs that replace many of the standard JavaScript keywords with object methods. These APIs are specifically designed to avoid common issues with authoring in standard JavaScript.

Coding Sample
Sample TIBET Source

Over our 20-plus years developing enterprise-class applications for the web we've found these changes to be critical to maintaining, extending, and evolving the TIBET platform and its applications with a minimum of overhead.

In return for a significant increase in power, control, and maintainability, TIBET requires you to make some simple changes in your approach to JavaScript coding.

In extreme form the quick summary of these changes is:

  • Don't use functions as types.
  • Don't use typeof or instanceof.
  • Don't use new.
  • Don't access properties directly.
  • Don't use {} as a hash/dictionary.
  • Don't use for/in.

TIBET provides more powerful replacements, namely:

  • defineSubtype and addTraits
  • isType, isKindOf, isMemberOf, etc.
  • construct (a pairing of alloc and init)
  • get() and set() (method lookup and change notification)
  • TP.core.Hash (TP.hc) and its collection API support
  • TIBET's perform, collect, select, detect, reject, or forEach, map, et. al.

By encapsulating keyword-based operations behind a method-based API TIBET is able to adjust to evolving standards, adapt to release-specific bugs, and capture critical metadata which supports advanced tooling and streamlined authoring.

Cookbook

Modules

TIBET doesn't implement a strict module construct although it does have a strong sense of load packages, groups of resources you configure to meet your application needs.

TIBET packages are not defined in JavaScript source code, they're described in package files which provide complete control over what constitutes each load package. Package definitions can be nested and automatically detect and eliminate circular or duplicate resource references. A resource can be referenced in as many packages as you like.

The TIBET Loader reads package definitions and loads the defined resources based on runtime data including whether you want minified source, concatenated source, source specific to a particular browser, language, user role, etc. You have fine-grained control over what loads for each individual user if necessary.

Don't use 'module' boilerplate in source files.

TIBET does not use constructs such as define or require. There's no boilerplate that must be placed in your TIBET source files. Just enter your code without ceremony.

//  DON'T DO THIS IN TIBET
define([...], function() {
    ...code...
});

//  DON'T DO THIS EITHER
require([...], function() {
    ...code...
});

//  FOR SANITY'S SAKE DO NOT DO THIS:
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS-like
        module.exports = factory(require('jquery'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.jQuery);
    }
}(this, function ($) {
    //    methods
    function myFunc(){};

    //    exposed public method
    return myFunc;
}));

If you feel like using a simple closure to help scope things you can use a wrapper function, but typical TIBET source files don't contain any boilerplate whatsoever.

//  DO THIS
...code...;

//  OR, AT MOST, DO THIS
(function() {
    ...code...
}());

//  BUT DO NOT DO THIS
(function() {
    //  NO...module.exports won't be found.
    module.exports = ...code...
}());

Seriously, just type in your code.

The only dependencies in typical TIBET source are that subtypes should be loaded after their supertype. That's an easy order to maintain in your package files.

Define modules in TIBET Loader package files.

The TIBET Loader uses external package definitions which allow you to control exactly how your application resources will be rolled up and vended to the client.

Default package configurations which load the TIBET library and your application code are provided with all TIBET project dna. Your application resources are typically defined in a package named after your project in ~app_cfg, your application config directory.

TIBET CLI commands such as the tibet tag command (which automatically adds your new tags to the current application's package configuration) help ease maintenance and allow you to leverage your package configuration data to drive linting, testing, etc.

See the tibet package and TIBET Loader documentation for more detail.

Types

How you define your types, attributes, and methods has a huge impact on your results.

Because it's such a critical part of the coding process and has such a dramatic impact on functionality TIBET takes control of type definition via strict APIs.

Use defineSubtype(), not Function instance.

Typical JavaScript development creates new types as instances of Function but Function-based types don't inherit behavior, restricting inheritance to instances only.

//  DON'T DO THIS IN TIBET
function Demo() { ... }

//  DON'T DO THIS EITHER
var Demo = function() { ... };

In TIBET you use the defineSubtype() call. Just pick the desired supertype object and message it. You know, like it was an object with behavior and stuff ;).

TP.lang.Object.defineSubtype('APP.demo.Demo');

TP.lang.Object is TIBET's root Object type so it's a common starting point. The TIBET library has a number of others specific to controllers, models, and tags.

Use defineAttribute(), not constructor/prototype properties.

Common JS practice defines properties on Function-based types by putting them inside the constructor function or via direct prototype assignment. Don't do that.

//  DON'T DO THIS IN TIBET
function Demo() {
    this.fluffy = null;
};

//  DON'T DO THIS EITHER
function Demo() {};
Demo.prototype.fluffy = null;

In TIBET use the defineAttribute method. This method creates a new attribute with appropriate object descriptor values on a targeted object.

//  DEFINE A TYPE ATTRIBUTE (INHERITED BY SUBTYPES)
APP.demo.Demo.Type.defineAttribute('fluffy');

//  DEFINE A LOCAL ATTRIBUTE (NOT INHERITED)
APP.demo.Demo.defineAttribute('fluffy');

//  DEFINE AN INSTANCE ATTRIBUTE
APP.demo.Demo.Inst.defineAttribute('fluffy');

While not shown above you can pass an optional default value as the second parameter or an object property descriptor. See the documentation on defineAttribute for more.

Use defineMethod(), not constructor/prototype properties.

In common JS development methods are just attributes whose values happen to be functions. As with attributes, don't define methods as properies in TIBET.

//  DON'T DO THIS IN TIBET
function Demo() {
    this.isFluffy = function() {
        ...
    };
};

//  DON'T DO THIS EITHER
function Demo() {};
Demo.prototype.isFluffy = function() {
    ...
};

In TIBET use the defineMethod method. This approach creates a new method with appropriate metadata and inheritance behavior.

//  DEFINE A TYPE (.Type) METHOD (INHERITED BY SUBTYPES)
APP.demo.Demo.Type.defineMethod('isFluffy', function() { ... });

//  DEFINE A LOCAL (No .Type or .Inst) METHOD (NOT INHERITED)
APP.demo.Demo.defineMethod('isFluffy', function() { ... });

//  DEFINE AN INSTANCE (.Inst) METHOD.
APP.demo.Demo.Inst.defineMethod('isFluffy', function() { ... });

As with attribute definition note that the object being messaged determines where the method ends up and how it is inherited. TIBET's OO system supports inheritance of both type and instance behavior similar to Ruby or Smalltalk.

Use init() not constructor function body.

If you're used to using functions as constructors you're probably used to putting constructor logic in the function body:

//  NOT IN TIBET!
var NewType = function(a, b) {
    ...
    this.fluff();
    ...
};

In TIBET override the instance (.Inst) method init instead:

TP.demo.Demo.Inst.defineMethod('init', function(a, b) {
    //  Invoke supertype version of 'init'.
    this.callNextMethod();

    //  Do the local instance initialization work.
    this.set('x', a);
    this.set('y', b);

    //  Return the new instance...or nothing to default.
});

Use callNextMethod(), not HardCodedType.method.call().

Many OO implementations in JavaScript require you to hard-code calls to supertype versions of overridden methods. In TIBET you simply call callNextMethod.

//  DON'T DO THIS
Demo.prototype.doX = function () {
    //  Maintenance headache ahead....
    SuperDemo.prototype.doX.call(this, arguments);
}

TIBET allows you to invoke "the next method" using callNextMethod:

TP.demo.Demo.Inst.defineMethod('doX', function(param) {
    this.callNextMethod();
    console.log('Y');
});

Note that:

  • We don't mention a supertype at any point in the overridden method.
  • We don't have to pass arguments to get param passed for us.
  • The "next method" might be an .Inst method if a local one exists.
  • The "next method" may traverse a variety of types due to trait resolution.

Use TP.isType(), not typeof === 'function'.

Using typeof === 'function' doesn't answer the real question since any function could be used as a constructor if you put new in front if it. While you might combine typeof with a name check to see if the function's name starts with an uppercase letter there's effectively no way in JavaScript to be sure a function instance is a type per se.

//  DON'T WASTE YOUR TIME
if (typeof(NewType) === 'function') {
  // Test the name for Titlecase...if it has one...useless.
}

In TIBET you should always use the TP.isType() call:

TP.isType(obj);

This method will return true if the obj serves as a TIBET or native type.

summary

In short, inheritance and reflection in JavaScript are either limited or broken depending on your viewpoint. Trying to use native reflection with TIBET's multi-track inheritance system only makes things worse. As you'll discover, TIBET provides Smalltalk/Ruby-level OO and reflection infrastructure, infrastructure you should use rather than native JS.

See the documentation for TIBET OO for more examples.

Instances

We've already seen some of the implications of using Function instances as types, and how they can negatively impact functionality and maintainability. When it comes to instances of those Function-based types there are similar issues that can arise.

Use construct(), not new, to create instances.

As mentioned earlier, JavaScript "types" are simply instances of Function. To create a new instance of a classic JS type you use the new keyword. That won't work in TIBET.

//  INCORRECT...throws an error.
var newInst = new App.demo.Demo();

Since TIBET types are not functions you can't use the new operator on a TIBET type. You have to message the type using construct() to create new instances.

//  CORRECT...newInst is a new instance with full OO capability.
var newInst = App.demo.Demo.construct();

Parameters you pass to construct are passed to the instance's init method.

var newInst = NewType.construct(3, false);

As an aside, prior to calling init on the instance, construct calls alloc on the type. You can override any of these methods, construct, alloc, and init, to control how your types create new instances (for example returning a singleton instance).

Use get() not this.x.

For the longest time JavaScript didn't support common getter/setter syntax. TIBET worked around this limitation by implementing set() and get() functions.

Of course, to really leverage get/set syntax, we couldn't leave them as simple wrappers for direct property access…that would be boring ;).

TIBET's get() function includes features like method/path lookup and access path traversal which make it much more powerful than direct property access.

//  DO THIS
var foo = newInst.get('foo');

//  NOT THIS!
var foo = newInst.foo;

TIBET's get call first checks the property name for path characters. You might have said get('foo.bar'), or get('foo[1,3]'), or any number of things which signify an 'access path' to TIBET. Access paths give you a way to limit boilerplate accessor creation.

Assuming you're just looking for a simple path TIBET uses reflection to see if a getter exists. If so it forwards to it. Using our previous foo slot as an example, as soon as we implement getFoo() TIBET's get() call will begin using it.

The get call will also try to find an "aspect path", essentially a mapping from a complex path to a simple name. For example, if you suddenly define foo as a path to lastname[0] TIBET will run that path in response to get('foo') calls. This feature lets you use a common get() API but remap the paths should your underlying data structure need to change (your JSON or XML shifts format).

If you were to use get('foo.bar') TIBET converts that into get('foo').get('bar') and runs the same lookup process for each step.

Each of these features is designed to improve maintenance by allowing you to remap how a particular property is accessed without changing the calling code.

Use set() not this.x = y.

The other half of encapsulation is protecting instance properties from being set to inappropriate or incorrect values. The set() call gives you a way to do that and, like TIBET's get() call, it performs method/path lookup and can process access paths to accomplish its task of updating object state.

//  DO THIS:
newInst.set('foo', 2);

//  NOT THIS:
newInst.foo = 2;    //  Change notification?

As with TIBET's get() call you can see how this is very similar on the surface to the functionality of native setters, however since it's a method you get inherited behavior that can be very compelling…behavior like change notification.

Automatic Change Notification

As you may have read elsewhere, TIBET is a heavily event-driven system. One of the key aspects of signaling in TIBET is change notification. Through the set() call most objects can serve as a 'model' in an MVC pattern. This is one reason TIBET doesn't have a special 'Model' type, virtually all objects can be models in TIBET.

When you set() a value TIBET checks to ensure the value is actually changing. If so it automatically creates a Change signal of the proper form and triggers it.

The thing about Change notification is that sometimes you want to set multiple values in a sequence and only trigger Change notification when you're done with them all. You need control. You need a flag. Which means you can't use a native setter since you can't pass anything but a value to one of them. Having a real method is better.

//  THIS:
//  It gives us a way to have a flag to tell Change notification not to fire -
//  the 'false' third parameter.
newInst.set('foo', 2, false);

//  NOT THIS:
newInst.foo = 2;  //    Where does the other parameter go?

If you need to set a lot of values in sequence and don't want to have to pass 'false' for the third parameter each time, you can temporarily switch off change notification from just that object via the .shouldSignalChange() method that all TIBET-based objects inherit:

//  Turn it off.
newInst.shouldSignalChange(false);

//  Do a lot of 'set's
newInst.set('foo', 2);
newInst.set('bar', 3);
newInst.set('baz', 4);
...
//  Turn it back on and signal manually.
newInst.shouldSignalChange(true);
newInst.changed();

Use TP.isKindOf(), not instanceof.

Because TIBET types aren't instances of Function the standard JavaScript 'reflection' keywords don't work on instances of TIBET types.

//  DON'T TRY THIS...it won't work
if (obj instanceof NewType) { ... };

TIBET's TP.isKindOf() call is used for full ancestor-chain checking. The object can be anywhere below the type:

TP.isKindOf(obj, TypeOrTypeName);

You can use TP.isKindOf() on native objects and TIBET will do its best to provide an accurate answer.

Use TP.isMemberOf(), not constructor checks.

As with instanceof checks, constructor checks don't work on most TIBET objects. If you know TIBET internals you might make it work -- until we change them.

//  DON'T DO THIS...it won't work.
if (obj.constructor === NewType) { ... };

TIBET's isMemberOf() is a 'direct instance' check meaning it returns true only if the instance was built by invoking construct on the type in question.

TP.isMemberOf(obj, TypeOrTypeName);

Like TP.isKindOf(), you can use TP.isMemberOf() on native JS objects as well.

Use TP.tname(), not typeof.

The native JavaScript typeof operator doesn't do what you want if what you want is to know what type of object you have in OO terms. Instead it tells you what the primitive JavaScript type is: 'object', 'number', 'string', 'boolean', or 'function'.

That's not what we want 95% of the time and the other 5% is questionable practice.

//  INCORRECT
typeof(TP.sys.getApplication());    //   => 'object' => useless.

If you must know the name of the current type you can ask via TP.tname().

//  CORRECT
TP.tname(TP.sys.getApplication());  //   => 'APP.demo.Application'.

Note that if you use TP.tname() on a TIBET type you'll get back an answer that might surprise you. TIBET types are instances of a special set of objects called metatypes. These metatype objects are arranged in a parallel hierarchy to the standard type hierarchy and found under the TP.meta and APP.meta namespaces.

If we use TP.tname() on a TIBET type we'll get back the name of the metatype:

TP.tname(TP.demo.Demo); //     => TP.meta.demo.Demo

All of this may seem strange/wrong until you recall that asking a native type (Array for example) what its type is will result in 'Function', a similar response that highlights that native types aren't instances of themselves either.

TP.tname(Array); //     => Function

If you want to know the name of the current type just use TP.name() instead:

TP.name(TP.demo.Demo); //  => TP.demo.Demo

As with virtually all of TIBET's reflection methods you can use TP.tname() on native JS objects and TIBET will try to 'do the right thing'.

Collections

One of the more unfortunate oversights in the original design of JavaScript is the absence of a true Hash or Dictionary type. As a result it's common practice to use raw object instances as dictionaries, a practice that can create numerous problems.

Using a raw object as a dictionary 'works' in the same rough fashion that copying slots onto objects 'works' for inheritance. It seems ok at the time, but it severely limits you and creates a lot of landmines to dodge at scale.

Use TP.hc(), not {}, for dictionaries.

The TP.hc() function creates an instance of TP.lang.Hash, a true dictionary type whose keys are not mixed with those of Object.prototype. With a TP.lang.Hash you don't need to use hasOwnProperty to be sure of your key/value pairs.

var dict = TP.hc('a', 1, 'b', 2);

//  OR:

var dict = TP.hc({ a: 1, b: 2 });

By using TP.hc() you can message a true Hash type via a number of other useful methods to get keys, values, pairs, counts, etc. Note that TP.hc() is just a shorthand for TP.lang.Hash.construct()… you didn't want to do all that typing anyway, right? ;-)

Parameter Lists

A common practice in JavaScript is to pass raw objects as parameter lists. Unfortunately, while most JS developers have learned that you need to use hasOwnProperty() when iterating, few use it with parameters.

//  Assume somewhere this happened...
Object.prototype.remove = function() { ... };

//  OUCH:
var bar = function(params) {
    if (params.remove) {
        //  Delete stuff...
    }
};

bar({x: 1});
...stuff deletes thanks to 'remove' being a real slot...

You're certainly free to argue extending Object.prototype is a horrible idea but the simple fact is the language allows it. Better to write defensive code than to spend hours tracking down some obscure bug.

//  BETTER - TIBET only exposes 'remove' as a key in the hash.
var bar = function(params) {
    if (params.at('remove')) {
        //  Delete stuff...
    }
};

bar(TP.hc('x', 1));

We'll make one more argument for using TP.hc() rather than {} for dictionaries. It's future-proof/future-enhancing.

If you write all your code using {} then when new types such as Set, Map, WeakMap, etc. arrive your existing code won't leverage them. If you use TP.hc() it's highly likely that when a native hash-like type arrives it will form the underlying implementation and your TIBET hash objects will suddenly get a performance boost.

Encapsulation rocks, whether it's at the property or object level.

Use perform(), not for/in, to iterate.

The for/in construct is a great way to iterate over an object, provided you verify all the keys are local keys. Unfortunately too many developers overlook this step.

var obj = {};

//  WORKS, but too many developers leave off the hasOwnProperty test.
for (i in obj) {
    if (obj.hasOwnProperty(i)) {
        //  do stuff with i
    }
}

Unfortunately, a problem comes up when you try to use for/in on an Array, something that we've seen numerous times:

var arr = [1, 2, 3];

for (i in arr) {
    //  NOT what most developers expect. Only the 'indexes' if Array.prototype
    //  hasn't been modified.
}

With the advent of native iterators such as forEach, map, filter, some, etc. you can avoid this problem by eliminating use of for/in on Array instances. Unfortunately, those methods don't currently work on Object instances.

TIBET provides iterators that work on all true collection types (see the discussion on TP.core.Hash). These iterators come in both internal (perform, select, detect, collect, reject, injectInto) and external (collection.getIterator()) form.

var arr,
    hash;

arr = [1, 2, 3];
hash = TP.hc({a: 1, b: 2, c: 3});

//  Think forEach
hash.perform(function(item, index) {
    //  item here is an ordered pair (['a', 1], ['b', 2], and so on)
});

//  Works only on indexes, regardless of Array.prototype extensions.
arr.perform(function(item, index) {
    //  item here is content of slot (1, 2, 3, and so on)
}

Use remove, not delete.

The delete operator shouldn't be used with TIBET collections. And you're not going to be using raw objects as hashes, right? ;) Since TIBET's collections manage their data independently under the covers the delete operation will fail.

var dict = TP.hc('age', 45, 'fname', 'Joe', ...);

//  INCORRECT...dict.age won't exist anyway.
delete(dict.age);

//  CORRECT
dict.removeKey('age');

In addition to handling deletion of a key, TIBET's hash type can also remove by value, remove all values, remove only particular key/value pairs, etc. It's a true collection, not an Object pressed into servitude as a poor substitute.

Comparisons

Object comparisons in JavaScript are subject to a number of unpredictable quirks. Some are a natural side-effect of how JavaScript treats certain values or performs type conversion. But the real point is you need clear semantics.

Use TP.isValid() or TP.notValid() for boolean checks.

Most JavaScript source is heavily sprinkled with conditionals which rely on the semantics of JavaScript Boolean type conversions. For the most part these tests work but they can often disguide hard-to-find bugs. We recommend using clear test functions instead.

//  YES!
if (TP.isValid(params)) {
    //  Use params...
}

//  NO!
if (params) {
}

The TP.isValid call ensures the value is not null, not undefined, and not NaN. The result is your semantics are consistent and predictable.

Use TP.isNull() or TP.notNull.

Checking for null values, or the absence of a null, is a common operation, one common enough that we found it useful to create clear test functions.

Use TP.isDefined or TP.notDefined().

As with null, checking for undefined (or the absence of it) is clearer when using clearly named test functions.

Use TP.equal(), not ==.

JavaScript's == operator does type conversion and as a result is subject to a number of quirks and unexpected results. As a result it's often recommended you replace == with === but that's not always a semantically equivalent test, identity isn't equality.

To perform a true "equality" check TIBET offers a the TP.equal method which leverages reflection and type-specialized methods to perform value comparsions.

//  CORRECT
if (TP.equal(a, b)) { ... };

//  INCORRECT
if (a == b) { ... };

TIBET's TP.equal() function leverages the types of the objects as well as knowledge of various quirks to provide a semantically clean answer to the question of whether two objects are 'equal'. As new types are added to the system methods invoked by 'equal' can ensure they compare appropriately.

Consider TP.identical() rather than ===.

For native object comparisons === is usually acceptable since an identity check is pretty straightforward. For locations in your code where you may want to spoof identity however, such as when using a proxy, the TP.identical call can be useful.

//  CORRECT...you can spoof identity as needed for testing, proxies, etc.
if (TP.identical(a, b)) { ... };

//  MOSTLY CORRECT...but not spoofable.
if (a === b) { ... };

summary

TIBET is the result of almost 20 years of hands-on experience building desktop-class applications in JavaScript; experience that led us to encapsulate many core features of the language behind APIs we can rely on to grant a higher level of control.

This document has presented the key differences you'll encounter in writing code for the TIBET platform relative to writing standard JavaScript. Almost all fall into two basic camps: always use methods and avoid using raw objects as collections.

Keywords considered harmful. Avoid if possible.

As you may have noticed, TIBET essentially removes new, typeof, instanceof, for/in, and delete from your JavaScript lexicon. Don't use them in TIBET.

The first three aren't useful because they don't work well with TIBET's powerful combination of multi-track OO and composition via Traits, the latter two fall short because they don't work consistently or properly with a true dictionary type.

ES6 keywords for types (class, static, extends) don't appear to add much value to TIBET code. ES6 classes don't provide the OO + Traits power of TIBET so while you're free to use them the resulting classes won't benefit from any of TIBET's advances and will continue to exhibit the same bugs/limitations of prior versions of the language.

ES6 module keywords like import and export seem useful on the surface but it's not clear how well they'll work in practice. For example, things you don't export are things you can't test. We'd prefer to test everything and let a lint-like process check for things like violations of encapsulation.

Prefer methods over operators where possible.

The quirks inherent in JavaScript's implicit and explicit type conversions lead to confusion and hard-to-track bugs. We recommend focusing on semantic methods that encapsulate that logic and give you the means to adapt to newly discovered quicks with a minimum of pain, methods like TP.isValid(), TP.isType(), etc.

Raw objects aren't dictionaries. Don't use them as such.

There are times when, yes, it'd be nice to have sugar for hash as nice as {}.

Unfortunately overloading poor Object with root-of-hierarchy and 'hash' responsibilities turns out to be a stellar way to end up having it perform both jobs poorly.

Since our priority is on the overall platform and not sugar for a single type we went with providing a real Hash type and suitable collection APIs.

Coding Standards

Our simple recommendation is "have one"…and enforce it with tooling.

For TIBET's version see TIBET Coding Standards.