Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending core types without modifying prototype

How does one extend core JavaScript types (String, Date, etc.) without modifying their prototypes? For example, suppose I wanted to make a derived string class with some convenience methods:

function MyString() { }
MyString.prototype = new String();
MyString.prototype.reverse = function() {
  return this.split('').reverse().join('');
};
var s = new MyString("Foobar"); // Hmm, where do we use the argument?
s.reverse();
// Chrome - TypeError: String.prototype.toString is not generic
// Firefox - TypeError: String.prototype.toString called on incompatible Object

The error seems to originate from String base methods, probably "split" in this case, since its methods are being applied to some non-string object. But if we can't apply the to non-string objects then can we really reuse them automatically?

[Edit]

Obviously my attempt is flawed in many ways but I think it demonstrates my intent. After some thinking, it seems that we can't reuse any of the String prototype object's functions without explicitly calling them on a String.

Is it possible to extend core types as such?

like image 435
maerics Avatar asked Aug 21 '11 23:08

maerics


3 Answers

2 years later: mutating anything in global scope is a terrible idea

Original:

There being something "wrong" with extending native prototypes is FUD in ES5 browsers.

Object.defineProperty(String.prototype, "my_method", {
  value: function _my_method() { ... },
  configurable: true,
  enumerable: false,
  writeable: true
});

However if you have to support ES3 browsers then there are problems with people using for ... in loops on strings.

My opinion is that you can change native prototypes and should stop using any poorly written code that breaks

like image 198
Raynos Avatar answered Nov 19 '22 15:11

Raynos


Update: Even this code does not fully extend the native String type (the length property does not work).

Imo it's probably not worth it to follow this approach. There are too many things to consider and you have to invest too much time to ensure that it fully works (if it does at all). @Raynos provides another interesting approach.

Nevertheless here is the idea:


It seems that you cannot call String.prototype.toString on anything else than a real string. You could override this method:

// constructor
function MyString(s) {
    String.call(this, s); // call the "parent" constructor
    this.s_ = s;
}

// create a new empty prototype to *not* override the original one
tmp = function(){};
tmp.prototype = String.prototype;
MyString.prototype = new tmp();
MyString.prototype.constructor = MyString;

// new method
MyString.prototype.reverse = function() {
  return this.split('').reverse().join('');
};

// override 
MyString.prototype.toString = function() {
  return this.s_;
};

MyString.prototype.valueOf = function() {
  return this.s_;
};


var s = new MyString("Foobar");
alert(s.reverse());

As you see, I also had to override valueOf to make it work.

But: I don't know whether these are the only methods you have to override and for other built-in types you might have to override other methods. A good start would be to take the ECMAScript specification and have a look at the specification of the methods.

E.g. the second step in the String.prototype.split algorithm is:

Let S be the result of calling ToString, giving it the this value as its argument.

If an object is passed to ToString, then it basically calls the toString method of this object. And that is why it works when we override toString.

Update: What does not work is s.length. So although you might be able to make the methods work, other properties seem to be more tricky.

like image 39
Felix Kling Avatar answered Nov 19 '22 15:11

Felix Kling


First of all, in this code:

MyString.prototype = String.prototype;   
MyString.prototype.reverse = function() {
    this.split('').reverse().join('');
};

the variables MyString.prototype and String.prototype are both referencing the same object! Assigning to one is assigning to the other. When you dropped a reverse method into MyString.prototype you were also writing it to String.prototype. So try this:

MyString.prototype = String.prototype;   
MyString.prototype.charAt = function () {alert("Haha");}
var s = new MyString();
s.charAt(4);
"dog".charAt(3);

The last two lines both alert because their prototypes are the same object. You really did extend String.prototype.

Now about your error. You called reverse on your MyString object. Where is this method defined? In the prototype, which is the same as String.prototype. You overwrote reverse. What is the first thing it does? It calls split on the target object. Now the thing is, in order for String.prototype.split to work it has to call String.prototype.toString. For example:

var s = new MyString();
if (s.split("")) {alert("Hi");}

This code generates an error:

TypeError: String.prototype.toString is not generic

What this means is that String.prototype.toString uses the internal representation of a string to do its thing (namely returning its internal primitive string), and cannot be applied to arbitrary target objects that share the string prototype. So when you called split, the implementation of split said "oh my target is not a string, let me call toString," but then toString said "my target is not a string and I'm not generic" so it threw the TypeError.

If you want to learn more about generics in JavaScript, you can see this MDN section on Array and String generics.

As for getting this to work without the error, see Alxandr's answer.

As for extending the exact built-in types like String and Date and so on without changing their prototypes, you really don't, without creating wrappers or delegates or subclasses. But then this won't allow the syntax like

d1.itervalTo(d2)

where d1 and d2 are instances of the built-in Date class whose prototype you did not extend. :-) JavaScript uses prototype chains for this kind of method call syntax. It just does. Excellent question though... but is this what you had in mind?

like image 2
Ray Toal Avatar answered Nov 19 '22 15:11

Ray Toal