Пашчотныя прынцыпы JavaScript і TypeScript, якія таямна ламаюць код
Трапы ў JavaScript і TypeScript, якія выглядаюць правільна, але прычыняюць проблемы: порэйнаванні NaN, прымусовая зміна типу, проблемы з часам выканання async/await і перакананні ў роўнасці, якія вводзяць у глухі кут.
Крыніцы: 20 JavaScript/TypeScript Mistakes That Look Right… But Break Your App
Пасткі зусібачэння і роўнасці
1. NaN ніколі не роўна нічаму, нават сабе самаму.
[1, NaN, 3].indexOf(NaN); // -1, even though NaN is right there
За спецыфікацыяй, NaN === NaN дае рэзультат false, таму будь-які механізм, заснованы на строгай роўнасці — укладаючы indexOf — не зможа яго знайсці, нават калі ён знаходзіцца праўда тут, у масе. У такім случыку трэба вжываць Number.isNaN(value) альбо .includes(), які спецыяльна створаны для правильнага адкуплення такога крайньего варыянту.
2. typeof null вяртае "object".
if (typeof value === "object") { value.someMethod(); } // value could be null
Это старая особеннае, яка ўжо часть самай мовы, і яна паказвае ся ў тых, хто вважае typeof надзейным спосабам пераканацца, чы ўсё-такі гэта об’ект. null праходзіць через такую пераканальную процедуру, але выклікае крах як толькі прабуеш прызваць на нем метод.
3. Порэшчанне об’ектаў за дапамой JSON.stringify() залежыць ад порядку.
JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 }); // false
Два об’екты, якія представляюць тыя ж данні, можу быть серыялізаваны ў строкі з ключамі в разным порядку — гэта прыродна выходзіць пасля працы з базай дадзеных або іншай часткай вашага коду. Адмовіцеся ад порэшчання страк і вамкніце настойчывую функцыю для пераканацца ў глыбокай роўнасці.
4. .sort() без компаратара спрацоўвае з усім як з тэкстам.
[10, 2, 1].sort(); // [1, 10, 2]
Значэнні спачатку ператвараюцца ў строкі, а пасля порৱнююцца символ за символам, што спяшчае правільную сортаванне числаў. Заўжды выкорыстоўвайце явную функцію-паруравнавальнік для числаў, напрыклад (a, b) => a - b.
Прымусовая трансформацыя типаў
5. Аператар + пераходзіць на з’еднанне строкаў як толькі адны з операндаў ёсць строка.
function total(price, quantity) {
return price + quantity; // "5" + 3 = "53" if price came from a form input
}
Значэнні, якія прыходзяць з параметраў запита, поль формы або парсаванага JSON, часта застаюцца строкамі, нават якшо выглядаюць як числа. + не будзе ператвараць строку ў число — у замест на гэта ён ператварае число ў строку.
6. Значэнні за замовчаннем пад час деструктурыравання прымаюцца толькі калі атрыбут ёсць undefined, ніколи калі ён ў стане null.
function greet({ name = "friend" } = {}) { return `Hi, ${name}`; }
greet({ name: null }); // "Hi, null" — the default never kicks in
Явна перадача null лічыцца як заданне значэння, а не як його прымусовая відсутнасць, таму альтернатыўны стандартны варіант выкарыстоўвання зовсім не працюе. Якщо вызывачы можаць законна адправіць null, трэба пераканацца ў його наявнасці окрема.
Асінхронна таймінгавання
7. Выконанне .filter(Boolean) над масавым об’ектам з прагненнямі нічога не дае.
const results = items.map((item) => checkAsync(item)); // array of Promise objects
const valid = results.filter(Boolean); // every Promise is truthy, filters nothing
Экземпляр Promise завжды є істінным, незалежна ад таго, каму ён у падсумку будзе перетворыцца чы ўспіша завершыцца. Перш заўсёды трэба яго завершыць за дапамогою await Promise.all(results), а толькі пасля чаго фільтруваць отрыманыя значэння.
8. Опцыйнае лануванне зупіняе адыскрыцьце всей выразаў, а не толькі опцыйнай часткі.
user?.getPermissions().includes("admin"); // if user is null, this is undefined, not an error, but also never calls .includes at all
Хочацца думаць, што прыкладзеўваецца толькі фрагмент ?., але ўсё, што прыўязана пасля яго, таксама перастае выконваліцца. Зазвычай гэта і є бажаная поведзенчасць, але гэта стае тонкім багам, калі які-небудзь заканчовы вызыв — напрыклад, для логавання чыў аналітыки — тыха ніколи не выконваецца.
9. Promise.all відкидае ўсе рэзультаты як толькі хоць адна з обяцаў не выйшла.
const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
// if taskC rejects, you get nothing back from taskA and taskB either, even if they succeeded
Гэта ўмышленая поведзенчасць «всё альбо нічога». Калі ў вашай ситуацыі дапускаецца частковая успешнасць, Promise.allSettled вяртае рэзультаты кожной обяці — незалежна ад таго, чы ўсё працавала чы ні — замест таго, каб відкинуць успешныя рэзультаты чераз неудачу адной задачы.
Мутацыя об’ектаў і масэв
10. const блакуе толькі прыязанне, а не сам об’ект, на які ён вяртаецца.
const config = { retries: 3 };
config.retries = 10; // completely legal, const never protected this
Заявленне чаго-небудзь з const толькі не дазволяе перазначыць гэтыя зменную на іншую значэнне. Об’ект, да якога ён аб’явлена, застаецца такім жа зменным, як і ў разе викорыстання let. Якщо вам патрэбна справжня незменнасць, викорыстаўце Object.freeze() — і памятайце, што гэта фіксуе толькі на аднам роўні.
11. .sort() зміняе первасны масэвы, на вядомаць ад .map() чы .filter().
const original = [3, 1, 2];
const sorted = original.sort();
console.log(original); // [1, 2, 3] — also changed, probably not what was intended
Лёгка прыпускать, што всі методы масэвых вяртаюць новы масэў, адколі большасць распашчытных методаў робяць самэ гэта. .sort() і .reverse() ўзьяўшыся за аднаковасці, і зміна масэвага, да якога ўсё ўсё ж існуе канектар у іншай частцы вашага коду, ведае да класычнай проблемы „гэта значэнне змянілася сама по сабе“.
12. for...in таксама пераглядае спадзічаныя атрыбуты, а не толькі сабе ключы об’екта.
Array.prototype.customMethod = function () {};
const arr = [1, 2, 3];
for (const key in arr) { console.log(key); } // includes "customMethod" too
У практыцы гэта рэдкасцю калі-небудзь створыя проблем, адколі расшырэнне вбудованых протатэпаў ёсць нечастым, але будзь-яны перагляджальныя атрыбуты, спадзічаныя з ланцуга протатэпаў, будуць практычна ў цыкле for...in. Іспользованне Object.keys() або for...of памогае цэлком ухіліцца ад такой неспадзяванкі.
Функцыі і дыямэнт
13. Функцыі-стрэлкі ніколі не прыяжджаюць сабе this.
const counter = {
count: 0,
increment: () => { this.count++; }, // `this` here is NOT the counter object
};
Функцыя-стрэлка берае this з лексычнага дыямэнта, дзе яна была напісана, а не з об’екта, які ёй калі-небудзь вызывае. Гэта робіць іх ідеяльнымі для калебакаў, якім патрэбна можлівасць выкарыстоўваць зовнішні this, але яны не падходзяць для методаў об’ектаў чы класаў, якія должны асабліва ссылацца на свой экземпляр.
14. Калі вы прытупіце break, оператываец switch праходзіць далей без жадных адзінакоў.
switch (status) {
case "pending":
console.log("Order is pending");
case "shipped": // no break above, this runs too
console.log("Order has shipped");
}
Якщо прытупіць адзін break, то выконваюцца ўсе наступныя кейсы, незалежна ад таго, падходзіць іх умова да заданых параметраў чы ні. Намераваны праходжэння далей іноды трапляецца, але гэта настолькі рэдкасць, што відсутнасць break практычна завжды ўскладнюе ситуацыю і свідчыць пра баг, а не пра выбор у дизайне.
Даты і ціфры
15. 0.1 + 0.2 не ўравнаеся 0.3.
0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004
Гэта вынікае з стандартнай формы адображэння дробяў у комп’ютерах, і гэта стосуецца практычна всіх мов, а не толькі JavaScript. Штонайлепш утрымайцеся ад выкарыстоўвання оператара строгай равнасці для вырахоўваных дробявых значэнняў; у замен пераканайцеся, чы разлік между двумя ціфрамі падпадае пад допустны ліміт тольеранцы.
16. Строкі з датай працуюць у формате UTC, тады як даты, роздзеленыя слэшамі, працуюць за местным часам.
new Date("2024-01-01"); // midnight UTC
new Date("01/01/2024"); // midnight local time
Две строкі, якія здаюцца описваюць тую ж дату, можа практычна відбіваць разныя календарныя даты залежна ад часовага поясу таго, хто запускае код. Выберыце адзін формат даты і строго прытрымайцеся яго, а таксама викорыстоўвайце перавераную бібліятэку для працы з датамі, а не прабуйце парсаваць строкі вручную.
Спецыфічныя проблемы TypeScript
17. Асерціі типу не пераканальваюць нічога пад час выконання.
const user = JSON.parse(rawData) as User; // TypeScript trusts you, nothing is actually checked
Запіс as User паведамляе кампайлеру абсалютна даваць вам довяроўваць, і ён так і робіць, не выканаўшы жадных пераканальваў, чы распарсаваная значэнне дзейсна мае тую форму, якую вы заявілі. Некалькі або неспадзеваныя даны продовжаюць праходзіць через вашу программу як пераканальваныя, пакуль ў канцэ наступнае не спрычыніт праблему.
18. Аператар асэрцыі non-null спакойна толькі стуманава паведамленні пра апазыранне, ён не зменяе рэальнасць.
function getName(user?: User) {
return user!.name; // TypeScript stops warning you; the runtime null check is still gone
}
Дадзенне ! паведамляе TypeScript, што вы впэўнены, што значэнне не ёсць ні null, ні undefined. Якщо гэта прыпуск некорактны, кампайлер проста перестае адзначаць рызык, і апазыранне выявляецца пад час выканання, адносова там, дзе раней система типаў могла яго зловіць.
19. Перакананне на наявнасць зайвых атрыбутаў дыяжыць толькі для літералайнаў об’ектаў, якія пасылаюцца безпосередна, а не для зменных, якія маюць той самы формат.
type Config = { retries: number };
function setup(config: Config) {}
setup({ retries: 3, timeout: 5000 }); // error: 'timeout' does not exist on type Config
const options = { retries: 3, timeout: 5000 };
setup(options); // no error — same excess property, but it slips through
Якщо передаць об’ект-літерал безпосередньа ў вызов функцыі, TypeScript пераглядае кожную яе власнасць і адхоўляе будзь-які дадатковы элемент. Якшо спачатку прызначыць гэты ж об’ект да зменнай, а потым передаць саму зменную, то такая пераглядка выключаецца цэлкам. Це справжняя особлівасць працы перакладчика типаў, якая значыць, што памилка напісання або непатрэбны поле могу таямна працягнуцца праз перакананне, залежна ад таго, як значэнне прайшло да функцыі.
20. Чыслаўскія enum-ы прыменяюць будзь-якія числы, а не толькі ўказаныя ў іх элементы.
enum Status { Pending, Shipped, Delivered }
function process(status: Status) {}
process(999); // no compile error, even though 999 isn't a declared Status value
У сэрцы TypeScript лякучыя значэння-чысла ёсць проста чысламі, таму система типаў будзе прымаць любое чысла як правядзячае, незалежна ад таго, чы яно падходзіць да заявленага элемента. Лякучыя значэння-строкі вводзяць рэальныя обмежэння, таму калі вам трэба, юначык насправды адхіліць некоректны ввод, краща выкорыстоўваць лякучыя значэння-строкі або ўніюн, створаны з літералай строк.
Ні адна з гэтых двадцяга проблем не ўскладненая чы адзінакавая. Кожная з іх настолькі распашчастая, што можа з’явіцца ў рэальным, пераглянутым і запусканым кодзе, адносна таго, што яна выглядае абсалютна звычайным JavaScript чы TypeScript, пакуль конкрэтны ввод, час або крайнія случаў не выявяюць яе. Адчуванне гэтага спісу не заменяе патрэбы рэтельна теставаць ваш код. Гэта дае вам прытуп: вы будете пераканальвацца на проблемы, якія вже знаеце, куды старацца, а не натрапляць на яны пазней у працэйнай сіткі.