Почему расширение нативных объектов - плохая практика?

136

Каждый лидер мнения JS говорит, что расширение нативных объектов - плохая практика. Но почему? Получим ли мы успех в исполнении? Боятся ли они, что кто-то сделает это «неправильно» и добавит перечислимые типы Object, практически уничтожив все циклы на любом объекте?

Возьмите TJ Holowaychuk «S should.js , например. Он добавляет простой поглотитель к Objectи все работает отлично ( источник ).

Object.defineProperty(Object.prototype, 'should', {
  set: function(){},
  get: function(){
    return new Assertion(Object(this).valueOf());
  },
  configurable: true
});

Это действительно имеет смысл. Например, можно было продлить Array.

Array.defineProperty(Array.prototype, "remove", {
  set: function(){},
  get: function(){
    return removeArrayElement.bind(this);
  }
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3);

Есть ли аргументы против расширения собственных типов?

buschtoens
источник
9
Что вы ожидаете, когда позже собственный объект будет изменен, чтобы включить функцию «удаления» с семантикой, отличной от вашей? Вы не контролируете стандарт.
ta.speot.is
1
Это не твой родной типаж. Это всем родной типаж.
6
«Не боятся ли они, что кто-то сделает это« неправильно »и добавит перечислимые типы к Object, практически уничтожив все циклы на любом объекте?» : Ага. В те времена, когда формировалось это мнение, невозможно было создать неперечислимые объекты. В этом отношении все может быть иначе, но представьте, что каждая библиотека просто расширяет собственные объекты так, как они этого хотят. Есть причина, по которой мы начали использовать пространства имен.
Феликс Клинг
5
Некоторые «лидеры мнений», например Брендан Эйх, считают, что расширение нативных прототипов - это совершенно нормально.
Бенджамин Грюнбаум
2
Foo не должен быть глобальным в наши дни, у нас есть include, requirejs, commonjs и т. Д.
Джейми Пейт,

Ответы:

127

Когда вы расширяете объект, вы меняете его поведение.

Можно изменить поведение объекта, который будет использоваться только вашим собственным кодом. Но когда вы меняете поведение чего-то, что также используется другим кодом, есть риск сломать этот другой код.

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

Если вам нужно настраиваемое поведение, гораздо лучше определить свой собственный класс (возможно, подкласс), а не изменять собственный. Так вы вообще ничего не сломаете.

Возможность изменять способ работы класса без создания подклассов - важная особенность любого хорошего языка программирования, но ее следует использовать редко и с осторожностью.

Абхи Бекерт
источник
2
Значит, добавление чего-то вроде .stgringify()будет считаться безопасным?
buschtoens
7
Есть много проблем, например, что произойдет, если какой-то другой код также попытается добавить свой собственный stringify()метод с другим поведением? На самом деле это не то, что вам следует делать в повседневном программировании ... нет, если все, что вы хотите сделать, это сохранить несколько символов кода здесь и там. Лучше определить свой собственный класс или функцию, которая принимает любой ввод и преобразует его в строки. Большинство браузеров определяют JSON.stringify(), лучше всего было бы проверить, существует ли он, а если нет, определить его самостоятельно.
Abhi Beckert
5
Я предпочитаю someError.stringify()больше errors.stringify(someError). Это просто и идеально соответствует концепции js. Я делаю то, что специально привязано к определенному объекту ErrorObject.
buschtoens
1
Единственный (но хороший) аргумент, который остается, заключается в том, что некоторые огромные макро-библиотеки могут начать заменять ваши типы и могут мешать друг другу. Или есть еще что-нибудь?
buschtoens
4
Если проблема в том, что у вас может быть коллизия, не могли бы вы использовать шаблон, при котором каждый раз, когда вы это делаете, вы всегда подтверждаете, что функция не определена, прежде чем определять ее? И если вы всегда помещаете сторонние библиотеки перед собственным кодом, вы всегда сможете обнаруживать такие коллизии.
Дэйв Кузино
32

Нет ощутимого недостатка вроде снижения производительности. По крайней мере, никто об этом не упомянул. Так что это вопрос личных предпочтений и опыта.

Главный аргумент за: он выглядит лучше и интуитивно понятнее: синтаксический сахар. Это функция, специфичная для типа / экземпляра, поэтому она должна быть специально привязана к этому типу / экземпляру.

Главный аргумент против: код может мешать. Если библиотека A добавляет функцию, она может перезаписать функцию библиотеки B. Это может очень легко взломать код.

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

Вот почему я не люблю такие библиотеки, как jQuery, подчеркивание и т. Д. Не поймите меня неправильно; они абсолютно хорошо запрограммированы и работают как шарм, но они большие . Вы используете только 10% из них, а понимаете около 1%.

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

TL; DR В случае сомнений не расширяйте собственные типы. Расширяйте собственный тип только в том случае, если вы на 100% уверены, что конечный пользователь будет знать об этом поведении и захочет его. В не случае манипулировать существующие функции основного языка, поскольку это нарушило бы существующий интерфейс.

Если вы решили расширить тип, используйте Object.defineProperty(obj, prop, desc); если не можете , используйте тип prototype.


Первоначально я задал этот вопрос, потому что хотел, чтобы Errors можно было отправлять через JSON. Итак, мне нужен был способ их связать. error.stringify()чувствовал себя лучше, чем errorlib.stringify(error); как предполагает вторая конструкция, я действую над собой, errorlibа не над errorсобой.

buschtoens
источник
Я открыт для дальнейших мнений по этому поводу.
buschtoens
4
Вы имеете в виду, что jQuery и подчеркивание расширяют собственные объекты? Они не Так что, если вы избегаете их по этой причине, вы ошибаетесь.
JLRishe
1
Вот вопрос, который, как мне кажется, упущен в аргументе о том, что расширение собственных объектов с помощью библиотеки A может конфликтовать с библиотекой B: почему у вас есть две библиотеки, которые либо настолько похожи по своей природе, либо настолько обширны, что этот конфликт возможен? Т.е. я либо выбираю lodash, либо подчеркиваю не то и другое одновременно. Наш текущий ландшафт libraray в javascript настолько перенасыщен, и разработчики (в целом) становятся настолько небрежными при добавлении их, что в конечном итоге мы
избегаем
2
@micahblu - библиотека может решить модифицировать стандартный объект только для удобства собственного внутреннего программирования (поэтому люди часто хотят это делать). Таким образом, этого конфликта нельзя избежать только потому, что вы не будете использовать две библиотеки с одной и той же функцией.
jfriend00 07
1
Вы также можете (начиная с es2015) создать новый класс, который расширяет существующий класс, и использовать его в своем собственном коде. так что, если у MyError extends Errorнего может быть stringifyметод без столкновения с другими подклассами. Вам все равно придется обрабатывать ошибки, не сгенерированные вашим собственным кодом, поэтому он может быть менее полезен, Errorчем с другими.
ShadSterling
16

На мой взгляд, это плохая практика. Основная причина - интеграция. Цитата из should.js docs:

OMG IT EXTENDS OBJECT ???!?! @ Да, да, с одним геттером должен, и нет, это не сломает ваш код

Ну откуда автору знать? Что, если мой фреймворк насмешек сделает то же самое? Что, если моя библиотека обещаний делает то же самое?

Если вы делаете это в своем собственном проекте, тогда ничего страшного. Но для библиотеки это плохой дизайн. Underscore.js - пример того, как все сделано правильно:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Стефан
источник
2
Я уверен, что в ответ TJ не использовал бы эти обещания или насмешливые рамки: x
Джим Шуберт
Этот _(arr).flatten()пример действительно убедил меня не расширять собственные объекты. Моя личная причина для этого была чисто синтаксической. Но это удовлетворяет мое эстетическое чувство :) Даже использование более обычного имени функции, например, foo(native).coolStuff()преобразование его в какой-то "расширенный" объект, выглядит великолепно синтаксически. Так что спасибо за это!
egst
16

Если вы посмотрите на это на индивидуальной основе, возможно, некоторые реализации будут приемлемыми.

String.prototype.slice = function slice( me ){
  return me;
}; // Definite risk.

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

String.prototype.capitalize = function capitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.

В этом случае мы не перезаписываем какой-либо известный базовый метод JS, но мы расширяем String. В одном из аргументов в этом посте упоминалось, как новый разработчик узнает, является ли этот метод частью основного JS или где найти документы? Что произойдет, если основной объект JS String получит метод с именем capitalize ?

Что, если вместо добавления имен, которые могут конфликтовать с другими библиотеками, вы использовали модификатор компании / приложения, понятный всем разработчикам?

String.prototype.weCapitalize = function weCapitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // marginal risk.

var myString = "hello to you.";
myString.weCapitalize();
// => Hello to you.

Если вы продолжите расширять другие объекты, все разработчики столкнутся с ними в дикой природе (в данном случае) с нами , что уведомит их о том, что это расширение, специфичное для компании / приложения.

Это не устраняет коллизии имен, но снижает вероятность. Если вы решите, что расширение основных объектов JS предназначено для вас и / или вашей команды, возможно, это для вас.

SoEzPz
источник
7

Расширять прототипы встроенных модулей - действительно плохая идея.Однако ES2015 представил новую технику, которую можно использовать для получения желаемого поведения:

использующий WeakMap s для связывания типов со встроенными прототипами

Следующая реализация расширяет Numberи Arrayпрототипов , не касаясь их вообще:

// new types

const AddMonoid = {
  empty: () => 0,
  concat: (x, y) => x + y,
};

const ArrayMonoid = {
  empty: () => [],
  concat: (acc, x) => acc.concat(x),
};

const ArrayFold = {
  reduce: xs => xs.reduce(
   type(xs[0]).monoid.concat,
   type(xs[0]).monoid.empty()
)};


// the WeakMap that associates types to prototpyes

types = new WeakMap();

types.set(Number.prototype, {
  monoid: AddMonoid
});

types.set(Array.prototype, {
  monoid: ArrayMonoid,
  fold: ArrayFold
});


// auxiliary helpers to apply functions of the extended prototypes

const genericType = map => o => map.get(o.constructor.prototype);
const type = genericType(types);


// mock data

xs = [1,2,3,4,5];
ys = [[1],[2],[3],[4],[5]];


// and run

console.log("reducing an Array of Numbers:", ArrayFold.reduce(xs) );
console.log("reducing an Array of Arrays:", ArrayFold.reduce(ys) );
console.log("built-ins are unmodified:", Array.prototype.empty);

Как видите, с помощью этой техники можно расширить даже примитивные прототипы. Он использует структуру карты иObject идентичность для связывания типов со встроенными прототипами.

Мой пример включает reduceфункцию, которая ожидает только в Arrayкачестве единственного аргумента, поскольку она может извлекать информацию о том, как создать пустой аккумулятор и как объединить элементы с этим аккумулятором из элементов самого массива.

Обратите внимание, что я мог бы использовать обычный Mapтип, поскольку слабые ссылки не имеют смысла, когда они просто представляют встроенные прототипы, которые никогда не собираются сборщиком мусора. Однако, a WeakMapне повторяется и не может быть проверен, если у вас нет правильного ключа. Это желательная функция, поскольку я хочу избежать любой формы отражения типа.


источник
Это отличное использование WeakMap. Будьте осторожны с xs[0]пустыми массивами. type(undefined).monoid ...
Спасибо
@naomik Я знаю - см. мой последний вопрос во втором фрагменте кода.
Насколько стандартна поддержка этого @ftor?
onassar
5
-1; какой в ​​этом смысл? Вы просто создаете отдельные типы отображения глобального словаря для методов, а затем явно просматриваете методы в этом словаре по типу. Как следствие, вы теряете поддержку наследования (метод, "добавленный" Object.prototypeтаким образом, не может быть вызван Array), а синтаксис значительно длиннее / уродливее, чем если бы вы действительно расширили прототип. Почти всегда было бы проще просто создавать служебные классы с помощью статических методов; Единственное преимущество этого подхода - ограниченная поддержка полиморфизма.
Марк Эмери
4
@MarkAmery Привет, приятель, ты не понимаешь. Я не теряю наследство, а избавляюсь от него. Наследование настолько 80-х, что о нем стоит забыть. Весь смысл этого хака состоит в имитации классов типов, которые, проще говоря, являются перегруженными функциями. Однако есть законная критика: полезно ли имитировать классы типов в Javascript? Нет, это не так. Лучше используйте типизированный язык, который поддерживает их изначально. Я почти уверен, что вы имели в виду именно это.
7

Еще одна причина, по которой вы не должны расширять собственные объекты:

Мы используем Magento, который использует prototype.js и расширяет множество вещей на собственные объекты. Это отлично работает, пока вы не решите добавить новые функции, и здесь начинаются большие проблемы.

Мы представили веб-компоненты на одной из наших страниц, поэтому webcomponents-lite.js решает заменить весь (собственный) объект события в IE (почему?). Это, конечно, нарушает работу prototype.js, что, в свою очередь, нарушает работу Magento. (пока вы не найдете проблему, вы можете потратить много часов, чтобы найти ее)

Если тебе нравятся неприятности, продолжай!

Евгений Вессело
источник
4

Я вижу три причины не делать этого ( по крайней мере, из приложения ), только две из которых рассматриваются в существующих ответах здесь:

  1. Если вы сделаете это неправильно, вы случайно добавите перечислимое свойство ко всем объектам расширенного типа. Легко обойти, используяObject.defineProperty , которое по умолчанию создает неперечислимые свойства.
  2. Вы можете вызвать конфликт с используемой библиотекой. Можно избежать прилежания; просто проверьте, какие методы используют библиотеки, прежде чем добавлять что-либо в прототип, проверьте примечания к выпуску при обновлении и протестируйте свое приложение.
  3. Вы можете вызвать конфликт с будущей версией собственной среды JavaScript.

Пункт 3, пожалуй, самый важный. С помощью тестирования вы можете убедиться, что расширения вашего прототипа не вызывают конфликтов с используемыми вами библиотеками, потому что вы сами решаете, какие библиотеки использовать. То же самое нельзя сказать о нативных объектах, если предположить, что ваш код работает в браузере. Если вы определитесь Array.prototype.swizzle(foo, bar)сегодня, а завтра Google добавит Array.prototype.swizzle(bar, foo)Chrome, вы можете столкнуться с недоумением коллег, которые задаются вопросом, почему.swizzle поведение компании не соответствует тому, что описано в MDN.

(См. Также историю о том, как mootools возились с прототипами, которыми они не владели, вынудили переименовать метод ES6, чтобы избежать взлома сети .)

Этого можно избежать, используя префикс для конкретного приложения для методов, добавленных к собственным объектам (например, определить Array.prototype.myappSwizzleвместо Array.prototype.swizzle), но это некрасиво; это так же хорошо разрешимо, используя автономные служебные функции вместо дополнения прототипов.

Марк Эмери
источник
2

Perf - тоже причина. Иногда вам может потребоваться перебрать ключи. Есть несколько способов сделать это

for (let key in object) { ... }
for (let key in object) { if (object.hasOwnProperty(key) { ... } }
for (let key of Object.keys(object)) { ... }

Я обычно использую, for of Object.keys()поскольку он делает правильные вещи и относительно краток, нет необходимости добавлять проверку.

Но это намного медленнее .

за-из против за-в результаты перфорации

Просто предположить, что причина Object.keysмедленной, очевидно, Object.keys()нужно выделить. Фактически, AFAIK он должен разместить копию всех ключей с тех пор.

  const before = Object.keys(object);
  object.newProp = true;
  const after = Object.keys(object);

  before.join('') !== after.join('')

Возможно, движок JS может использовать какую-то структуру неизменяемых ключей, чтобы Object.keys(object)возвращать ссылку, которая выполняет итерацию по неизменяемым ключам и object.newPropсоздает совершенно новый объект неизменяемых ключей, но что бы то ни было, он явно медленнее до 15 раз

Даже проверка hasOwnPropertyвыполняется в 2 раза медленнее.

Суть всего в том, что если у вас есть чувствительный к производительности код и вам нужно перебирать ключи, вы хотите иметь возможность использовать for inбез вызова hasOwnProperty. Вы можете сделать это, только если вы не изменилиObject.prototype

обратите внимание, что если вы используете Object.definePropertyдля изменения прототипа, если добавляемые вами вещи не перечисляются, они не повлияют на поведение JavaScript в вышеуказанных случаях. К сожалению, по крайней мере, в Chrome 83 они влияют на производительность.

введите описание изображения здесь

Я добавил 3000 неперечислимых свойств, чтобы попытаться вызвать любые проблемы с производительностью. Всего с 30 свойствами тесты были слишком близки, чтобы сказать, есть ли какое-либо влияние на производительность.

https://jsperf.com/does-adding-non-enumerable-properties-affect-perf

Firefox 77 и Safari 13.1 не показали разницы в производительности между классами Augmented и Unaugmented, возможно, v8 будет исправлен в этой области, и вы можете игнорировать проблемы производительности.

Но позвольте мне также добавить, что есть историяArray.prototype.smoosh . Краткая версия - это популярная библиотека Mootools, созданная самостоятельно Array.prototype.flatten. Когда комитет по стандартам попытался добавить родной язык, Array.prototype.flattenони обнаружили, что не могут не сломать множество сайтов. Разработчики, которые узнали о перерыве, предложили назвать метод es5 smooshшуткой, но люди испугались, не поняв, что это шутка. Они остановились flatвместоflatten

Мораль этой истории в том, что вы не должны расширять родные объекты. Если вы это сделаете, вы можете столкнуться с той же проблемой, что и ваши вещи, и если ваша конкретная библиотека не станет такой же популярной, как MooTools, поставщики браузеров вряд ли решат проблему, которую вы вызвали. Если ваша библиотека действительно станет такой популярной, это будет означать, что все остальные будут работать над проблемой, которую вы вызвали. Итак, пожалуйста, не расширяйте собственные объекты

GMan
источник
Подождите, hasOwnPropertyсообщает вам, находится ли свойство в объекте или в прототипе. Но for inцикл дает вам только перечислимые свойства. Я только что попробовал: добавьте неперечислимое свойство в прототип массива, и оно не будет отображаться в цикле. Нет необходимости не изменять прототип простейшего цикла к работе.
ygoe
Но это все еще плохая практика по другим причинам;)
gman