DEV Community

Marko V
Marko V

Posted on

I.can.has("cheezeburger")

When I was a youngin', there was a site called icanhas.cheezeburger.com...

I have been eyeing the mocha/chai/expect framework just for the sake of understanding/learning how it works in relation to the javascript internals to get that kind of behavior. And I was reminded of the old website, icanhas.cheezeburger.com when I saw someone name their check for undefined or null in a nested object to "has". So... I set on to improve on that code (because it was relying on implicit checks) as well as making my own funny version of it.

May I introduce the "I" function.

var food = { hamburger: false } 
I(food).can.has("cheezeburger");

So this is using ES2015 style code, in that it's not a class that you strictly need to new up, it also uses Object.defineProperty to make getters/setters and it's compatible with IE11. However it also uses an anti-pattern which really isn't OK strictly speaking, since there's a getter that modifies state, but the way that this function is used is also not a common/natural pattern in javascript world, only in unit-tests, so you could argue that it's OK.

So let's walk through it.

The first thing is that the function needs to keep to a fluent-like API where you can just keep on chaining methods/functions/properties from it because they all return a modified version of the function. So that's why a getter used in this way where it modifies state is considered OK because of the fluent-like api but using the getter as a necessary evil to provide that type of functionality that otherwise would have required a function call/invocation to be able to provide it.

This means that every property needs to return an instance of this unless it's a terminating expression/word. Like ".is.undefined" or similar. Now undefined was unfortunately a protected word. I was not able to override it in ES2015 style code, so I would have to dig a little deeper to see how mocha/chai does it

After initial checks it seems that it's using an Object internal function to override the default for a property if it already exists. So I'm not gonna do that for this.

So non-terminal getters always return the same instance is how we chain them without using a function-call. Since a property-getter is accessed just like any javascript object property

var normalObj = { prop: "value" }
> normalObj.prop
"value"

var objWithGetter = {}
Object.defineProperty(objWithGetter, 'prop', { get: function() { return "value"; }});

> objWithGetter.prop
"value"

Now it may not be apparent why this is an advantage, but it sort of gives you access to a function that can perform evaluation at access-time rather than pre-computing a value to a normal property. This is the key-ingredient for this type of functionality.

So what properties do we want? Well.... "I" is taken care of as the function name. We could choose to make it a static-like function that takes no parameters. But then we'd have to instantiate itself much later in the chain and there's no real advantage to that. So true to the mocha/chai pattern, we'll add the object we're working with when calling the function. I(food).

can, as a property really doesn't add anything to the chain. It could potentially add a flag saying that following this statement in the chain the end-property has to be a function, or something, it's a stretch of the mind to attribute the name "can" as a moniker for a function. But for me, it doesnt add anything, so all it does, is just return the same instance untouched.

has, as a property is just a function that checks for the existence of something and returns true/false. The function itself is based off of this SO article with a few improvements.

And then it would be a very small function/class if we ended it there. So I extended it with a not as well as null or undef checks, to make it slightly more useful.

not, as a property actually modifies state of the instance before returning an instance reference. It flips an internal boolean when used. So technically speaking you could chain it indefinitely and it would toggle it on/off all the time.

null, as a property just checks if the initial object provided is equal to null

undef, as a property checks if the intial object provided is equal to undefined

So with these properties, null, undef and has are the terminators as they dont return an instance of the function/class when they return a value. Ofc you could make it so that they do and you add a value property that contains the end-result. So that you always have an instance to work with. But I guess that's just a flavor of choice.

Trying to reconcile not instantiating/newing up a class/function but still maintain an instance for the sake of syntax, was a bit trickier than I wanted, so I ended up just making sure that we're not overwriting "this." whenever the function was called. This in the end, became cleaner than doing it the "proper" way.

function I(obj) {
    this._obj = obj;
    this._negate = false;
    if(!this.can) Object.defineProperty(this, 'can', { get: function() { return this; } });
    if(!this.is) Object.defineProperty(this, 'is', { get: function() { return this; } });
    if(!this.not) Object.defineProperty(this, 'not', { get: function() { this._negate = !this._negate; return this; } });
    function everyProp(currObj) {
        return function(prop) {
            if( typeof currObj === "undefined" || currObj === null || !(prop in currObj))
                return false;
            currObj = currObj[prop];
            return true;
        };
    }
    this.has = function(key) {
        var tObj = this._obj;
        const returnObj = key.split(".").every(everyProp(tObj));
        if (this._negate) return !returnObj;
        return returnObj;
    };
    if(!this.undef) Object.defineProperty(this, 'undef', { get: function() { 
        if(this._negate) return typeof this._obj !== "undefined";
        return typeof this._obj === "undefined"; 
    }});
    if(!this.null) Object.defineProperty(this, 'null', { get: function() { 
        if(this._negate) return this._obj !== null;
        return this._obj === null; 
    }});
    return this;
}

var food = { hamburger: false } 
console.log(I(food).can.not.not.has("cheezeburger"));

So can I? node has.js

false

FeelsBadMan

Top comments (2)

Collapse
 
omnoms profile image
Marko V

Note; This is play-code. Not to be used in production. This particular code for instance uses a singleton pattern which means that you will spill values from one usage to the next. To get rid of that you need to implement the class-pattern where you new-up your own instance and dont get spilled data.

Collapse
 
omnoms profile image
Marko V • Edited

For the sake of brevity, here's a class-style variation

const I = (function () {
    function I(obj) {
        this._obj = obj;
        this._negate = false;
    }
    function everyProp(currObj) {
        return function(prop) {
            if( typeof currObj === "undefined" || currObj === null || !(prop in currObj))
                return false;
            currObj = currObj[prop];
            return true;
        };
    }
    I.prototype.has = function(key) {
        var tObj = this._obj;
        const returnObj = key.split(".").every(everyProp(tObj));
        if (this._negate) return !returnObj;
        return returnObj;
    };
    return I;
}());
Object.defineProperty(I.prototype, 'can', { get: function() { return this; } });
Object.defineProperty(I.prototype, 'is', { get: function() { return this; } });
Object.defineProperty(I.prototype, 'not', { get: function() { this._negate = !this._negate; return this; } });
Object.defineProperty(I.prototype, 'undef', { get: function() { 
    if(this._negate) return typeof this._obj !== "undefined";
    return typeof this._obj === "undefined"; 
}});
Object.defineProperty(I.prototype, 'null', { get: function() { 
    if(this._negate) return this._obj !== null;
    return this._obj === null; 
}});

var food = { hamburger: true, bun: { top: "wholemeal"} } ;
console.log(new I(food).can.not.has("bun.top"));
console.log(new I(food).can.has("cheezeburger"));

Now if you still want to hide the new-ing up of an instance, you can wrap it.

If you name your IIFE class to something like "myutil" and then make the global function "I" return a new instance of myutil using the same provided parameters as the function accepts.

// update all I.prototype to myutil.prototype
const myutil = (function () {
    function myutil(obj) {
        this._obj = obj;
        this._negate = false;
    }
   ...
   return myutil;
}())
// Update all Object.defineProperty calls on I.prototype to myutil.prototype
...

function I(obj) {
    return new myutil(obj);
}