Расширение основных типов без изменения прототипа - PullRequest
18 голосов
/ 22 августа 2011

Как можно расширить основные типы JavaScript (String, Date и т. Д.) Без изменения их прототипов?Например, предположим, что я хотел создать производный строковый класс с некоторыми вспомогательными методами:

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

Кажется, что ошибка возникает из базовых методов String, в данном случае, вероятно, "split", так как ее методы применяютсяк некоторому нестроковому объекту.Но если мы не можем применить объекты к нестроковым объектам, то можем ли мы действительно повторно использовать их автоматически?

[Редактировать]

Очевидно, что моя попытка во многом ошибочнано я думаю, что это демонстрирует мои намерения.После некоторых размышлений кажется, что мы не можем повторно использовать какие-либо функции объекта-прототипа String без явного вызова их для String.

Возможно ли расширение основных типов как таковых?

Ответы [ 5 ]

17 голосов
/ 22 августа 2011

2 года спустя: мутирование чего-либо в глобальном масштабе - ужасная идея

Оригинал:

Что-то «не так» с расширением собственных прототипов - это FUD в браузерах ES5.

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

Однако, если вам требуется поддержка браузеров ES3, то возникают проблемы с людьми, использующими петли for ... in для строк.

Мое мнение таково, что вы можете изменить собственные прототипы и должны прекратить использование любого плохо написанного кода, который нарушает

3 голосов
/ 22 августа 2011

Обновление: Даже этот код не полностью расширяет собственный тип String (свойство length не работает).

Имо, вероятно, оно того не стоитподход.Есть слишком много вещей, чтобы рассмотреть, и вы должны потратить слишком много времени, чтобы убедиться, что он работает полностью (, если это вообще работает). @ Raynos предлагает еще один интересный подход .

Тем не менее, вот идея:


Кажется, что вы не можете вызвать String.prototype.toString ни для чего, кроме настоящей строки,Вы можете переопределить этот метод:

// 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());

Как видите, мне также пришлось переопределить valueOf, чтобы он работал.

Но: Я не знаю, являются ли это единственными методами, которые вы должны переопределить, а для других встроенных типов вам, возможно, придется переопределить другие методы.Хорошим началом было бы взять спецификацию ECMAScript и взглянуть на спецификацию методов.

Например, второй шаг в алгоритме String.prototype.split:

Пусть S будет результатом вызова ToString, предоставив ему this *Значение 1038 * в качестве аргумента.

Если объект передается в ToString, то он в основном вызывает метод toString этого объекта.И именно поэтому он работает, когда мы переопределяем toString.

Обновление: Что не работает равно s.length.Таким образом, хотя вы можете заставить методы работать, другие свойства кажутся более сложными.

2 голосов
/ 22 августа 2011

Прежде всего, в этом коде:

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

переменные MyString.prototype и String.prototype обе ссылаются на один и тот же объект!Назначение одному - назначение другому.Когда вы добавили метод reverse в MyString.prototype, вы также записали его в String.prototype.Поэтому попробуйте это:

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

Последние две строки обе предупреждают, потому что их прототипы - это один и тот же объект.Вы действительно расширили String.prototype.

Теперь о вашей ошибке.Вы назвали reverse на вашем MyString объекте.Где этот метод определен?В прототипе, который такой же, как String.prototype.Вы переписали reverse.Что это первое, что он делает?Он вызывает split на целевом объекте.Дело в том, что для работы String.prototype.split необходимо вызвать String.prototype.toString.Например:

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

Этот код генерирует ошибку:

TypeError: String.prototype.toString is not generic

Это означает, что String.prototype.toString использует внутреннее представление строки для выполнения своей задачи (а именно, для возврата еевнутренняя примитивная строка), и нельзя применять к произвольным целевым объектам, которые совместно используют прототип строки .Поэтому, когда вы вызвали split, реализация split сказала: «О, моя цель - не строка, позвольте мне вызвать toString», но затем toString сказала: «Моя цель не строка, и я не универсальный»так что он выбросил TypeError.

Если вы хотите узнать больше о дженериках в JavaScript, вы можете увидеть этот раздел MDN о дженериках массивов и строк .

Какчтобы заставить это работать без ошибок, см. ответ Александра.

Что касается расширения точных встроенных типов , таких как String и Date и так далее, без изменения их прототипов,Вы действительно этого не сделаете, не создавая оболочки, делегаты или подклассы.Но тогда это не позволит использовать синтаксис, такой как

d1.itervalTo(d2)

, где d1 и d2 - это экземпляры встроенного класса Date, прототип которого вы не расширили ,:-) JavaScript использует цепочки прототипов для этого вида синтаксиса вызова метода.Это просто так.Хороший вопрос, но ... но вы это имели в виду?

0 голосов
/ 12 июля 2013

Я думаю, что основной ответ вы, вероятно, не можете. То, что вы можете сделать, это то, что делает Sugar.js - создавать объектоподобный объект и расширяться от него:

http://sugarjs.com/

Sugar.js полностью посвящен собственным расширениям объектов, и они не расширяют Object.prototype.

0 голосов
/ 22 августа 2011

Вы ошиблись только в одной части.MyString.prototype не должен быть String.prototype, он должен быть таким:

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

[Редактировать]

Чтобы ответить на ваш вопрос лучше, нет, это не должно быть возможным,Если вы посмотрите на это: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor это объясняет, что вы не можете изменить тип в bools, ints и string, поэтому они не могут быть "подклассами".

...