Почему распространение нативных объектов - плохая практика?
Каждый руководитель мнения JS говорит, что расширение родных объектов - это плохая практика. Но почему? Получаем ли мы удар производительности? Они боятся, что кто-то делает это "неправильно" и добавляет перечислимые типы в Object
, практически уничтожая все циклы на любом объекте?
Возьмите TJ Holowaychuk should.js, например. Он добавляет простой getter в Object
, и все работает отлично (source).
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);
Существуют ли какие-либо аргументы против расширения собственных типов?
Ответы
Ответ 1
Когда вы расширяете объект, вы меняете его поведение.
Изменение поведения объекта, которое будет использоваться только вашим собственным кодом, прекрасное. Но когда вы изменяете поведение чего-то, что также используется другим кодом, существует риск, что вы нарушите этот другой код.
Когда приходит применение методов к классам объектов и массивов в javascript, риск взлома чего-то очень высок из-за того, как работает javascript. Долгий многолетний опыт научил меня, что этот вид вещей вызывает всевозможные ужасные ошибки в javascript.
Если вам нужно настраивать поведение, гораздо лучше определить свой собственный класс (возможно, подкласс) вместо того, чтобы изменять собственный. Таким образом, вы ничего не сломаете.
Возможность изменять способ работы класса без его подклассификации является важной особенностью любого хорошего языка программирования, но его нужно использовать редко и с осторожностью.
Ответ 2
Нет никакого измеримого недостатка, как удар производительности. По крайней мере, никто не упоминал об этом. Так что это вопрос личных предпочтений и переживаний.
Основной аргумент:. Он выглядит лучше и интуитивно понятен: синтаксический сахар. Это специфическая функция типа/экземпляра, поэтому она должна быть конкретно привязана к этому типу/экземпляру.
Основной аргумент contra: Код может вмешиваться. Если lib A добавляет функцию, она может перезаписать функцию lib B. Это может очень легко сломать код.
Оба имеют точку. Когда вы полагаетесь на две библиотеки, которые напрямую меняют ваши типы, вы, скорее всего, окажетесь в сломанном коде, поскольку ожидаемая функциональность, вероятно, не такая. Я полностью согласен с этим. Макро-библиотеки не должны манипулировать родными типами. В противном случае вы как разработчик никогда не узнаете, что действительно происходит за кулисами.
И именно по этой причине мне не нравятся такие библиотеки, как jQuery, underscore и т.д. Не поймите меня неправильно; они абсолютно хорошо запрограммированы и работают как шарм, но они большие. Вы используете только 10% из них и понимаете около 1%.
Вот почему я предпочитаю атомный подход где требуется только то, что вам действительно нужно. Таким образом, вы всегда знаете, что происходит. Микробиблиотеки только делают то, что вы хотите, чтобы они делали, поэтому они не будут вмешиваться. В контексте того, что конечный пользователь знает, какие функции добавлены, расширение родных типов можно считать безопасным.
TL; DR В случае сомнений не распространяйте родные типы. Расширяйте только родной тип, если вы на 100% уверены, что конечный пользователь узнает об этом и хочет этого поведения. Ни в коем случае не манипулируйте существующими функциями нативного типа, так как он нарушит существующий интерфейс.
Если вы решили расширить тип, используйте Object.defineProperty(obj, prop, desc)
; если вы не можете, используйте тип prototype
.
Я изначально придумал этот вопрос, потому что я хотел, чтобы Error
мог быть отправлен через JSON. Итак, мне нужен был способ их укорачивания. error.stringify()
чувствовал себя лучше, чем errorlib.stringify(error)
; как предполагает вторая конструкция, я работаю на errorlib
, а не на Error
.
Ответ 3
По-моему, это плохая практика. Основная причина - интеграция. Quoting should.js docs:
OMG IT EXTENDS OBJECT???!?! @Да, да, это так, с одним геттером должен, и нет, он не сломает ваш код
Ну, как может автор знать? Что, если мои издевательские рамки делают то же самое? Что делать, если моя promises lib делает то же самое?
Если вы делаете это в своем собственном проекте, тогда все в порядке. Но для библиотеки это плохой дизайн. Underscore.js - пример того, что сделано правильно:
var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Ответ 4
Если вы посмотрите на него в каждом конкретном случае, возможно, некоторые реализации приемлемы.
String.prototype.slice = function slice( me ){
return me;
}; // Definite risk.
Завершение уже созданных методов создает больше проблем, чем решает, поэтому на многих языках программирования обычно заявляется, чтобы избежать этой практики. Как Devs узнали, что функция была изменена?
String.prototype.capitalize = function capitalize(){
return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.
В этом случае мы не переписываем какой-либо известный JS-метод ядра, но мы продолжаем String. Один из аргументов в этом сообщении упоминал, как новый разработчик знает, является ли этот метод частью ядра JS или где найти документы? Что произойдет, если основной объект 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.
Если вы продолжали распространять другие объекты, все разработчики столкнулись бы с ними в дикой природе (в данном случае) we, которые уведомили бы их, что это расширение для компании/приложения.
Это не устраняет столкновений имен, но уменьшает вероятность. Если вы определите, что расширение основных объектов JS для вас и/или вашей команды, возможно, это для вас.
Ответ 5
Расширение прототипов встроенных модулей - действительно плохая идея. Однако ES2015 представил новую технику, которая может быть использована для получения желаемого поведения:
Использование WeakMap
для связывания типов со встроенными прототипами
Следующая реализация расширяет прототипы 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);
Ответ 6
Еще одна причина, почему вы не должны распространять собственные объекты:
Мы используем Magento, который использует prototype.js и распространяет много материала на собственные объекты. Это прекрасно работает, пока вы не решите получить новые функции и что там начинаются большие неприятности.
Мы ввели Webcomponents на одной из наших страниц, поэтому webcomponents-lite.js решает заменить весь (собственный) объект Event в IE (почему?). Это, конечно, ломает prototype.js, который, в свою очередь, разрывает Magento. (пока вы не найдете проблему, вы можете потратить много часов на ее отслеживание)
Если вам нравятся неприятности, продолжайте это делать!
Ответ 7
Я вижу три причины не делать этого (изнутри приложения, по крайней мере), только два из которых рассматриваются в существующих ответах здесь:
- Если вы ошибетесь, вы случайно добавите перечислимое свойство ко всем объектам расширенного типа. Легко работать с помощью
Object.defineProperty
, который по умолчанию создает неперечислимые свойства.
- Вы можете вызвать конфликт с используемой библиотекой. Его можно избежать с усердием; просто проверьте, какие методы используют библиотеки, прежде чем добавлять что-то в прототип, отметьте заметки о выпуске при обновлении и протестируйте приложение.
- Вы можете вызвать конфликт с будущей версией среды 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
), но такого рода уродливые; он также может быть разрешен с использованием автономных функций полезности вместо увеличения прототипов.