5

Disclaimer

  • This thread is supposed to serve as a help for other people encountering similar problems as well as checking whether there are better solutions. I will attach my own solution, but ideas and improvements (besides making it more generic) are welcome.
  • I know that generally, extending the built-in objects is a bad idea. So an assumption for this thread is that there is a good reason and no way around it.

Scenario

As a developer, I want to add a method someMethod to all Javascript objects, wherein the implementation is different for Object, Number and String.

I want the solution to meet the following acceptance criteria:

  • A) The solution works in a browser
    • A1) The solution works in strict mode in case the script is used within a strict context
    • A2) The solution works in non-strict mode because 'use strict'; will be removed during compression in, e.g., the YUI Compressor[1]
  • B) The solution works in node.js
    • B1) The solution works in strict mode (reason see A1)
    • B2) The solution works in non-strict mode for the same reason as in B2, plus strict mode in node.js cannot be activated on function level[2]
  • C) I want other objects to be allowed to override this method
  • D) If possible, I want to have control over whether or not the method shows up in a for .. in loop to avoid conflicts with other libraries
  • E) The solution shall actually modify the prototypes.

[1] Minfication removes strict directives
[2] Any way to force strict mode in node?

Community
  • 1
  • 1
Ingo Bürk
  • 19,263
  • 6
  • 66
  • 100

2 Answers2

3

My own solution

While trying to figure this out, I have encountered a few problems causing one or another acceptance criterium to break (e.g. a problem described in [1]). After some time I came up with the following solution which seems to work for me. This can be written in a more generic way, of course.

(function () {
    'use strict';

    var methodName = 'someMethod',
        /** Sample method implementations */
        __someMethod = {
            'object': function () {
                var _this = this.valueOf();

                return ['Object'].concat( Array.prototype.slice.call( arguments ) );
            },

            'number': function () {
                var _this = this.valueOf();

                return ['Number'].concat( Array.prototype.slice.call( arguments ) );
            },

            'string': function () {
                var _this = this.valueOf();

                return ['String'].concat( Array.prototype.slice.call( arguments ) );
            },

            'boolean': function () {
                var _this = this.valueOf();

                return ['Boolean', _this];
            }
        };

    if( Object.defineProperty ) {
        Object.defineProperty( Number.prototype, methodName, {
            value: __someMethod['number'],
            writable: true
        } );

        Object.defineProperty( String.prototype, methodName, {
            value: __someMethod['string'],
            writable: true
        } );

        Object.defineProperty( Boolean.prototype, methodName, {
            value: __someMethod['boolean'],
            writable: true
        } );

        Object.defineProperty( Object.prototype, methodName, {
            value: __someMethod['object'],
            writable: true
        } );
    } else {
        Number.prototype[methodName] = __someMethod['number'];
        String.prototype[methodName] = __someMethod['string'];
        Boolean.prototype[methodName] = __someMethod['boolean'];
        Object.prototype[methodName] = __someMethod['object'];
    }
})(); 

Edit: I updated the solution to add the solution for the problem mentioned in [1]. Namely it's the line (e.g.) var _this = this.valueOf();. The reason for this becomes clear if using

'number': function (other) {
    return this === other;
}

In this case, you will get

var someNumber = 42;
console.log( someNumber.someMethod( 42 ) ); // false

This, of course, isn't what we'd want (again, the reason is stated in [1]). So you should use _this instead of this:

'number': function (other) {
    var _this = this.valueOf();
    return _this === other;
}

// ...

var someNumber = 42;
console.log( someNumber.someMethod( 42 ) ); // true

[1] Why does `typeof this` return "object"?

Community
  • 1
  • 1
Ingo Bürk
  • 19,263
  • 6
  • 66
  • 100
  • I'm not understanding the point. You're simply adding the method to the `.prototype`, and making the property non-enumerable and non-configurable if possible. What am I missing? –  Aug 22 '13 at 20:45
  • I'm sorry – I did miss one thing; namely what was mentioned in [1]. Other than that, no you're not missing anything. But I had many versions of something like this before that was broken in the browser or nodejs due to all the possibilites mentioned. – Ingo Bürk Aug 22 '13 at 20:48
  • 1
    Huh, OK. In any case, is there a reason you're adding a wrapping function when `Object.defineProperty` isn't supported, instead of just doing `Number.prototype[methodName] = __someMethod['number']` ? –  Aug 22 '13 at 20:51
  • Yes and no. There used to be one, built on a wrong believe for a second and never to be changed again (that `this` would refer to the wrong object). I'll adapt the solution (see, this is why I posted it here :) ). – Ingo Bürk Aug 22 '13 at 20:53
  • Trying to do the same for `Boolean.prototype` I have found that the approach needs to be changed a little. Instead of doing `var _this = Number( this )`, which won't work with `Boolean`, `var _this = this.valueOf()` needs to be used. I'll update the answer. – Ingo Bürk Aug 23 '13 at 13:26
1

Creating a wrapper object (note this is just an example, it is not very robust):

var $ = (function(){
  function $(obj){
    if(!(this instanceof $))
        return new $(obj);

    this.method = function(method){
        var objtype = typeof obj;
        var methodName = method + objtype[0].toUpperCase() + objtype.substr(1);
        typeof _$[methodName] == 'function' && _$[methodName].call(obj);
    }
  }

  var _$ = {};

  _$.formatNumber = function(){
    console.log('Formatting number: ' + this);
  }

  _$.formatString = function(){
    console.log('Formatting str: "' + this + '"');
  }

  _$.formatObject = function(){
    console.log('Formatting object: ');
    console.log(JSON.stringify(this));
  }

  return $;
})();

Usage:

var num = 5;
var str = 'test';
var obj = {num: num, str: str};

var $num = $(num);
$num.method('format');

$(str).method('format');
$(obj).method('format');

Demo

Paul
  • 139,544
  • 27
  • 275
  • 264
  • Yes, it works – but it misses the point of the question: To modify the prototype. I guess I should've been more clear and add it as a criterium, even though it was sort of in the assumptions. (edit: done). – Ingo Bürk Aug 22 '13 at 21:02