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


каждый лидер мнения JS говорит, что расширение родных объектов является плохой практикой. Но почему? Есть ли у нас хит производительности? Они боятся, что кто-то делает это "неправильно", и добавляет перечислимые типы на Object, практически уничтожив все петли на любом объекте?

забрать TJ Holowaychuk ' s должны.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);

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

7 98

7 ответов:

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

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

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

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

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

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

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

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

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

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

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

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

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


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

на мой взгляд, это плохая практика. Основная причина-интеграция. Цитировать надо.Яш документы:

OMG ОН РАСШИРЯЕТ ОБЪЕКТ???!?!@ Да, да это так, с одним геттером должен, и нет, это не сломает ваш код

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

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

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()

если вы посмотрите на него в каждом конкретном случае, возможно, некоторые реализации приемлемы.

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.

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

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

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 для вас и / или вашей команды, возможно, это для вас.

расширение прототипов встроенных модулей-это действительно плохая идея. тем не менее, 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 тип, так как слабые ссылки не имеет смысл, когда они просто представляют собой встроенные прототипы, которые никогда не собираются в мусор. Однако,WeakMap Не повторяется и не может быть проверен, если у вас нет правильного ключа. Это желаемая функция, так как я хочу избежать любой формы отражения типа.

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

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

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

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

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

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

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

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

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