Галоўная / Артыкулы / Дзесяць повсякдневных правіл JavaScript, якія пашкоджаюць вашу ментальную модель.

Дзесяць повсякдневных правіл JavaScript, якія пашкоджаюць вашу ментальную модель.

Разглядаеся дзесяць тонкіх аспектаў працы JavaScript – ад можлівасці змены значэння зменных типу const да клаусураў і асінхроннага керавання памылкамі – які таямніча спрычынаюць багі ў коде апытных разработчыкаў.

4076 слоў

Калі вы прыязнаецеся з синтаксам JavaScript, ён перестае казацца страшным. Вы большэй не падаеце на прынцыповыя знакі; асінхронныя калебацы становяцца звычайной часткаю роботы, а разлік межу let і const стае чыстаю прывычкай. Пасля таго, як вы завершылі достатню колькасць проектаў, гэты язык пачынае адчувацца як стары друг. Вы момантальна бачыце распашчастыя памылкі, у вас є чытлае разумеўнэ для таго, як вирашаюцца прабамы, і вы знаеце, што null і undefined — гэта не адна й тая ж рошчынь, незалежна ад таго, як дзеякі API прымусова іх спаяваюць.

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

Самыя складныя часткі JavaScript рэдка калі ўскладнены экзотычнымі крайнімі прыкладамі чы ўзбочаннямі мовы. Це звычныя правілы, якія спаўнаюцца некалькім нечаканым спосабам. Кожны окремы рядок выглядае нормальна, легка чытаецца і праходзіць праз поверхневую пераглядку. Неспакой выйшае з таго, як система запуску склеюе гэтыя рядкі так, як гэта не адпавядае ментальнаму моделю, які ў вас быў пад час чытання коду.

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

1. const захоўвае прыязначэнне, а не об’ект

Звычным скрацаннем для адукаціі const ёсь тое, што гэта «стварае значэнне, якое не можа змяніцца». Гэта чыстая спрасцавленне для прымітываў, але яна не працуе для масоў і об’ектаў. Тое, што на самай працы гарантуе const, — гэта тое, што зменнай не можна перазначыць іншы об’ект. У гэтым няма ніч пра тое, чы можна зменіць той об’ект, на які вона выказваеся.

Об’ект user, заявлены ўжо з const, все ж можа атрымваць новыя атрыбуты. Його внутрані выкладкі таксама можна адначасова апдэйтаваць. Маса, заявлена ўжо з const, все ж можна падсылаць новыя элементы, сортаваць чы абчысляць. З пагляду JavaScript ніч гэтага не паўтараецца ў зв’язку з самай зменнай — вона як і раней выказваецца на той самы об’ект.

Гэты разлік межа адзючаннямі і рэальнае становішча яшчэ спрычыняе справжніяі багі ў кодавых базах, якія сильна спалегваюць ідэю незменнасці. Об’ект налашчання імпортуецца як сталяя значэнне, пасля чым адзін модуль таямна яго зменшвае, і раптам усі іншыя модулі, якія выкарыстоўваюць гэты апсал, бачаюць змяну. Масіва, які зберагаецца ў стане прыемленае, сортаваецца на месцы, чаго янычыць як «тэчнае» значэнне, так і старэйшае значэнне, якое іншая частка коду супакоўвалася, што яно є зафіксаваным історычным знімкам. Тэст зменшвае спакульнае об’екта, і абсалютна некаляваны тэст пачынае выконвацца няправільна, але толькі тады, калі запускач тэста випадкова выконвае ўсё іншым порядкам.

Ключавая слова const дае вам хыбнае чувства безпекі — яно выглядае як захоўнай панцыр з-паўна навакол значэння, але час выканання захоўвае толькі паказвач на зменную, а не саму структуру, на якую ён паказвае.

Якщо вам дася праця, каб паветнасць не было, вам трэба яе стварыць самім. Гэта можа значыць вярненне абсалютна новых об’ектаў заместа рэдагавання існуючых, застосоўванне Object.freeze да конкрэтных значэнняў, якія маюць значэнне, вжытак бібліятэкі, створанай на адной паветнасці, або проектаванне функцый так, каб яны з самага пачатку не выдавалі ссылакі на внутраніяі зменныя даны. Нават Object.freeze блокуе толькі верхні ўрад, і якщо не заморозіць кожны внутрані об’ект, внутрэшнія шары застаюцца такімі ж зменнымі, як і раней.

Большасць апытных разработчыкаў можа запамятаць гэты правілу, пры тым як ўсё рава выступае здзівленне кожны раз, калі візуальны стыль коду паўтарае болей строгія гаранцыі, чым на самай працэ ўможліва даўае JavaScript.

2. Об’ектны распрасцэнне копіюе менш, чым здаецца

Распространэнне об’екта за дапамою { ...obj } стала аднай з популярных ідіом у JavaScript. Яе легка чытаць, яна чудова для з’еднання стандартных значэнь з перакрыццямі, і яна дазволяе стварыць зменена версію значэння без парадксу з орыгіналам.

Візуальна частка паказвае як цэлы, незалежны дубляк. Насамперадзе, копія існуе толькі на адным роўні.

const original = {
  profile: {
    name: "Umar",
    skills: ["JavaScript", "Node.js"],
  },
};

const copy = { ...original };
copy.profile.skills.push("TypeScript");

Калі гэты фрагмент запускаецца, нова дадзена навык таксама прадстаўляецца ў original.profile.skills. Об’ект вышэйшага роўня дзейсна є новым, але об’ект profile, які знаходзится ўнутры яго, і маса skills, якая знаходзится ўнутры таго, застаюцца абсолютна тымі ж об’ектамі, на якія вказываў орыгінал.

Гэта і ўсё, што робіць гэты баг такім стойкім: першы слой працуе абсалютна так, як і павінен. Пераканальванне copy !== original вяртае значэння true, што здаецца доказам таго, што была адбыта чыстая сепарацыя. Проблема спільных мутацыйяў выклікаецца толькі тады, калі пераходзиш на наступны рэвень.

Выкарыстанне гэтага падходу таксама прыносіць другую неспадзячку праз злучэнне об’ектаў налаштаванняў. Запіс { ...defaults, ...options } заменяе цэлыя вкладаныя об’екты, а не злучае ўсі іх окремыя ключы. Якщо options зменяе які-небудзь настаўлення ў вкладаным об’екте, усі суседнія настаўлення, якія знаходзіліся ў defaults, таксама зникаюць, нават якіх вызывач ніколі не хацеў чыпляцца.

Рашэнне не ўсега ёст магчымасць «глыбокай клонаванні». Полная глыбока клонаванне можа быць даскладной, магчыма, апусціць ідэнтычнасць об’ектаў, ад карысткі якіх вы насправдзе спакульватэце ў іншых частках коду, а таксама стварыць дублікаты дадзеных, якія мелі застацца спакульнаванымі. Найважлівейша — гэта выявіць, калькі саме внутрошняя шляхі трэба зробіць незалежнымі копіямі. Распрацаваўце іх якраз, выкарыстоўваючы спецыяльныі інструмент для незменных апдэйтаў, або пераструктуруйце глыбока внутрошняя дадзеныя так, каб межы ўласнасці былі відразу видныя.

Функцыя распрасцэрання об’ектаў працуе абсалютна так, як апісана, калі вам дасправды трэбна толькі паверхневая копія. Яе становіцца небяспекай тады, калі ўпрощаная синтаксіс гэтай функцыі плутаецца з полным, глыбоким клонаваннем.

3. Звычная роўнасць можа скрыць калькі пераканвертавання

Самыя апэктыўныя развіццелі зазвычай выбіраюць ===, каб ухіліцца ад проблем, якія выступаюць у зв’язку з ==. Штаўна, гэтыя прыкметы ёсць карыстнымі, але яны не захаўнуюць ад усіх непрыкметных пераканвертаў, якія выкарыстоўвае JavaScript; багато іншых такіх пераканвертаў выступаюць у месцах, якія не маюць нічынага з апэратаром равенства.

Ключы аб’ектных сваяйностей — гарны прыклад. За аднаковасцю з сімволамі, кожны ключ аб’екта пад спадом ёсць строкай. Таму заданне object[1] і пазней чытанне object["1"] адночасна выкарыстоўваюць тую ж сваяйність. Код, які ментальна спрыягае числовыя та строчковыя ідэнтыфікаторы як два адналежны разныя светы, можа пастаць у непрыемных ситуацыях у такім разе.

Порэхунакі спадзеянняў выканаюць свае сабсконверсіі залежна ад таго, што парабяляецца. Два строкі парабяляюцца лексыкагачысленнем (знак за знакам), тады калі парабяляецца число з числовым строкам, можа выконвацца числовы порэхунак. Таму "20" < "100" дае рэзультат false, а 20 < "100" — true. Як толькі змяніцца месца, з якога берэцца значэнне, логіка сортавання або верыфікацыі можа змяніць сваю дзеясносць, нават якшы выказваныя значэнні выглядаюць ідэнтычна.

Аператар + ўзначна складны, таму што ён выконвае як абавяць лічбовыя значэння, так і спаўнюе функцію з’еднання строк, пры чым JavaScript выбирае, якую з гэтых функцый выкарыстаць, на адповіднасці з контекстам. Одна строка, якая з’являецца рана ў ланцугу абавяння, може зменіць тлумачэнне всего, што следуе за ёю. Паколькі значэння, якія прыходзяць з полаў формы, параметраў запиту URL і іншых джэрел HTML, зазвычай прыходзяць у вастоўе строкаў, выраз, які ідеальна працаваў з внутршнімі лічбовымі значэннямі, можа без жадных сігналоў перайсці на спаўнення функцыі з’еднання строк як толькі будзе паўязаны з інпутам, які бачыць корыстувальнік.

Апэктываныя команды ухілляюцься ад гэтых падступоў шляхом нормалізацыі значэнняў пры самай межы, замест таго каб пакладацца на аператарыяў у правільнай інтэрпретацыі неапранутых даных у рэальны час. Ідэнтыфікатор з самага пачатку фіксуецца як строка або число. Сума грошэй перакладаецца у паверыжаны числовы тип. Дата аналізуецца і ператвараецца у належны тэмпаральны тип або фіксаваную ISO-строку прытаманне будзь-якім порэванням.

Сама прымусовая ператварэння не ёсць справжнім проблемам, правілы JavaScript у гэтым аспекте ўзгаданы чыста і адносна стабільныя. Справжняя проблема заключаецца у тым, што гэтыя правілы продовжаюць дзейсніцца там, дзе нічога ў коде візуальна не сигналізуе пра тое, што адбываецца ператварэння.

4. Array.prototype.sort Пераранжуе на месцы і за замовчэнням порэваняе текст

Сортаванне выглядае як адна з найпростейшых операцый, якія ўздольны выканаць з масавым дадзеннем, але на самай працоўнае ёно аб’еднвае два явышчы, якія часта спантажуюць людзей.

Першыя неспадзекі стаюцца таму, што сортаванне мутуюць масавае дадзенне. Выклік .sort() пераранжуе масавае дадзенне, на яком ён быў выкліканы, і вяртае звязак да таго ж самога масавага дадзення. Лёгка ўтрымліваць значэнне, якое вяртаеся, у новай зменной, і думаць, што первіснае масавае дадзенне все ўтрымае свой пачатковы порядак, толькі ўсё ж выявляецца, што оба звязкі тепер вядуць да аднаго і тога ж, пераранжаванага списку.

Другія неспадзекі стаюцца таму, як працююць порэванні, калі не пасылаецца функцыя-кампаратар. У такім случае JavaScript перакладае кожны элемент у строку і аранжуе іх лексыкагачысленнем. Якшо запрацаваць гэта з масавым дадзеннем цалых, можна пабачыць ў рэзультате ўсё такое 1, 100, 20, 3, а не асцярожныя числовыя порядкі.

Гэта комбінацыя стварае апасненне ў кантролі стану фронтэнду. Уявіце компонент, які сортавае масэвую прыблізна перед адрасаваннем, не усвядомляючы, што гэтая масэва ўсё тая ж рэферэнцыя да дадзейнаў, якія зберагліся ў кэшы або наданы серверам. У результате гэты компонент мутуюць ту самую спільную базу дадзейнаў. Компонент, які чытае тыя ж дадзейны, таксама бачыць новы порядак, не выкананыя ніякога функцыі сортавання. Логіка выявлення змян і мемаізацыі таксама можа не працаваць правільна, адколі ідэнтычнасць масэвой (яе рэферэнцыі) ніколі не зменялася, хоць ўсё яе содержанне зменілася — таму поверхневая параболка не пакажае нічога як разныя.

Савэцкі JavaScript адказвае нечыстаючыміся альтэрнатывамі: toSorted() доступны ў средах, якія яго падтрымляюць, а кланаванне масівы пры сортаванні застаецца стандартным рашэнням там, дзе яго няма. Для числовага сортавання вам яшчэ трэба передаць .sort() явны компаратар, які закодавае тую порэўнанне, якая вам насправды патрэбна.

Глобальны вывад заключаецца ў тым, што называнне методу не паведамляе вас пра ўсія яго параметры. „Сортаванне“ описвае тое, што вы бачыце ў канцы, але ніч не гаворыць пра тое, чыстаецься метод на месцы, як ён перакладае значэнні для порэўнання, якія гарантіі стабільнасці ён надае, чыі спецыфічны порядак вам можа знадобіцца. Нават апытныя разработчыкі паддаюцца на глупасці, калі метод здаецца настолькі стандартным, што яны ніколі не зупіняюцца, каб перазважыць, што ён насправды рабіць унутро.

5. Об’ект Date адмініструее адзін момент — большая частка вхідных дадзенняў не паўнаспраўна адпавядае ўзэму

Проблемы з часовымі поясамі ў JavaScript на самай справе не стаюць з таго, што часовыя поясы ў концэптуальнам сенсе єсць складнымі. Яны выступаюць унаследке таго, што короткая строчка несе значна больш за тое, што можна бачыць, неявных прыпускаў.

По суті, об’ект Date — це метка часу: тачны момент, вымераны за стандартам UTC. Але большая частка дат, з якімі фактычна працуюць людзі — дзень нараджэння, канец тэрміна, цыкл вырачоўвання рахункоў, запланаваная зустрэч — є календарнымі поняттямі, а не фіксаванымі моментамі у універсальным часе, і кожна з іх адносіцца да часовых поясоў по-разнаму.

Якщо вы парсіце толькі рэчып з датай і пасля чаго відобразіце яго за местным часам, дата у календары, якую бачыць корыстнік, можа змяніцца залежна ад таго, дзе ён знаходзіцца. Значэнне, якое мяжвае прадставляць „3 серпня“, можа быць інтэрпретавана як паловача ночы за UTC, і тады ў местным формате яна будзе паказана як 2 серпня. Таксама, часовы пазнак зразка, створанага без явнага сдвігу UTC, можа быць прачытаны інакша залежна ад точнага формата рэчыпа і способу яго парсінга пад час выканання.

Летній час стварае ўсё больш сложнасцей. Дадзенне фіксаванага колькасці мілісекунд да об’екта Date не гарантуе, што гэта будзе тое ж сама, што дадзенне аднаго календарнага дня за местным часам — у дзеярых днях у зонах, дзе дэйствуе зміна часу, трываласць фактычна становіць 23 або 25 гадзін.

Аптэктываныя разработчыкі яшчэ іноды сталкаюцца з гэтым, таму што ў іх кодзе для адражэння калькольных незв’язаных ідэй выкарыстоўваецца адны тып Date. У системе типаў няма нічога, што паведамляла б, чы розглядзаемая значэнне мае быць тачным моментам часу, простым календарным датам чы местным часам, прызначаным для адражэння ў конкретным регіоне.

Надзейныя системы рашаюць гэтыя проблемы, чынячы намеры явнымі, а не неявнымі. Моменты часу пазначаюцца з урахоўваннем смещэння UTC або выражаюцца безпосередна ў формате UTC. Значэння, якія апісваюць толькі календар, застаюцца такімі ж, а не безпатрэбна ператвараюцца ў повныя меткі часу. Рэжымы, прызначаныя для конкрэтных регіонаў, зберагаюць контекст часовай зоны, у якой былі створаны. Працэсы парсінгу і форматавання адбываюцца на чытка вяліканых граніцах у системе, а не там, дзе проста прадстаўляецца чы чытаецца дата.

Таму супракэс зазвычай не ў тым, што задзейнаюць часовыя зоны — а ў тым, што якась на першы погляд безнэбяжная працэса пераканваркі вже зробіла тылуцяны выбор, каторую зоны вжываць.

6. Обявленне можа быць выканана раней, чым яго функцыя-адаптавальнік насправдзе запрацюе

Выкаанавана обявленне здаецца завершаным задзеем, але функцыя, прыўязаная да яе за дапамою .then — або код, які знаходзится пасля await — не запрацоўвае прымусова ў момент выканання сінхроннага коду. У замен яна патрапляе у чергу мікрзадаў і запрацоўвае пасля таго.

Такая абсарбацыя створае пэркладку, якая ўсё ж можа застаць неспакою дажы самых апытных разработчыкаў, калі прабамы, таймеры, обработчыкі змаганняў і звычны сінхронны код пачынаюць супакоўвацца. Прабама, якая вже рашылася, плануе свое продажчэнне на пазнейшы час, тады калі зараз выконваны сінхронны код продолжае працаваць без перашкод. Гэтае запланаванае продажчэнне зазвычай выконваецца раней, чым прыйдзе час на выконанне калебэка таймера для наступнага завдання — нават setTimeout з адлегліваннем нуля мілісэкундаў.

Праўdziва практычная небяпека не ў тым, каб здагадвацца пра порядак выводу у консоль пад час тэсту. Яна крыўціцца у розуменні таго, што фразы „прабама выкарана“ і „яе обробнік фактычна запрацаваў“ — гэта два разныя моменты часу. Апдэйт стану, які адбываецца через обробнік прабамы, можа ўсё ще не быць видным для коду, які выконваецца пазней у той самы сінхронны стэк. Тэст можа пераглядаць значэнне раней, чым його незавершаныя мікрзадачы паводле заплану выкараны. А дастаткова дзьвінгая ланцюжок мікрзадач можа відклаць выкарыстанне таймероў і процес адрасавання візуальнага контэнту далей, чым прыпускалася, адколі сэрвіс выконання завжды спачатку абрабоцвае весь кяу мікрзадач, перш чым перейсці да чаго-небудзь іншага.

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

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

7. Абгортаванне async вызову ў try/catch не гарантуе, што будзе падхоплена якая-небудзь адмова

Блок try/catch, які выкананы навакол вызову функцыі, здаецца мае захістваць гэты вызов ад неудач. У разе асінхронных функцыяў тое, чы рэальна дзейсніцца гэты захіст, залежыць абсалютна ад таго, чакаецеся вы на вярнутую прамісу.

try {
  saveAuditLog(record);
} catch (error) {
  reportError(error);
}

Якщо saveAuditLog выклікае несінхронна і раней, чым вярне прамісу, блок catch будзе якраз правільна ўпрацоўваць гэту неудачу. Але якщо saveAuditLog ўжо є асінхронной функцыяй, якая пазней збываецца, то калі ёй не вдаёцца, на тым часе яна вярнула прамісу, і выкананне выйшла з блока try. Неудача тады застаёцца ў гэтай прамісу — яна не мае нічынага з тым сінхронным вызовам, які вже завершыўся.

Дадзенне await паўтарае прыяўленне адмовы да ўсё таго try/catch, але гэта працуе толькі тады, калі функцыя, яку вы пішаце, сама можа чакаць на ўсё. Перадача прамісы ў іншую ланцоўку таксама можа захаваць гэтыя зв’язкі адмов. Простая вызова асінхронной функцыі з усередзіны коду для обработкі адмов, без чакання на ёё рэзультат або яго вярнення, нічага не дае.

Гэтая памылка часта з’яўляецца ў функціях-обработчыках запуску, функціях-калбэках для масоў і хукі бібліятак, дзе супакоўаны API часта не ведае, што робіць з прамісай, якая вярнулася з асінхроннага калбэка — і часта проста яе ігнаруе. Калбэк усё равно правільна адмовляе, але ніхто гэта не спазірае. Залежна ад среды выканання, гэта можа выражацца як паведамленне пра неконтрольваную адмову, як запісаная адмоўка, як зупіненне процэса або як робота, якая тылу ніколи не атрымвае падтрымкі.

Разработчыкі, які страждалі ад такіх проблемаў з відстэйкамі чераз выкарыстанне прысоедзення обяваў замест адступав коду. Яны спытваюць, які самэўчас трымаеся за асінхронную роботу, дзе адмова ператвараецца на ўжываны результат, і чыяя з намерна выконваных без адстэйку вызоваў все ўсё ж маюць реальны спосаб прыняць адчутак пра бяда.

Асінхронныя бяды пераходзяць па ланцоўкі прысоедзенняя, а не па глыбіне вкладанняя вашых склянэкаў.

8. Значэнні за замовчанням пад час деструктурыравання працуюць толькі для undefined

Значэнні за замовчанням пад час деструктурыравання выглядаюць як адмоўлівае захаванне проты нехваткі вводу.

const { timeout = 5000 } = options;

Гэтае значэнне застаўляецца толькі калі timeout ёсьць undefined або проста не ўключанае ў об’ект. Яго не застаўляць для null, 0, порожней строчкі чы якога-небудзь іншага значэння, якое было явна заданае.

Частаўна гэта самэ ж тое паведамленне, якое вы хочаце. Значэння «ноль» можа быць намеровым спосабам выключэння затрымкі, а null можа мець сабой самастоятнее значэнне. Проблемы пачынаюцца, калі хтось вырашае, што стандартны значэння ўсё пакрывае для всіх рэчаў, якія «немагчыма викорыстоўваць». API можа адправіць null, і код, які працуе далей, прабуе выкарыстоўваць яго для матэматычных расчыткаў. Поле формы можа быць заполненаю порожнім рэчаменем, тады стандартны значэння ніколі не будзе застосавана. Загрузчык налаштоўкаў можа розлічваць «зменную, якая не задана», ад «зменной, якая явна задана як порожняя», тады калі код, які выкарыстоўвае гэтыя налаштунакі, ставится да обох варыянтаў адночасна, ён пераходзіць на стандартны значэнне.

Стандартныя параметры функцый працуюць па той ж самы логікі: калі вызваць функцыю без аргумента, прыменяецца стандартны значэнне, але якщо явнаа задаць null, то стандартны параметр не будзе выкарыстоўвацца. Гэта разлік мае вельмі важны значэнне, калі даныя перадаюцца через JSON-пакеты, рядкі базы дадзеных, заповненыя формы і API трэціх сторон — у всіх гэтых месцах null часта з’являецца.

Разработчыкі, якія паважаюцца на гэты патэрн, зазвычай роздзеляюць задаванне стандартных значэнняў і перакананне ў правільнасці дадзеных. Стандартны параметр адпавядае на пытанне "што будзе, калі нічога не было задана?"; Перакананне ў правільнасці адпавядае на пытанне "чы рэальна задана інформацыя є приемлімай?" Аднаўленне обох задач у адну синтаксічную структуру можа змусіць падазроцельныя даныя выглядаць так, нібы яны былі правільна ініціялізаваны.

Правіла гэтага языка ў даным случае точныя і аднаковыя. Непразумеласці выходзяць чыста з таго, што слова "default" натыякае на шырэйшы дыяхунок, ніж фактычна дае строгае паведанне, якое дазволяе толькі значэння undefined.

9. Відсутняя властасць, выдаленая властасць і undefined — гэта тры розныя речы

JavaScript дазволяе властасці об’екта мячыць значэнне undefined, або ж ўзагалі не існаваць у об’екте. Калі вы прыменяеце до якой-небудзь з іх оперэйшання, вы отрымаеце undefined, таму яны здаюцца взаімназаменнымі — але гэта не так.

Такі інструменты, як оператор in, Object.hasOwn, Object.keys, синтаксіс распрасцявання, ітерацыя, верыфікаторы схэмы і серыяванне JSON, можаюць паказаць разлік. Напрыклад, JSON.stringify пачынае абсалютна без значэнняя атрыбуты, якія маюць значэнне undefined, тады як элемент масівы, ў яком размешчана значэнне undefined, обрабоўваецца інакш. З’едынэнне об’ектаў таксама можа перазапісаць цэлком нормальнае існуючае значэнне на undefined, нават калі чалавек, які выкарыстоўвае цячэнне, проста хацеў заліць гэты поле недаступным.

Гэтыя прасоўкі часта стаюць прычыной тонкіх багоў пад час апдэйтаў. Кантэнт PATCH-запыткаў з бакэнду можа спрыймаць прыгледзеная поле як „не трэба яго змяніць“, а поле, якое явна заданае на null, — як „трэба яго адчыстыць“. У той жытак фарм на фронтэндзе можа ствараць значэнне undefined для будзь-каго поля, якога корыстнік ніколи не чапаў — але калі гэтыя даны фарма вводзяцца ў об’ект апдэйта, гэтыя значэння undefined такім чынам залишаюцца і зменшуюць існуючыя значэнні пад час з’еднання.

Ёжы ўтримацца ад гэтага, неабходна намеравана, а не випадкова, задаць сэмантыку апдэйтаў. Прыгледзенне, undefined, null і законныя порожнія значэнні не должны сама сабою прабаваць значэнням, проста таму, што викорыстоўваецца які-небудзь механізм серыялізацыі чы з’еднання об’ектаў.

Гэта мае важнае значэнне ў TypeScript, дзе необавязковая атрыбут і обавязковы атрыбут, тип якога включае undefined, выражаюць два структурна разныя намеры — нават якщо код прыемліка пазней будзе ставіцца да іх як да однаковых. Болей строгія настройка компайляра можа забезпечыць такое разграджэнне на рывень типаў, але фактычныя даны, якія праходзяць через прыемлік у час експлуатацыі, все рава патрабуюць сабстаячай пераканалення.

Два значэнні, якія выглядаюць ідэентычнымі калі яны чытаны, можаць ўсё ж выражаць вельмі разныя контракты, калі яны пераходзяць через межу сеті або праз операцыю апдэйта.

10. Клозура зберагае переменную, а не кадр, зафіксаваны ў часе

Закрыцція ўзлегка адна з наймоцнейшых з’явоў, якія дае вам JavaScript. Функцыя зберагае доступ да зменных з таго простору імен, дзе яна была задана, і самэ гэта дазволяе калебэкам, фабрычным функцыям, обробнікам запуску падзеяў і шаблонам модуляў працаваць так сама практычна, як і ў дзеяннях.

Пашчотнае апісанне, якое вжываюць людзі, — гэта тое, што закрыцце „памятае значэнне“. Болей тачны апіс ў тым, што яно зберагае доступ да прыўязкі зменнай. Якщо значэнне той зменной зменіцца прытаманна закрыццю, яно будзе бачыць новае значэнне — а не тое, якое існавало час яго стварэння.

Это класычныя пояснення працоўкі багоў у цыклах, якія стаюцься з var, але разработчыкі з большым дазнаўаннем часта сталкаюцца з болей тонкімі версіямі таго ж прыблуджэння ў асінхронным кодзе і логіцы UI. Калебэк можа чытаць об’ект налашчання, які змяніўся пасля пачатку аперацыі. Обработчык запуску можа выкарыстоўваць стан, які ўжо не актуальны. Функцыя з запазджэнням можа працаваць з текущым станам зміннага об’екта, калі разработчык прыпускаў, што вона будзе выкарыстоўваць об’ект такім, якім ён быў у момент планавання функцыі.

Фреймворкі з’яўляюць своія правіла жыцёвага циклу на адной з гэтых базовых структурах, што часта робіць эфект болей зноўклівым. Напрыклад, у React калектбек, створаны пад час аднаго конкрэтнага рендару, прыкладзяецца да значэнняў, якія існавалі пад час гэтага рендару. Гэта можа створыць атмосферу «застарэлага стану», навет які ўсё-такі базовы механізм калектбека працуе абсолютна так, як і планавалася. Неспакою ўсё таму, што люди чакаюць, што калектбек якімсь спосабам сам атрымае найновейшы стан — аднак калектбекі працуюць не так.

Правільны рашэнні залежыць ад таго, каму вы насправдэўна патрабуеце. Інодзе вам дасканаліваеся значэнне такое, якое існавала ў момент пачатку аперацыі, у такім случае правым крокам будзе зберагчы стабільны скрыншот з самага пачатку. Інодзе вам трэбуецца найсвежэйша доступная значэнне, для чаго патрэбны каштоўныя кансэнтры або функцыональныя апдэйты. А інодзе правым рашэннем ёст тое, каб зменяючыяся залежнасці перзначылі весь калбэк.

Корыстныя пытанне, якое трэба задаць, — гэта чы робота з адзячэнням павінна відбіваць стан света ў момент яго запланавання, чы стан света ў момент яго фактычнага выконання. Сам калбэк не мае ніякога дзеяння ў гэтым пытанні — яны верна зберагчаюць усе адносы, якія задаў ваш код, нават калі гэтыя адносы не былі тымі, якія вы хацелі стварыць.

JavaScript паводзіцца адносова — нашы ментальныя скорысткі — ні.

Ніякі з описаных тут прынцыпаў не ўзьміваюцца як довільныя особлівасці. const блакуе сама прыязначэнне, а не значэнне ў яй. Функцыя распрасцёрування стварае копію толькі на аднам роўні. Стандартная сортараванне масэв ператварае елементы спачатку на строкі. Абрабатчыкі Promise выконваюцца як мікрозадачы. Функцыя дэструктурыравання за замовчаннем реагуе толькі на undefined. Замыканні зберагаюць лексычныя прыязначэння, а не фіксаваныя значэнні. Кожны з эых прынцыпаў застосоўваецца языкам з абсалютным адпаведнасцю.

Няспакоўы выступаюць тады, калі разработчыкі паслугоўваюцца ментальнымі моделямі, якія простэйшыя, чым тое, што на самай жарадзе рэалізуецца. Мы кажам, што сталярная зменна „не можа змяніцца“, распрасцёрування „стварае копію“, асінхронны вызов „знаходзіцца ў try/catch“, а замыканне „памятае значэння“. Гэтыя спрасцавленні ўжытковыя, пакуль якаясь новая вимога не будзе залежаць самэ прыгожо ад тых деталяў, якія мы прыгледзелі.

Тое, што захарактэруе ўзкупленых разработчыкаў, — гэта не запамятованне стрэлкаваючага списку дробных фактов, а прыzwыканне ясна фіксаваць умовы ўсярод тады, калі данні пераходзяць за межы. Гэта значыць нормалізаваць значэнні, якія прыходзяць з зовнішняго света, спрыймать „відсутныя“ і „некоректныя“ данні як аднаецца панявы, ухіллявацца ад випадковай змены спяльных кансаўтацый, чытка адзначаць, хто адпавядае за обробку памылак у асінхронных операцыях, зберагаць первачны значэнне часоўых метак і часовых зонаў, а таксама заздалегідь вярнуццася да пытання, чы рэакцыя на запаздзелы калбэк павінна вестыся на старым чы на ныякім стане.

Стварэнне надзейныя JavaScript-коду не значыць ухіллявання ад кожнай гнучкай можнасці, яку прысвячае мова. Гэта значыць выкарыстоўваць гэтыя можнасці, не прыпускаючы, што ўзгодная сінтаксіс обяцвае больш, чым на самай працэ выдае.

JavaScript продовжае дзіваваць апытных разработчыкаў самэлькі таму, што апыт стварае довер'е да коду, які здаецца знайомым. Апасцярожнасць тут у тым, што синтаксіс, які здаецца знайомым, можа ўсё ж такі таямна скрываць рашэнні ўжо па канкрэтным звязкам, перакладу типаў, часе выканання і адпаведнасці за бягы — рашэнні, якія стануць виднымі толькі тады, калі ў системе змяніцца ўсё інша.

Язык, практычна завжды, рабіць тое, што яму было прадначана.

Дзіво заключаецца у тым, калі вы розумееце, канкрэтна ў чым ваш код яго прадначаў.

Спадневаная літэратура

  • Дзесяць частаўых прычын у JavaScript, якія таямна пашкоджаюць вашу базу коду — Характарызуецца дзесяцью распашчастымі проблемамі ў JavaScript і TypeScript, ад слабкай роўнасці да мутацыі стану, і паказвае безпечнейшыя способы для замены кожной з іх.