Чому розширення рідних об’єктів є поганою практикою?


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);

Чи є якісь аргументи проти поширення нативних типів?


9
Що ви очікуєте, що станеться, коли згодом власний об’єкт буде змінено, щоб він включив до себе функцію "видалити" з різною семантикою? Ви не контролюєте стандарт.
ta.speot.is

1
Це не ваш рідний тип. Це кожен рідний тип.

6
"Вони бояться, що хтось зробить це" неправильним способом ", і додають численні типи в" Об'єкт ", практично знищуючи всі петлі на будь-якому об'єкті?" : Так. Ще в часи, коли ця думка формувалася, створити незліченну властивість було неможливо. Зараз у цьому плані все може бути інакше, але уявіть, що кожна бібліотека просто розширює рідні об'єкти так, як вони цього хочуть. Є причина, чому ми почали використовувати простори імен.
Фелікс Клінг

5
Для чого варто, щоб деякі "лідери думок", наприклад, Брендан Ейх, вважають, що цілком чудово розширити натурні прототипи.
Бенджамін Грюенбаум

2
Foo не повинен бути глобальним в ці дні, у нас є включати, Requjs, Commonjs і т.д.
Джеймі Пате

Відповіді:


127

Розширюючи об’єкт, ви змінюєте його поведінку.

Зміна поведінки об'єкта, який буде використовуватися лише вашим власним кодом, є непоганим. Але коли ви зміните поведінку чогось, що також використовується іншим кодом, є ризик, що ви порушите цей інший код.

Якщо мова йде про додавання методів до класів об'єктів та масивів у javascript, ризик зламати щось дуже високий, завдяки тому, як працює JavaScript. Багаторічний досвід мене вчив, що подібний матеріал викликає всілякі жахливі помилки в JavaScript.

Якщо вам потрібна власна поведінка, набагато краще визначити власний клас (можливо, підклас), а не змінювати рідний. Таким чином ви взагалі нічого не зламаєте.

Здатність змінювати, як працює клас, не підкласируючи його, є важливою особливістю будь-якої хорошої мови програмування, але вона повинна використовуватися рідко і обережно.


2
Тож додавання чогось подібного .stgringify()вважатиметься безпечним?
buschtoens

7
Проблем багато, наприклад, що відбувається, якщо якийсь інший код також намагається додати власний stringify()метод з різною поведінкою? Це насправді не те, що ви повинні робити в повсякденному програмуванні ... ні, якщо все, що ви хочете зробити, це зберегти кілька символів коду тут і там. Краще визначити свій власний клас чи функцію, яка приймає будь-які введення та стримує їх. Більшість веб-переглядачів визначають JSON.stringify(), найкраще було б перевірити, чи існує, а якщо не визначити це самостійно.
Абхі Беккерт

5
Я віддаю перевагу someError.stringify()понад errors.stringify(someError). Це просто і ідеально підходить до концепції js. Я роблю щось, що спеціально пов'язане з певним ErrorObject.
buschtoens

1
Єдиний (але хороший) аргумент, який залишається, - це те, що деякі гумористичні макроязовики можуть почати захоплювати ваші типи і можуть заважати один одному. Або є ще щось?
buschtoens

4
Якщо проблема полягає в тому, що у вас може виникнути зіткнення, чи можете ви використовувати шаблон, коли кожен раз, коли ви це робили, ви завжди підтверджуєте, що функція не визначена перед її визначенням? І якщо ви завжди ставите сторонні бібліотеки перед власним кодом, ви повинні мати можливість завжди виявляти подібні зіткнення.
Дейв Кузен

32

Немає вимірюваних недоліків, як хіт виступу. Принаймні ніхто не згадував. Тож це питання особистих уподобань та досвіду.

Основний аргумент: Це виглядає краще та інтуїтивніше: синтаксичний цукор. Це специфічна для типу / екземпляра функція, тому вона повинна бути спеціально пов'язана з цим типом / екземпляром.

Основний протилежний аргумент: Код може заважати. Якщо lib A додає функцію, вона може замінити функцію lib B. Це може зламати код дуже легко.

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

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

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

TL; DR Якщо ви сумніваєтесь, не поширюйте рідні типи. Розширити нативний тип можна лише в тому випадку, якщо ви на 100% впевнені, що кінцевий користувач буде знати про це та бажає такої поведінки. Ні в якому разі не маніпулюйте наявними функціями рідного типу, оскільки це порушить існуючий інтерфейс.

Якщо ви вирішили поширити тип, використовуйте Object.defineProperty(obj, prop, desc); якщо ви не можете , скористайтеся типом prototype.


Я спочатку поставився до цього питання, тому що хотів Error, щоб його можна було передати через JSON. Отже, мені потрібен був спосіб їхньої послідовності. error.stringify()відчував себе краще, ніж errorlib.stringify(error); як підказує друга конструкція, я працюю над собою, errorlibа не над errorсобою.


Я відкритий щодо подальших думок з цього приводу.
buschtoens

4
Чи ви маєте на увазі, що jQuery і підкреслення розширюють рідні об'єкти? Вони ні. Тож якщо ви їх уникаєте з цієї причини, ви помиляєтесь.
JLRishe

1
Ось питання, яке, на мою думку, пропущено в аргументі, що розширення нативних об'єктів з lib A може конфліктувати з lib B: Чому у вас є дві бібліотеки, які або такі схожі за своєю природою, або такі широкі за своєю суттю, що цей конфлікт можливий? Тобто я обираю лодаш або підкреслюю не обидва. Наш нинішній ландшафт бібліотеки в javascript настільки насичений, і розробники (загалом) стають настільки недбалими, додаючи їх, що ми намагаємося уникати кращих практик для заспокоєння наших
лібордів

2
@micahblu - бібліотека може вирішити змінити стандартний об'єкт саме для зручності власного внутрішнього програмування (саме тому люди хочуть це робити). Отже, цього конфлікту не уникнути лише тому, що ви б не використовували дві бібліотеки, що мають однакову функцію.
jfriend00

1
Ви також можете (станом на es2015) створити новий клас, який розширює існуючий клас, і використовувати його у власному коді. тож якщо MyError extends Errorвін може мати stringifyметод без зіткнення з іншими підкласами. Ви все ще повинні обробляти помилки, не генеровані вашим власним кодом, тому це може бути менш корисно, Errorніж для інших.
ShadSterling

16

На мою думку, це погана практика. Основна причина - інтеграція. Цитуючи документи must.js:

OMG IT РОЗШИРУЄТЬ ОБ'ЄКТ ???!?! @ Так, так, це може, з одним одержувачем слід, і ні, він не порушить ваш код

Ну, як автор може це знати? Що робити, якщо моя глузуюча рамка робить те саме? Що робити, якщо мої обіцянки lib роблять те саме?

Якщо ви робите це у власному проекті, то це добре. Але для бібліотеки - це погана конструкція. Underscore.js - приклад того, що зроблено правильно.

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

2
Я впевнений, що відповідь TJ полягає в тому, щоб не використовувати ці обіцянки або глузуючі рамки: x
Джим Шуберт

_(arr).flatten()Приклад на насправді переконав мене не розширювати рідні об'єкти. Моя особиста причина для цього була суто синтаксичною. Але це задовольняє моє естетичне почуття :) Навіть використання якоїсь більш регулярної назви функції, як foo(native).coolStuff()перетворення її в якийсь "розширений" об'єкт, виглядає чудово синтаксично. Тож дякую за це!
напр.

16

Якщо ви дивитесь на це в кожному конкретному випадку, можливо, деякі реалізації є прийнятними.

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.

Якщо ви продовжуєте розширювати інші об'єкти, усі диски зіткнулися б із ними в дикій природі з (у цьому випадку) ми , які повідомили б, що це розширення, яке стосується компанії / програми.

Це не усуває зіткнення імен, але зменшує можливість. Якщо ви визначите, що розширення основних об'єктів JS призначене для вас та / або вашої команди, можливо, це саме для вас.


7

Розширення прототипів вбудованих модулів - це справді погана ідея. Однак ES2015 представив нову техніку, яку можна використовувати для отримання бажаної поведінки:

Використання WeakMaps для асоціації типів із вбудованими прототипами

Наступна реалізація розширює 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це не можна виправити, і його не можна перевірити, якщо у вас немає потрібного ключа. Це бажана особливість, оскільки я хочу уникати будь-якої форми відображення типу.


Це класне використання WeakMap. Будьте обережні з цим xs[0]на порожніх масивах Тхо. type(undefined).monoid ...
Дякую

@naomik Я знаю - див. моє останнє запитання, фрагмент 2-го коду.

Наскільки стандартна підтримка цього @ftor?
onassar

5
-1; який сенс у чомусь із цього? Ви просто створюєте окремі глобальні типи відображення словника до методів, а потім явно шукаєте методи у цьому словнику за типом. Як наслідок, ви втрачаєте підтримку успадкування (метод "доданий" до Object.prototypeцього способу неможливо викликати на an Array), і синтаксис значно довший / негарніше, ніж якщо ви справді продовжили прототип. Практично завжди було б простіше просто створити класи утиліти статичними методами; Єдиною перевагою цього підходу є деяка обмежена підтримка поліморфізму.
Марк Амері

4
@MarkAmery Ей, друже, ти цього не розумієш. Я не втрачаю спадщини, але позбавляюся від неї. Спадщина настільки 80-х, про це слід забути. Вся суть цього хака в тому, щоб імітувати класи типів, які, просто кажучи, є перевантаженими функціями. Однак є законна критика: чи корисно наслідувати класи типів у Javascript? Ні, це не так. Радше використовуйте набрану мову, яка підтримує їх на самому світі. Я майже впевнений, що це ви мали на увазі.

7

Ще одна причина, чому ви не повинні поширювати рідні об’єкти:

Ми використовуємо Magento, який використовує prototype.js і розширює багато матеріалів на рідні Об'єкти. Це прекрасно працює, поки ви не вирішите отримати нові функції, і саме тут починаються великі неприємності.

Ми представили Webcomponents на одній із наших сторінок, тому 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.

(Дивіться також історію про те, як чвари мутантів з прототипами, якими вони не володіли, змусили метод переназвати ES6, щоб уникнути зламу в Інтернеті .)

Цього можна уникнути, використовуючи специфічний для програми префікс для методів, доданих до нативних об’єктів (наприклад, визначити Array.prototype.myappSwizzleзамість Array.prototype.swizzle), але це некрасиво; це так само добре вирішимо, використовуючи окремі утилітні функції замість збільшення прототипів.


2

Перф - це теж причина. Іноді вам може знадобитися перекидання клавіш. Існує кілька способів зробити це

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()як це робить правильно, і порівняно коротко, немає необхідності додавати чек.

Але, це набагато повільніше .

for-of vs for-in perf results

Думаю, що причина 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створює абсолютно новий об’єкт незмінних ключів, але що б там не було, це явно повільніше на 15x

Навіть перевірка hasOwnProperty- до 2 рази повільніше.

Сенс всього цього полягає в тому, що якщо у вас є чутливий код perf і вам потрібно перебирати клавіші, то ви хочете мати можливість користуватися 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 буде виправлено в цій області, і ви можете ігнорувати проблеми з perf.

Але дозвольте також додати, що є історіяArray.prototype.smoosh . Коротка версія - популярна бібліотека Mootools, зроблена своїми силами Array.prototype.flatten. Коли комітет зі стандартів спробував додати місцевий, Array.prototype.flattenвони виявили, що не змогли, не зламаючи багато сайтів. Розробники, які дізналися про перерву, запропонували назвати метод es5 smooshяк жарт, але люди злякалися, не розуміючи, що це жарт. Вони влаштувалися flatзамістьflatten

Мораль цієї історії полягає в тому, що ви не повинні поширювати рідні предмети. У цьому випадку ви можете зіткнутися з тим самим випуском ваших речей, і якщо ваша бібліотека не настільки популярна, як MooTools, постачальники браузерів навряд чи зможуть обійти проблему, яку ви викликали. Якщо ваша бібліотека набула такої популярності, це було б неначе змусити всіх інших вирішити проблему, яку ви викликали. Тож, будь ласка, не розширюйте рідні об’єкти


Зачекайте, hasOwnPropertyповідомляє вам, чи є властивість в об'єкті, або в прототипі. Але for inцикл отримує лише численні властивості. Я тільки що спробував це: Додайте до прототипу Array масив, що не перелічує, і воно не з’явиться в циклі. Не потрібно не змінювати прототип для найпростішого циклу для роботи.
ygoe

Але це все-таки погана практика з інших причин;)
gman
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.