3

Just a small Prototypical Inheritance question
Recently I was trying to create a custom Method jut like e.g: JS's .toUpperCase() and other methods... that uses the this reference from the prefixed object.

And it works nice (stupid usless example) :

Object.prototype.customMethod = function(){
    return this.toLowerCase() ; 
};

And can be used like:

// access some object and get a key value
member.name; // JOHN
// use custom Method
member.name.customMethod(); // john

The issue is that seems like the Method .customMethod() inherits globally every Object.
How to make it less intrusive and be referenced to the prefixed object only? or at all?

Here is an example: http://jsbin.com/evaneg/2/edit

// CREATE OBJECT
var obj = { "text" : "Hi" };

// TEST OBJECT KEY VALUE
alert( "OBJECT TEXT: "+ obj.text );     // Hi


// CREATE CUSTOM METHOD ( just like e.g. JS's .toUpperCase() method )
Object.prototype.addText = function(){ 
  return this+' there!';  
};

// USE CUSTOM .addText() METHOD
alert( "OBJECT TEXT+method: "+obj.text.addText() ); // Hi there! // wow, the method works!


for(var key in obj){
  alert( 'obj KEYS: '+ key ); // text // addText
}

// hmm... addText method as obj Key ???
// ok let's try with a new one...

var foobee = { "foo" : "bee" };

for(var key in foobee){
  alert( 'foobee KEYS: '+ key ); // foo // addText
}

// addText  ...again... but why?

I read also this http://javascript.crockford.com/prototypal.html and lots of other similar things here on SOverflow but most of them focus on the use of creating a new F( arguments ) that use specific arguments, which is not my case. Thanks for any explanation

Boaz
  • 19,892
  • 8
  • 62
  • 70
Ginnani
  • 325
  • 1
  • 5
  • 19
  • Sorry for the jQuery tag, it was just to attract more gurus :) – Ginnani Jan 27 '13 at 22:17
  • 2
    playing with fire tampering with `Object`. What if in future a new property is added that collides with yours? – charlietfl Jan 27 '13 at 22:22
  • You would be using a Constructor pattern. The `new F()` which you mentioned to not be your case is actually your exact use case. It creates a new instanced object which inherits the prototype of `F`. For simplicity on ES5-compatible browsers, you can also use `Object.create`. – Fabrício Matté Jan 27 '13 at 22:22
  • @charlietfl Exactly I'm aware of that, but I cannot understand from what BRILLIANTLY Fabrício said how to do it. – Ginnani Jan 27 '13 at 22:24
  • @FabrícioMatté I'm so lost in that matter, and I know that what you said leads to a success, but I'm just unable to make a working example that will not recall `addText` for every single object I use – Ginnani Jan 27 '13 at 22:26
  • I guess Crockford has a simpler page explaining how constructors work. Also, see `#4` in [this answer](http://stackoverflow.com/a/504888/1331430)'s code – Fabrício Matté Jan 27 '13 at 22:27
  • @FabrícioMatté I've seen that answer before. I'm just unable to implement the `#4` into a working demo. – Ginnani Jan 27 '13 at 22:34
  • I thought there would be answers by now. Alright I'll try to put together an answer then. – Fabrício Matté Jan 27 '13 at 22:38

2 Answers2

4

You don't have to add the method to every object -- only the kind of object which you are working with. If it is a string method, you can add it to String.prototype and have it defined on all strings.

var obj = { "text" : "Hi" };

// We only want to add this method to strings.
String.prototype.addText = function(){ 
  return this+' there!';  
};

alert("OBJECT TEXT+method: " + obj.text.addText());

Note that this does make methods enumerable, which means they show up in a for..in loop.


Object.defineProperty

If you can only care about supporting browsers since 2010 (no IE8), then I highly recommend using Object.defineProperty to define properties so that they won't be enumerable:

var obj = { foo: 'bar' };

// Extending all objects this way is likely to break other scripts...
Object.prototype.methodA = function() { return 'A'; };

// Instead you can do extensions this way...
Object.defineProperty(Object.prototype, 'methodB', {
    value: function() { return 'B'; },
    // Prevent the method from showing up in for..in
    enumerable: false,
    // Allow overwriting this method.
    writable: true,
    // Allow reconfiguring this method.
    configurable: true
});

for (var propertyName in obj) {
    console.log(propertyName);
    // Logs: "foo" and "methodA" but not "methodB"
}

The code above logs the "foo" property and the "methodA" property but it doesn't log the "methodB" property because we defined it as non-enumerable. You'll also notice built-in methods like "toString", "valueOf", "hasOwnProperty", etc also don't show up. This is because they are also defined as non-enumerable.

This way other scripts will be allowed to use for..in freely and everything should work as expected.


Going back to specific built-ins

We can define non-enumerable methods on specific types of objects with Object.defineProperty as well. For instance, the following adds a contains method to all arrays which will return true if the array contains a value and false if it doesn't:

Object.defineProperty(Array.prototype, 'contains', {
    value: (function() {
        // We want to store the `indexOf` method so that we can call
        // it as a function. This is called uncurrying `this`.
        // It's useful for ensuring integrity, but I'm mainly using
        // it here so that we can also call it on objects which aren't
        // true Arrays.
        var indexOf = Function.prototype.call.bind(Array.prototype.indexOf);
        return function(value) {
            if (this == null)
                throw new TypeError('Cannot be called on null or undefined.');
            return !!~indexOf(this, value);
        }
    })(),
    enumerable: false,
    writable: true,
    configurable: true
});

var colors = [ 'red', 'green', 'blue', 'orange' ];

console.log(colors.contains('green'));  // => true
console.log(colors.contains('purple')); // => false

Note that since we defined this method on Array.prototype, it is only available on arrays. It is not available on other objects. However, in the spirit of other array methods, it is written with enough generality that it can be called on array-like objects:

function foo() {
    Array.prototype.contains.call(arguments, 5);
}

console.log(foo(1, 2, 3, 4, 5));  // => true
console.log(foo(6, 7, 8, 9, 10)); // => false

Calling contains on arguments works above, event though arguments isn't a true array.


Simplifying the Boilerplate

Using Object.defineProperty provides a lot of power, but it also requires a lot of extra code which most people would rather not type all the time. That was understood when the ECMAScript committee defined the function, but they assumed people could write helper functions to make the code cleaner. Keep this in mind. For example, you can always do something like the following:

var define = (function() {
    // Let's inherit from null so that we can protect against weird situations
    // like Object.prototype.get = function() { };
    // See: https://mail.mozilla.org/pipermail/es-discuss/2012-November/026705.html
    var desc = Object.create(null);
    desc.enumerable = false;
    desc.writable = true;
    desc.configurable = true;
    return function define(constructor, name, value) {
        if (typeof constructor != 'function'
            || !('prototype' in constructor))
            throw new TypeError('Constructor expected.');
        desc.value = value;
        Object.defineProperty(constructor.prototype, name, desc);
    }
})();

Then you can do:

define(String, 'addText', function() {
    return this + ' there!';
});

console.log('Hi'.addText()); // => "Hi there!"

There are even some small libraries which have been developed to help with some of this stuff. Check out Andrea Giammarchi's redefine.js as an example.


Caution

The only real caution here is that if you add your own methods to built-ins, it's possible to have name clashes with (a) future versions of JavaScript or (b) other scripts. (The name-clash problem is being resolved in the next version of JavaScript with symbols.) Many would argue that this is a big enough problem that you shouldn't ever modify built-ins but should stick to modifying only the stuff you "own" -- things you created with your own constructors. I would tend to agree in some (or many) situations, but I think for your own personal use and learning, playing around with built-in prototypes can be a very helpful and fun thing to do.

Check out this article on weighing some of the costs of modifying built-ins for a more detailed description of possible drawbacks: http://perfectionkills.com/extending-built-in-native-objects-evil-or-not/ Note that this article is a little dated (1 1/2 years) and one of his major complaints is the enumerability thing, which is possible to overcome with Object.defineProperty in all modern browsers. As I said, the other major problem (name-clashes) will be solved in the next version of JavaScript. Things are getting better!

Nathan Wall
  • 10,530
  • 4
  • 24
  • 47
  • +1 Your answer is incredibly accurate and explanatory. Let me please read it again from top to bottom again. And I have yet a Qestion if it's in your domain: going to support IE8, IE7 ... how would one get into that problem? – Ginnani Jan 27 '13 at 23:21
  • You can use a [ES5 Shim](https://github.com/kriskowal/es5-shim) to make `Object.defineProperty` "work" in old IE but those will fail silently to set the "writable", "enumerable" and "configurable" properties, that is, they will still appear in the `for ... in` loop. – Fabrício Matté Jan 27 '13 at 23:25
  • Sorry, Ginnani, but unfortunately IE7 and 8 don't support `Object.defineProperty` and have no way to define (arbitrary) properties as non-enumerable. In light of this, if you need IE7 or 8 support, you can either just live with breaking other scripts which use `for..in` or just not modify built-ins. Neither prospect is that exciting. Fortunately there is a better web coming. IE8 is losing market share rapidly, and [Google even dropped support for it in November](http://www.computerworld.com/s/article/9231316/Google_to_drop_support_for_IE8_on_Nov._15). – Nathan Wall Jan 27 '13 at 23:28
  • guys... I have no other words but THANKS! I'm just so frustrated right now that I'll leave the question opened for some days, I'm scared not to get more answers cause I really want to understand that matter in depth. – Ginnani Jan 27 '13 at 23:28
  • I guess Nathan did a pretty good job explaining all these approaches (nevertheless the `Array.prototype` examples look a bit unrelated to the question). @Ginnani I guess if you specify a little more about what kind of behavior you want you're more likely to get other/better answers. Say, if you want ready-made inherited methods, you will need to modify a prototype. If you're working with strings, you will need to add to the String prototype. If you're working with objects, you can create a custom Constructor so you don't mess with the native prototypes. – Fabrício Matté Jan 27 '13 at 23:34
  • I'm just impressed how unflexible for a "standard" point of view JS can be. I think that the main difference from other languages that uses classes now comes to a point of extreme difference. – Ginnani Jan 27 '13 at 23:35
  • @Nathan, if you continue to add text to this answer, I'll contact the creators of SO to give me more `+1`s :) amazed by this one. The best answer I got ever – Ginnani Jan 27 '13 at 23:37
  • 2
    @Ginnani Constructors are more flexible than Classes from a point of view - you can `apply` an instance's method on another object, for instance. ES6 will have real classes as far as I remember. And the language is pretty flexible tbh, as long as you can bear with the side effects. `=]` – Fabrício Matté Jan 27 '13 at 23:38
  • @FabrícioMatté can't wait ECMA become really Scriptable :) – Ginnani Jan 27 '13 at 23:39
0

You should use new F() as well. you just need to define F. Lets call F 'Name' though so its easier to identify.

var Name = function(){
    var newName = {
        text: 'Hello',
        customMethod: function(){
            this.text = this.text.toUpperCase();
        }
    }
    return newName;
}

var Member = function(){
    var newMember = {
        name = new Name(),
        customMethod: function(){
        this.name.customMethod(); // make a member invoke upperCase on name (or any number of vars)
        }
    }
    return newMember;
}

var member = new Member();  //now when you run new Member, it runs the function above. and automatically makes your object with it's properties and methods.

console.log(member.name.text); //Hello!
member.customMethod();
// or
member.name.customMethod();
console.log(member.name.text); //HELLO!
Patrick Gunderson
  • 3,263
  • 17
  • 28
  • Thanks, Ok, let's say you have an Object like `member.name`. How can I use your example like: `member.name.text()` – Ginnani Jan 27 '13 at 22:43
  • you have to make a member object that contains a name object. I updated the example to show this – Patrick Gunderson Jan 27 '13 at 22:51
  • @Ginnani is `name` a string of text? That won't be easy to assign a method to it without messing with the String prototype. You can pass it as a parameter to a generic method though. – Fabrício Matté Jan 27 '13 at 22:51
  • @FabrícioMatté Yes, as you can clearly see from my examples I'm accessing the object using dot notation to get the value. The question is actually simpe. How to create a custom JS Method. – Ginnani Jan 27 '13 at 22:54
  • @Ginnani Not really simple when you're trying to call a method on a string. Does `foo` always have the same name or you want to apply it to a dynamic naming/more than a single string property? – Fabrício Matté Jan 27 '13 at 22:59
  • 1
    @Ginnani I updated my example again to use the customMethod() to act on name. – Patrick Gunderson Jan 27 '13 at 23:03
  • @FabrícioMatté simply. Let's say I'm accessing an object property like `user.date // 20-01-1990` and by doing simply `user.date.toAge()` I can get `// 23 ` but I don't know what will be the prefix, and by saying prefix I mean the `user.date` ?? any thoughts ? As I've added an example in the Question that works amazingly, the only issue is the messing around with `Object` that is throwing unexpected results all over the place. – Ginnani Jan 27 '13 at 23:06
  • I guess I understood this time. Will read once more to make sure. But if it is what I'm thinking, you should go with @PatrickGunderson's answer if you don't want to pollute `Object.prototype`. That means initializing your objects through a constructor. Otherwise, you can create a function that takes as parameters a reference to the object and the property name to work upon it. – Fabrício Matté Jan 27 '13 at 23:11
  • @FabrícioMatté you're so kind and now I feel sorry for your wasted time and your removed answer... I'm aware of the possibilities Patrick posted, and I tried lots of similar concepts for days. but that is (reading the answer all over again) not my main issue ...I think. ... – Ginnani Jan 27 '13 at 23:15
  • 1
    @Ginnani I've removed it personally, it wasn't answering your question at all. Considered posting another more direct answer but I'm a little slow today and the current answers basically say it all. Nathan's answer will be the simplest one, but has an (extremely small) risk of name clashing as he mentions in the last paragraph. The other practical solution is using Constructors as Patrick answered. A third option would be using functions passing an object reference and property name, but I guess I'm the only one crazy enough to use that. `:P` – Fabrício Matté Jan 27 '13 at 23:19
  • @FabrícioMatté hahah, I'm willing to suck life from JS out until I find an answer finally. Lost days on that matter on official DOCS and other places. No-one gives a simple example on how to achieve that. While the same time if you read the jQuery DOCS on how to create a custom method, you'll be able to setup one in seconds. – Ginnani Jan 27 '13 at 23:26
  • @Ginnani Heh true that. Add a method to `$.fn` then wrap your objects inside a jQuery object. `x]` – Fabrício Matté Jan 27 '13 at 23:29
  • @FabrícioMatté don't be surprised that I've done that too. If I remember well it was a success. – Ginnani Jan 27 '13 at 23:31