Розширення нативних об'єктів JavaScript - зло це? Маніфест SugarJS

       SugarJS logoУ коментарях до посту про Underscore / Lo-Dash я згадав, що серед бібліотек, що розширюють стандартну бібліотеку JavaScript, я віддаю перевагу SugarJS, який, на відміну від більшості аналогів, працює через розширення нативних об'єктів.
 
Це викликало гарячу дискусію про те, чи припустимо розширювати нативні об'єкти. Мене дуже здивувало, що практично всі висловилися виступили проти.
 
Це спонукало мене перевести маніфест SugarJS з цього питання. По всій видимості, автору цієї бібліотеки доводилося дуже часто чути подібні нападки. Тому він дуже виважено і досить неупереджено прокоментував кожну з них.
 
У цьому матеріалі розбираються підводні камені JavaScript, відомі і не дуже, а також пропонуються методи захисту. Тому я думаю, що стаття буде цікава і корисна будь-якому JS-розробнику, незалежно від його ставлення до проблеми розширення нативних об'єктів.
 
Передаю слово Andrew Plummer.
 
 

Отже, Sugar — бібліотека, яка модифікує нативні об'єкти JavaScript. Зачекайте, хіба це не в зло? — Запитаєте ви, — ви що, не витягли урок з гіркого досвіду Prototype?
 
З цього приводу існує багато помилок. Sugar уникає підводні камені, про які спотикався Prototype, і фундаментально відрізняється за своєю суттю. Проте цей вибір — не без наслідків. Нижче розібрані потенційні проблеми, які викликаються зміною нативних об'єктів, і викладена позиція Sugar щодо кожної з них:
 
     
  1. Модифікація об'єктів середовища
  2.  
  3. Опції як перераховуються властивості
  4.  
  5. Перевизначення властивостей
  6.  
  7. Конфлікти в глобальному просторі імен
  8.  
  9. Допущення щодо відсутності властивостей
  10.  
  11. Дотримання специфікації
  12.  

 1. Модифікація об'єктів середовища
 

Проблема:

Термін «об'єкти середовища» (host objects) означає об'єкти JavaScript, що надаються оточенням, в якому виповнюється код. Приклади host-об'єктів: Event, HTMLElement, XMLHttpRequest. На відміну від нативних об'єктів JavaScript, які строго відповідають специфікації, об'єкти середовища можуть мінятися на розсуд розробників браузерів, та їх реалізації в різних браузерах можуть відрізнятися.
 
Не вдаючись у подробиці, якщо Ви вносите об'єкти середовища, ваш код може бути схильний до помилок, може гальмувати і бути уразливим до майбутніх змін оточення.
 
 

Позиція Sugar:

Sugar працює тільки з нативними об'єктами JavaScript. Об'єкти середовища йому нецікаві (або, точніше кажучи, невідомі). Цей шлях обраний не тільки, щоб уникнути проблем з host-об'єктами, але і щоб зробити бібліотеку доступною великому безлічі оточень JavaScript, у тому числі працюючих поза браузера.
 
Від перекладача: ось модуль Sugar в репозиторії Node.
 
 2. Функції як перераховуються властивості
 

Проблема:

У браузерах, які не наступних сучасним специфікаціям, визначення нового властивості робить його перічесляемим (enumerable). При обході циклом властивостей об'єкта, нова властивість буде порушено нарівні з властивостями, що містять дані.
 
 Подробиці За замовчуванням, при визначенні у об'єкту нової властивості, воно стає перераховуваних. Таким чином ми зберігаємо в об'єктах дані і проходимся по них у циклі:
 
 
var o = {};
o.name = "Harry";
for(var key in o) {
  console.log(key);
}
// => name

Якщо в якості нового властивості ми призначимо функцію (або, висловлюючись мовою ООП, додамо об'єкта метод), ця функція теж стане перераховується:
 
 
Object.prototype.getName = function() {
  return this.name;
};
for(var key in {}) {
  console.log(key);
}
// => getName

В результаті обхід властивостей об'єкта циклом буде приводити до непередбачених результатом, а ми цього зовсім не хочемо. На щастя, за допомогою трохи іншого синтаксису ми можемо визначати неперічісляемие методи:
 
 
Object.defineProperty(Object.prototype, 'getName', {
  value: function() {
    return this.name;
  },
  enumerable: false
});
for(var key in {}) {
  console.log(key);
}
// => (пусто)

Проте, як завжди, є підступ. Можливість визначати неперечісляемие властивості відсутня в Internet Explorer 8 і нижче.
 
Отже, з перечісляемостью властивостей звичайних об'єктів розібралися, але що щодо масивів? Зазвичай для обходу значень масивів використовують звичайний цикл
for
зі счетічком.
 
 
Array.prototype.name = 'Harry';
var arr = ['a','b','c'];
for(var i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}
// => 'a'
// => 'b'
// => 'c'

Як бачите, проблем з перечісляемостью властивостей можна уникнути, просто накручуючи лічильник. Якщо ж обходити об'єкт за допомогою
for..in
, що перераховуються властивості потраплять в цикл:
 
 
Array.prototype.name = 'Harry';
var arr = ['a','b','c'];
for(var key in arr) {
  console.log(arr[key]);
}
// => 'a'
// => 'b'
// => 'c'
// => 'Harry'

З цієї причини, при зверненні до властивостей об'єктів за іменами властивостей (і до значень масивів за номерами індексу) в циклах виду
for..in
слід використовувати метод
hasOwnProperty
. Це виключить властивості, що не належать об'єкту безпосередньо, а дісталися йому через ланцюжок прототипів:
 
 
Array.prototype.name = 'Harry';
var arr = ['a','b','c'];
for(var key in arr) {
  if(arr.hasOwnProperty(key)) {
    console.log(arr[key]);
  }
}
// => 'a'
// => 'b'
// => 'c'

Це один з найбільш загальних прикладів хороших практик JavaScript. Завжди використовуйте його при зверненні до властивостей об'єктів за іменами властивостей.
 
Від перекладача:
 
Автор не згадує, що є ще один метод обходу масивів:
Array.prototype.forEach
. В результаті побіжного пошуку я знайшов поліфілл від Mozilla Developer Network, який, за їх словами, алгоритмічно відтворює специфікацію (і, як ви можете переконатися за наступним посиланням, має ідентичну з нативним
forEach
продуктивність). У коді поліфілла використовується перший (безпечний) спосіб обходу масиву. Водночас, відомо, що
forEach
помітно повільніше найпростішого циклу
for
з лічильником, судячи з усього, через додаткові перевірки.
 
 
forEach
доступний у всіх сучасних мобільних і десктопних браузерах. Відсутня в IE8 і нижче.
 

Позиція Sugar:

Sugar робить свої методи неперечісляемимі завжди, коли це можливо, тобто у всіх сучасних браузерах. Однак, поки IE8 не загине остаточно, потрібно завжди мати цю проблему на увазі. Її корінь лежить в обході властивостей циклом, і ми повинні розглянути окремо два основних види об'єктів, які можна обходити циклом: звичайні об'єкти і масиви.
 
Через цю проблему (а також через проблеми перевизначення властивостей) Sugar НЕ модифікує Object.prototype, як це робиться в прикладах вище. Це означає, що використання циклів
for..in
на звичайних об'єктах JavaScript ніколи не приведе до потрапляння в цикл невідомих властивостей, тому що їх немає.
 
З масивами ситуація дещо складніша. Стандартним способом обходу масивів є простою цикл
for
який при кожній ітерації збільшує лічильник на одиницю і використовує його в якості імені властивості. Цей спосіб безпечний, і проблема також не виникає. Обходити масив за допомогою циклу
for..in
теж можливо, але це не вважається хорошою практикою. Якщо ви вирішили використовувати цей підхід, завжди застосовуйте метод
hasOwnProperty
, щоб перевіряти, чи належать властивості безпосередньо об'єкту (див. останній приклад у раскривашке вище).
 
Виходить, обхід масиву циклом
for..in
і відсутність перевірки
hasOwnProperty
— це погана практика всередині поганий практики. Якщо такий код буде виконаний в застарілому браузері (IE8 і нижче), назовні вилізуть всі властивості об'єктів, включаючи методи Sugar, тому важливо відзначити, що проблема існує . Якщо ваш проект ламається при включенні до нього Sugar, перше, що ви повинні Провея, це правильно ви обходьте властивості об'єктів в циклах. Варто також відзначити, що ця проблема не є проблемою одного тільки Sugar, а має місце для всіх бібліотек, які надають поліфілли для методів масивів.
 
Висновок. Якщо ви не можете переписати проблемний код обходу масивів, а підтримка IE8 і нижче вам важлива, значить, ви не можете користуватися пакетом Array бібліотеки Sugar. Зберіть свою збірку Sugar , виключивши цей пакет.
 
 3. Перевизначення властивостей
 

Проблема:

У JavaScript практично кожна сутність є об'єктом, а значить, може мати властивості у вигляді пар ключ-значення. У JavaScript «хеши» (вони ж хеш-таблиці, словники, асоціативні масиви) — це звичайні об'єкти, а «методи» — це просто функції, присвоєні властивостям об'єктів замість даних. Добре це чи погано, але будь-який метод, оголошений для об'єкта (безпосередньо або далі по ланцюжку прототипів) також є властивістю, і звернення до нього відбувається тим же способом, що і для даних.
 
Проблема стає очевидною. Наприклад, якщо для всіх об'єктів визначити метод
count
, а потім якого-небудь об'єкту записати дані у властивість з тим же ім'ям, то метод виявиться недоступний.
 
 
Object.prototype.count = function() {};
var o = { count: 18 };
o.count
// => 18

Властивість
count
, безпосередньо визначене для об'єкта, як би затуляє однойменний метод, який лежить далі по прототипну ланцюжку (в оригіналі «кидає тінь» — is «shadowing»). В результаті викликати метод для цього об'єкта стає неможливо.
 
 

Позиція Sugar:

Разом з проблемою перераховуються властивостей, це основна причина, чому Sugar НЕ модифікує
Object.prototype
. Навіть якщо ви наперед знаєте, які методи об'єктів ви будете використовувати і вирішите уникати вживання однойменних властивостей, ваш код все одно буде вразливий, а налагодження перевизначених властивостей — завдання не з приємних.
 
Замість цього, Sugar воліє представляти всі методи для простих об'єктів як статичних методів класу
Object
. Поки JavaScript не робить різниці між властивостями і методами, цей підхід не зміниться.
 
Від перекладача:
 
При бажанні, ви можете перенести методи Sugar для роботи з звичайними об'єктами в властивості конкретного об'єкта. Це робиться за допомогою
Object.extended()
:
 
 
var
  foo = {foo: 'foo'},
  bar = {bar: 'bar'};

foo_extended = Object.extended(foo);
foo_extended.merge(bar);
console.log(foo_extended);
// => {foo: 'foo', bar: 'bar'}

 4. Конфлікти в глобальному просторі імен
 

Проблема:

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

Позиція Sugar:

Насамперед, важливо точно позначити суть цієї проблеми: це питання поінформованості. Якщо ви єдиний розробник у проекті, модифікація прототипів несе мінімальну небезпеку, адже ви в курсі, що модифікуєте і як. Якщо ж ви працюєте в команді, то можете не бути в курсі всього.
 
Скажімо, якщо розробники Вася і Петя визначають в одному і тому ж прототипі два методи, які роблять одне і теж, але мають різні імена, то вони всього лише працюють неузгоджено, але нічого кримінального. Якщо ж вони визначають два методи, що виконують різні завдання, але мають однакове ім'я, то вони поламають проект.
 
Цінність Sugar полягає в числі іншого в тому, що він надає єдиний, канонічний API, єдиним завданням якого є додавання маленьких допоміжних методів в прототипи. В ідеалі, це завдання слід довіряти тільки одній бібліотеці (будь то Sugar або якась інша). Виводити на поле глобального простору імен нових гравців, з якими ви мало знайомі і чиї завдання менш очевидні, — значить збільшувати ризик. Це, звичайно, не означає, що ви відразу підхопите проблему. Ступінь ризику потрібно співвідносити зі ступенем вашої обізнаності.
 
Бібліотеки, плагіни та інші middlemware не повинні використовувати Sugar з тієї ж причини. Модифікація глобальних об'єктів повинна бути свідомим рішенням кінцевого користувача. Якщо автор бібліотеки таки вирішує використовувати Sugar, він повинен повідомити про це своїм користувачам на самому видному місці.
 
Від перекладача: Я вважаю, що будь-яка бібліотека повинна прагнути до того, щоб мати якомога менше залежностей, особливо таких необов'язкових, як Sugar, Underscore і подібні бібліотеки. Вони не роблять нічого такого, що не можна було б переписати на чистому JavaScript. Зловживання цим правилом з боку авторів бібліотек може призводити до того, що ваш проект буде мати кашу з залежностей з дублюючим і абсолютно надлишковим функціоналом: Lazy.js, Underscore, Lo-Dash, wu.js, Sugar, Linq.js, JSLINQ, From. js, IxJS, Boiler.js, sloth.js, MooTools… Так що рекомендація «не використовуйте Sugar в middleware» справедлива і на адресу інших бібліотек.
 
 5. Допущення щодо відсутності властивостей
 

Проблема:

Наскільки небезпечні конфлікти в глобальному просторі імен, настільки ж шкідливі і допущення про те, що міститься (або чого не міститься) в глобальному просторі імен.
 
Уявіть, що у вас є функція, яка може приймати аргумент двох типів: рядок і об'єкт. Якщо функції передали об'єкт, вона повинна витягти рядок з певної властивості, наявного у цього об'єкта. Тому ви перевіряєте, чи є у аргументу така властивість:
 
 
function getName(o) {
  if(o.first) {
    return firstName;
  } else {
    return lastName;
  }
}

Цей оманливе простий код робить неявне припущення — що властивість
first
ніколи не буде визначено небудь у прототипну ланцюжку об'єкта (навіть якщо це рядок). Звичайно, гарантій цього вам ніхто не дасть, адже
Object.prototype
і
String.prototype
— це глобальні об'єкти, і змінити їх може кожен.
 
Навіть якщо ви противник зміни нативних об'єктів, ви не можете собі дозволити писати код, який, як у прикладі вище, робить допущення про вміст глобального простору імен. Такий код вразливий і може призводити до проблем.
 
 

Позиція Sugar:

Виправити код з останнього прикладу зовсім нескладно. Ви вже знайомі з рішенням:
 
 
function getName(o) {
  if(o.hasOwnProperty('first')) {
    return firstName;
  } else {
    return lastName;
  }
}

Тепер функція буде перевіряти тільки властивості, оголошені безпосередньо для переданого об'єкта, а не для всіх властивостей в його ланцюжку прототипів. Можна було б піти далі і заборонити передавати різні типи даних, але навіть такий простий перевірки достатньо, щоб уникнути проблем, пов'язаних із змінами в глобальному просторі імен.
 
На своєму віку Sugar спровокував цю проблему в коді великих бібліотек два рази: jquery # 1140 і mongoose # 482 . Винуватцями в обох випадках були невдало названі методи Sugar. Ми охоче їх перейменували, чим і дозволили проблему. Крім того, одна з бібліотек (jQuery) пропрацювала проблему спільно з нами, щоб усунути ваду на своєму боці.
 
Sugar намагається дуже акуратно працювати в глобальній області видимості, проте тут не обійтися без кооперації між авторами бібліотек. Коренем проблеми є природа самого JavaScript, який не робить відмінностей між властивостями і методами.
 
 6. Відповідність специфікації
 

Проблема:

Специфікація ECMAScript — це стандарт, що визначає поведінку нативних методів. Розробники середовищ виконання JavaScript прагнуть, щоб їх реалізація нативних методів була одночасно точної та актуальною. Тому важливі дві речі: щоб наші методи завжди діяли відповідно до специфікації і щоб можливі зміни специфікації в майбутньому не стали причиною регресій.
 
 

Позиція Sugar:

З самого початку ми розробляли Sugar, прагнучи не тільки відповідати специфікації, але й еволюціонувати разом з нею.
 
У пакеті ES5 Sugar пропонує поліфілли методів, описаних в сучасній специфікації. Звичайно, при наявності в середовищі виконання нативной реалізації методів, використовуються нативні. Sugar має великий набір тестів, завдяки якому можна бути впевненим, що всі поліфілли точно відповідають специфікації. Крім того, при бажанні ви можете відмовитися від пакету ES5 і використовувати будь-який інший ES5-поліфілл.
 
Відповідати специфікації — значить і підлаштовуватися під зміни в ній. На Sugar лежить відповідальність завжди бути на передовій стандартів. Чим раніше почнеш відповідати новій драфту специфікації, тим безболісніше буде перехід на неї в подальшому. Починаючи з версії 1.4, Sugar рівняється на стандарт ECMAScript 6 (і поглядає на 7, що знаходить на самих ранніх етапах розвитку). У міру зміни специфікації, Sugar продовжить підлаштовуватися, уникаючи конфліктів і прагнучи витримувати баланс між практичністю і відповідністю нативной реалізації.
 
Звичайно, адаптація хороша для тих користувачів, хто готовий регулярно оновлювати залежності. Але як поведуться проекти, що сидять на старій версії Sugar, коли середовище перейде на чергову версію специфікації? Уявіть собі ситуацію: браузери відвідувачів вашого сайту оновлюються, і сайт у них ламається. Sugar нещодавно прийняв нелегке рішення перевизначати методи, які не описані в специфікації явним чином. Тепер ніяких
if (!Object.prototype.foo) Object.prototype.foo = function(){};
, усі відсутні в ECMAScript методи перевизначаються безумовно.
 
Хоч і може здатися навпаки, але це рішення спрямоване на поліпшення підтримки сайтів. Навіть якщо в результаті оновлення специфікації нативні методи зміняться і вступлять у конфлікт з Sugar, Sugar перевизначить їх. Отже, методи продовжать працювати як і раніше — поки у вас не дійдуть руки оновити сайт. Але як уже було сказано, ми прагнемо йти далеко попереду специфікації, мінімізуючи цю потребу.
 
 TL / DR
Давайте пройдемося по всіх проблемах і пов'язаним з ними ризиками:
     
  1. Проблема: Модифікація об'єктів середовища
    Ризик: відсутня.
  2.  
  3. Проблема: Опції як перераховуються властивості
    Ризик: мінімальний. При обході звичайних об'єктів ризику немає, як і при обході масивів безпечним способом. При обході масивів небезпечним способом будуть проблеми в IE8 і нижче.
  4.  
  5. Проблема: Перевизначення властивостей
    Ризик: відсутня.
  6.  
  7. Проблема: Конфлікти в глобальному просторі імен
    Ризик: мінімальний, але зростає обернено пропорційно вашої обізнаності про те, що відбувається в глобальному просторі імен вашого проекту. В ідеалі, проект не повинен містити більше однієї бібліотеки начебто Sugar, і її використання має бути задокументовано. Не використовуйте Sugar, якщо самі пишете бібліотеку; в крайньому випадку, повідомляйте про застосування Sugar користувачам вашої якомога голосніше.
  8.  
  9. Проблема: Допущення щодо відсутності властивостей
    Ризик: мінімальний. Проблема виникала двічі в історії Sugar, обидва випадки були швидко вирішені.
  10.  
  11. Проблема: Дотримання специфікації
    Ризик: зовсім незначний. Sugar намагається бути настільки обережним з модифікацією нативних об'єктів, наскільки це взагалі можливо. Але чи достатньо цього? Відповідь на це питання залежить від переконань користувача і структури проекту, а також змінюється з часом (в кращу сторону).
  12.  
 
Ці висновки зробили ми самі на основі досвіду використання Sugar в реальному світі та відгуків користувачів. Якщо вони викликають у вас сумніви, кожен пункт ми розібрали в статті докладно — в надії, що це допоможе вам зробити власні висновки про те, чи підходить Sugar вашому проекту.
 
 Від перекладача
 

Продуктивність SugarJS

Будучи більш зручним у використанні, Sugar помітно програє Lo-Dash в продуктивності. Однак питання продуктивності, на мій погляд, має значення тільки на обробці скільки-великих обсягів даних. Якщо ви працюєте з фронтенда, то ніякої різниці в швидкодії цих бібліотек ви не виявите.
 
Для тих же, кому продуктивність критична, рекомендую LazyJS . У випадках, коли після обходу властивостей об'єкту / масиву не потрібна повернути новий об'єкт з усіма властивостями, LazyJS виграє у Lo-Dash в продуктивності. На складових операціях розрив стає значним. Наприклад, на операції
map -> filter
LazyJS в п'ять разів швидше, ніж Lo-Dash, і в п'ятнадцять, ніж SugarJS. Якщо ж вам потрібно не просто пройтися по значеннях властивостей, а зібрати новий об'єкт / масив, то LazyJS втрачає перевагу. StreetStrider підказує , що ліниві обчислення на ланцюжках плануються в LoDash версії 3.
 
 Тут (англ.) можна прямо в своєму браузері порівняти продуктивність десяти аналогічних бібліотек на безлічі типових операцій.
 
 

Зручність, яке ми втратили

Вам може бути цікаво, як елегантно проблема розширення нативних об'єктів вирішена в Ruby. Там запропоновано механізм уточнень (refinements), який у версії Ruby 2.1 вийшов з експериментального статусу.
 
Припустимо, розробник Вася, який пише бібліотеку Vasya, не соромиться робити манки-патчі: зі своєї бібліотеки він (пере) визначає методи стандартних об'єктів за допомогою конструкції
refine
.
 
 
refine String do
  def petrovich
    "Petrovich says: " + self
  end
end

Розробник Петя різьбить одну з частин великого проекту, над яким трудиться багато програмістів. Коли Петя підключає бібліотеку Vasya, Васін манки-патчі не поширюються на весь проект і не заважають жити іншим кодерам. При підключенні бібліотеки перевизначення нативних об'єктів взагалі не відбувається.
 
Щоб скористатися новими методами, Петя в своєму коді вказує, манки-патчі з яких бібліотек йому потрібні:
 
 
using Vasya
using HollowbodySixString1957

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

  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.