Подтвердить что ты не робот

Расширение основных типов без модификации прототипа

Как продлить основные типы 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.

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

4b9b3361

Ответ 1

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

Оригинал:

Что-то "неправильное" с расширением собственных прототипов - это FUD в браузерах ES5.

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

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

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

Ответ 2

Обновление: Даже этот код не полностью расширяет собственный тип 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 в качестве аргумента.

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

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

Ответ 3

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

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 в массивах Array и String.

Как заставить это работать без ошибки, см. ответ Alxandr.

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

d1.itervalTo(d2)

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

Ответ 4

Здесь у вас есть только одна часть. 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 и strings, таким образом они не могут быть "подклассом".

Ответ 5

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

http://sugarjs.com/

Sugar.js - все о внутренних расширениях объектов, и они не расширяют Object.prototype.