Прошу прощения за «жёлтый» заголовок, но надо же как-то привлечь.
В статье о moment.js, в первой же ветке, разгорелся горячий спор, в котором участвовали даже мэтры JS на хабре. Это очень странно для меня, мне всё казалось таким очевидным, а уж тем более для мэтров.
И я считаю, это достаточно серьёзный спор для того, чтобы у него была отдельная статья. Очень важно с этим моментом наконец разобраться. Если мы придём к обратному заявленому в статье решению – это, в конце концов, тоже будет важный ответ.
Так расширять встроенные типы (String
, Array
, Number
, …) в JS или не расширять?
Однозначно и безоговорочно нет. Под катом – нерелигиозные аргументы, пояснения, когда всё-таки можно, и дискуссия в комментариях.
Исторический взгляд
Что мы получим, если рассмотрим исторический процесс становления JavaScript? (это мой вольный взгляд по памяти на историю JS, вполне возможно в нём есть реальные отклонения от настоящего).
- Появляется JavaScript и в нём прототипное наследование. И встроенные типы в нём тоже построены на прототипном наследовании. И ещё в нём есть неудобные операции с DOM. Это безэмоциальные факты.
- JS используют для мелких скриптов, в которых не задумываются о наследовании и никаких вопросов соответственно не стоит
- Браузеры развиваются, постепенно JS всё плотнее входит в веб-разработку и становится нужным писать на JavaScript более крупные вещи, чем раньше. Динамические языки ещё не так популярны, популярны: C++, PHP, Java
-
Из-за необходимости написания крупных вещей (и неудобства DOM в JS) появляются библиотеки с функциями-помощниками. Одна из них - Prototype.js. Поскольку о наследовании вопрос ещё не стоял и не задавался, авторы библиотеки по привычке подсовывают нужные методы в прототипы встроенных объектов и чувствуют себя совершенно спокойно. И разработчики тоже. Где-то в это же время разработчики начинают хотеть классы в JS (и ваш покорный слуга из их числа) и, откопав где-то копипасту с функцией
extend
или чем-то подобным, начинают её активно использовать. Прототипы это сложно, а классы – это знакомо и проверено временем. Так что это тоже происходит довольно безболезненно, поскольку классическое ООП нормально ложится на прототипное наследование - Незаметное событие: Те, кто использует Prototype.js в крупных проектах неожиданно замечают вкусный на вид оператор for-in, прикручивают его, но чуть позже начинают обнаруживать странные ошибки при проходах по массивам и объектам
- Вина за эти ошибки интуитивно падает на JavaScript, эта информация расходится по интернетам, цикл for-in становится нерекомендуемым, все спокойны и рады. На фоне появляется Ajax, после нескольких попыток для него появляется работающая на тот момент кроссбраузерная копипаста, все вроде тоже довольны, но всё же как-то не хватает стандартизации.
- Неожиданно появляется чуть больше людей, которые разобрались в прототипном наследовании и понимают, что то что происходит – не очень хорошо. Появляется и тут же популяризуется jQuery (к коду которого у меня никаких претензий, кроме нынешнего веса библиотеки - но так тоже сложилось исторически) (с удобным методом ajax, btw)
- Адепты прототипного наследования выходят из нор и пытаются раскрыть другим программистам глаза на возможности, которые уже давно им доступны, но они их упускают. Библиотеки ещё весят мало, поэтому их иногда смешивают. Где-то здесь появляется конфликт JQuery и Prototype.js, выросший из проблемы с document.getElementByClassName, причина которой в расширении встроенного типа
- Время идёт, разобравшихся в прототипном наследовании всё больше. С другой стороны подступает функциональщина, которой тоже удобно писать на JS, используя созданные in-the-place объекты
- Появляется node.js, в котором отказываются от классического ООП и используют паттерн “модуль” из CommonJS, где отдельный файл содержит неймспейс с функциональщиной (или, нежелательно, ООП) внутри. Это шаг к отказу от ООП
- Войны ООП vs прототипы/функциональщина
- Сейчас мы здесь
Видно, что ошибки научили ещё не всех.
Логический взгляд
-
Почему в JS не рекомендуется использовать оператор
for-in?
-
Неправильный ответ: потому что в перечисление могут попасть ненужные свойства и методы объекта
-
Сомнительный ответ: не использовать для массивов, потому что для массивов не гарантировано сохранение порядка перебора (это правда, но это провис языка и/или движков браузеров и сейчас это меняется)
-
Сомнительный ответ: в массив можно записать свойство не по индексу через
a["woo"] = 23;
и тогда оно попадёт в цикл. Да, можно, но зачем осознанно так делать? -
Правильный ответ: в перечисление могут попасть все не-
enumerable
свойства объекта, а в том числе все свойства из цепочки прототипов. Если у объекта длинная цепочка прототипов, то это будет происходить долго из-за её перебора. Для отделения свойств на верхнем уровне цепочки можно использовать `hasOwnProperty``, для определения, принадлежит ли свойство объекту. -
Да, но что если у нас есть простой объект без сложной цепочки, созданный прямо на месте? Вот так:
{'a': 2, 'b': 3}
? Тоже нельзя использоватьfor-in
? -
Неправильный ответ: Да, нельзя. Это не рекомендуется.
-
Сомнительный ответ: Эммм….
-
Почти правильный ответ: Можно, он для этого и был прездназначен. Но есть одно «но». Программисты библиотеки, которую ты используешь, подсовывают в прототипы встроенных объектов свои методы и естественно не заботятся об
enumerable: false
. И эти методы могут попасть в перечисление. Безопаснее этим не пользоваться.
Эй, а почему вдруг безопаснее отгородить себя? Значит так и будет продолжаться? Это разве решение проблемы?
- Правильный ответ: Давно пора образумить этих программистов.
Практический взгляд
var a = [12, 14, 13, 6];
for (var i in a) { console.log(i, a[i]); }
> 0 12
> 1 14
> 2 13
> 3 6
Array.prototype.foo = function() { console.log('bar'); }
for (var i in a) { console.log(i, a[i]); }
> 0 12
> 1 14
> 2 13
> 3 6
> foo bar
for (var a in { 'a': 2, 'b': 3 }) { console.log(a); }
> a
> b
Object.prototype.foo = function() { };
for (var a in { 'a': 2, 'b': 3 }) { console.log(a); }
> a
> b
> foo
Array.prototype.forEach = function(...) { ... };
var matches = 'test'.match(/t/);
console.log( matches instanceof Array );
> true
for (var i in matches) console.log(i, matches[i]);
> 0 t
> index 0
> input test
> forEach function() { }
(за обнаружение последнего примера спасибо TheShock)
Где-то здесь появляется конфликт JQuery и Prototype.js, выросший из проблемы с document.getElementByClassName
JavaScript Гарден, глава «Великий прототип», последний раздел «Расширение встроенных прототипов». Мой там только перевод, этот документ писали опытные JS-программисты (хотя и там есть косяки. но не в этом абзаце).
Вы уверены, что в соседней библиотеке не захотят переопределить метод и назвать его также? И тогда будет невозможно пользоваться ни вашей библиотекой, ни соседней. Но случаи смешанных библиотек случаются всё реже. Хуже, если этот метод войдёт в следующую спецификацию и будет обладать другим поведением – вы отрежете его пользователям вашей библиотеки.
…По-моему как раз «соответствует всей идеологии JS» и «все JS фреймворки» – это религия. В языке есть дырки и одна из них – то, что можно расширять встроенные типы.
JQuery пользуется расширением прототипа собственного объекта – это вполне себе ок и как раз соответствует идеологии. А мы говорим о расширении встроенных типов — это две разные вещи. Встроенные типы по правилам any-типизированных языков должны быть закрыты для расширения. В JS у вас есть возможность их расширить и это сработало как неизвестное медленно-текущее вирусное заболевание. Заразились одни, не ощутили последствий, а через месяцы оказались заражены все вокруг.
Разумный взгляд
Вы помните чем кончается переопределение операторов в C++? Вы знаете, что не можете расширить встроенные классы Java? Вы пытаетесь отнаследоваться от str
в Python? Нет. Так почему же вы это делаете в JS?
Сейчас мы живём в эру быстро сменяющихся версий браузеров, избавления от старых, и перехода на HTML5, так может и в JS стоит забыть некоторые первобытные страхи?
Когда можно
- Если вам нужно в своём личном скрипте обеспечить наличие метода, который будет в будущем имплементирован. При этом нужно, чтобы интерфейс который он возвращает также соответствовал спецификации
- Всё
Альтернативы
Да какие хотите (ок, все приведённые функции – в видимости какого-то своего объекта, не в глобальной):
-
function trim(str) { return str.replace(...); } trim(" string to trim ");
-
function trim() { return this.replace(...); } trim.call(" string to trim ");
-
utils.trim(" string to trim ");
-
var a = new ExtendedString(" string to trim "); a.trim();
-
$.each([ 1, 2, 6, 6], …);
Статьи по теме
- Extending built-in native objects. Evil or not? by kangax (спасибо smashercosmo)
- What’s wrong with extending DOM? by kangax (спасибо smashercosmo)
-
Javascript
for...in
with arrays @ SO -
Exploring JavaScript
for...in
loops
Тесты
В этом комментарии Zibx провёл тест по скорости двух способов: через расширение прототипа и отдельную функцию и работа через прототип чуть не показалась нам действительно значительно быстрее. Но, к счастью, Markel опроверг этот факт, создав соответствующий тест на jsperf.