Extending core types without modifying prototype

不打扰是莪最后的温柔 提交于 2019-11-27 13:37:34

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

Felix Kling

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.

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?

You got only one part wrong here. MyString.prototype shouldn't be String.prototype, it should be like this:

function MyString(s) { }
MyString.prototype = new String();
MyString.prototype.reverse = function() {
  this.split('').reverse().join('');
};
var s = new MyString("Foobar");
s.reverse();

[Edit]

To answer your question in a better way, no it should not be possible. If you take a look at this: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor it explains that you can't change the type on bools, ints and strings, thus they cannot be "subclassed".

I think the basic answer is you probably can't. What you can do is what Sugar.js does - create an object-like object and extend from that:

http://sugarjs.com/

Sugar.js is all about native object extensions, and they do not extend Object.prototype.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!