Astea Solutions организира своята трета и за първи път публична конференция: Astea Conference: Revolutions. Датата е 12 май 2018 г., мястото е Интер Експо Център.  „JavaScript: Ненужните Части“ бе сред най-запомнящите се лекции на Astea Conference 2017, а днес ви предлагамe ключови части от нея:

Човек и добре да живее, рано или късно му се налага да сортира масив от числа с JavaScript. За непросветените, това изглежда горе-долу така:

Е, нищо страшно. Всеки си има недостатъци. В крайна сметка, всяка година правят нововъведения в езика. Нормално е да очакваме такива дреболии постепенно да се изчистят. Да вземем, например, една относително скорошна функционалност – обещанията. Това е страхотна абстракция, решаваща многобройни проблеми вече над 40 години – както се казва, изпитана е с времето. Да направим една интересна аналогия със смъртните им врагове – отклиците (патриотичен превод на „callback“):

В какъв ред ще се появят буквите в конзолата? Може да ви подведе здравия ви разум, но буквите ще се изведат в обратен ред – първо „c“, после „b“ и накрая „a“. Защо „a“ и „b“ се извеждат в обратен ред – все пак и двете искаме да се изпълнят на следващата стъпка в цикъла от събития? Отговорът ще последва, но тук искам да вмъкна една проста житейска истина – новите стандарти на JavaScript не са особено по-логични, нито по-консистентни от старите. Ще си позволя да цитирам мъдреца XANi_ от Редит:

> Remember kids, just because it is in spec doesn’t mean it is not idiotic

Ще ви споделя няколко мои идеи за това как би могъл да се подобри езикът с минимални промени (разбира се, даже и тези промени са прекалено радикални…).

## Цикълът от събития (добре де, event loop)

Изключително фундаментален компонент от езика е асинхронността – основно защото нямаме паралелизъм. Ако трябва да сме точни, отскоро съществуват (донякъде) паралелни животни с гръмкото название „web worker“, но те не са част от езика. Ако ще влизаме в подробности, цикълът от събития също не е част от езика, а от средата, в която се изпълнява. Това, с което работи езика, са опашки („Job queues“), в които могат да се трупат задачи (една задача представлява функция с допълнителна метаинформация).

Близо 20 години средата, в която се е изпълнявал JavaScript, е определяла как да се държи цикълът от събития. С други думи, не самият език е предоставял механизми за асинхронност, а, като изключим частни случаи като process.nextTick в „node.js“, е било достатъчно човек да си представя само една голяма опашка, в която всички трупат асинхронни задачи (например със setTimeout с липсващ втори аргумент).

Но през 2015 година хората стават свидетели на чудо – второто пришествие във вид на софтуерен стандарт – ECMAScript 2015, още познат като ES6. Съвременното общество живее в ерата, която започва след като излиза този стандарт. Размерът в страници на ES6 се удвоява спрямо ES5, както и броят абсурди. Що се касае до опашките със задачи – сега JavaScript стандартът определя цели две опашки – „ScriptJobs“ за „обикновени“ задачи (добавяни с де-факто-стандартни функции като setTimeout/setInterval) и PromiseJobs са, очевидно, обещания. В ES5 стандарта въобще не се говори за опашки, нито за задачи.

Въвеждането на различни опашки за различни цели вероятно е била породена от концепциите за task/microtask от браузърните стандарти – общо взето, на всяка стъпка на цикъла от събития се изпълнява една задача (task), тоест един фрагмент блокиращ JavaScript, след което се изпълняват всички натрупани микрозадачи (microtask), които са отново фрагменти блокиращ JavaScript, и чак тогава браузърът има време за преизчисления, прерисувания и тем подобни – понеже, на теория, всичко трябва да се случва на една нишка. Малко псевдокод:

Дали има нужда от task и microtask опашки в браузърите е дълбоко философски въпрос. Но дали трябва JavaScript опашката ScriptJobs да съответства на браузърната опашка task, а PromiseJobs – на microtask опашката? Бих казал твърдо нье, но, за съжаление, това е такa.

Накратко – функциите, закачени към обещания, ще се изпълнят преди функции, закачени със setTimeout с втори параметър 0 (или липсващ втори параметър).

Очевидно хората искат по-добър контрол над това кои функции кога се изпълняват. Но дали това е начинът? Гадателските ми способности предвиждат наплив от неловко добавяне на всякакви функции към microtask опашката посредством обещания през следващите няколко години – също както в момента setTimeout без втори параметър се използва по повод и без повод.

Каква възможност за подобрение виждам? Въвеждането на интерфейс като TaskQueue.enqueue(fun)? Предимствата на такъв интерфейс биха били, че вградените опашки на езика не са обвързани по абсурден начин с браузърските опашки (и да, node.js-ските опашки работят по подобен начин) и че има метод enqueue с ясно предназначение. Това би позволило и добавянето на допълнителни методи, например TaskQueue[Symbol.iterator], който да позволява на човек да итерира натрупаните задачи.

Обекти

Обектите в JavaScript се използват за две цели – като анонимни структури от данни и като речници. За щастие, има и по-добър кандидат за речници – вграденият тип Map (от ES2015 стандарта). За съжаление, този тип има други проблеми, за които ще спомена по-нататък.

Но сега, да се опитаме да решим проблем, който не би трябвало да съществува, а именно липсата на грешка при достъпването на несъществуващи полета. Ако смятате, че това не е проблем, вероятно не сте се борили достатъчно дълго време с undefined.png. Да вземем базовия случай:

Един хитър подход е да се възползваме от прокси обектите, добавени в ES2015:

След което, вместо обекта „john“ използваме „johnProxy“:

Този подход се оказва даже достатъчно хитър за

Създаването на допълнителни обекти оставя малко неприятен вкус в устата, но всъщност е напълно поносимо. Защо обаче допълнителните прокси обекти са необходими, а не можем да използваме символи, за да предефинираме това поведение? Историята мълчи. Подходът с проксита е приемлив, но е достатъчно неловък, че да не се използва масово.

Като стана дума за метапрограмиране, едно интересно решение от ES2015 стандарта е да се позволи да се „скрият“ полета от „with“ блоковете по доста ясен начин:

Къде е тънкият момент? with блоковете са забранени в ‘strict mode’, а именно това е режимът, в който се изпълняват всички ES2015 модули. С други думи, в този стандарт е добавена функционалност, която е предназначена за случаите, когато искаме да използваме съвременна версия на JavaScript, но същевременно не искаме да използваме модули, които вече са де-факто стандарт. За да бъде добавена такава функционалност в езика, очевидно на някого много му е трябвало да използва този частен случай.

Но да се върнем към проблема с обектите. Как могат да се подобрят те?

  • Както видяхме от предните два примера, в ES2015 са добавени два нови начина за метапрограмиране – символи и проксита. Защо има нужда и от двете? И защо има нужда двете да покриват различна функционалност? Не открих добри отговори, затова смятам, че само символите трябва да останат и да покриват функционалността и за двете.
  • Предефиниране на повече оператори. В момента могат да се предефинират само особено специфични оператори – например instanceof – но не и операторите != и == (които и без това не са популярни), аритметичните оператори, както и предефиниране на хеширащото поведение.
  • Да се добави вграден вид обекти, които да проверяват за липсващи полета, вместо да следва идеологията на отбранителното програмиране.
  • В началото бе словото. После – масивите. Сега има итератори, но няма нито map, нито reduce, нито zip, нито още една камара функционалност, която е необходима на хората за работа с итератори. А това не е начинът, по който се окуражава използването на нововъведения. Не би навредил и по-приятелски синтаксис за Map, вместо new Map([['key', 'value']]).

Числа

Ако някога сте се чудили до какво довежда инатът, помислете си за числата в JavaScript. Има единствен числен тип, еквивалентен на „float64“ в някои други езици. Същевременно, има много функционалност в езика, която разчита на цели числа. Даже няма нужда да се ровим на дълбоко – да си помислим за индексиране на масиви. Според стандарта, индекс на масив може да бъде всяка стойност „p“, удовлетворяваща ToString(p) === ToString(ToUint32(p)) && ToUint32(p) != 2 ** 32 - 1, тоест само 32-битови цели числа без знак (с изключение на 2^32 – 1). Защото JavaScript поддържа само 32-битови цели числа, окомплектовани в 64-битови числа с плаваща запетая. Какво се случва, ако присвоим елемент с индекс „2 ** 32“ на масив? „Прекалено големите“ индекси се смятат за обикновени полета, нямащи общо с масива отдолу. Но това е друга тема. Да се върнем към числата.

Фундаменталният въпрос е – защо не можем да имаме хубави неща в живота? Защо стандартизиращият комитет толкова инатливо се бори против целите числа, a същевременно стандарта ги използва? Защо няма целочислено делене? Защо побитовите операции се прилагат над 32-битова част от 64-битовите числа с плаваща запетая, при условие, че всъщност има място за 52-битови цели числа? И не, “на процесорите така им е удобно” не е добър отговор в случая.

Езици като Ruby и Python имат типове, които обобщават цели числа с фиксирана (32/64 бита) и произволна дължина (т.нар. BigInt). Даже и това решение да е прекалено „сложно“ за JavaScript, липсата на отделен тип за 64-битови цели числа е напълно неоправдано.

null

null има null смисъл. В някои езици няма отделни стойности за false и/или null, а вместо това се използва 0. За сравнение, в JavaScript имаме 0 и false, но имаме и null и undefined. Каква е разликата между null и undefined? Има много разлики, но нито една от тях не е достатъчно съществена да оправдае съществуването и на двете. null се използва на доста места в спецификацията, но предимно за „вътрешни“ операции в езика.

Единственото полезно приложение, където null е незаменимо, е създаването на обект без прототип – Object.create(null). Защо би било полезно това? При тези обекти няма частни случаи при in оператора (с други думи, 'valueOf' in Object.create(null) не е изпълнено, докато 'valueOf' in {} е), което ги прави подходящи за речници в случаите, когато не може да се използва Map. Това е място, където null не може да се замени с undefined (защото тогава Object.create хвърля грешка), но няма и причина това да не се разреши, ако се премахне изцяло null. Защо да се маха null, а не undefined? Два често-срещани примера за безумие:

и, по-сочният пример, typeof null === 'object' е изпълнено, въпреки, че null не е нито инстанция на Object (ако трябва да сме прецизни, Object.create(null) също не е), нито може да се индексира, нито е reference тип, нито се държи като обект в каквото и да е друго отношение. Носят се слухове, че това всъщност е бъг и е следствие от факта, че JavaScript е създаден за 10 дни.

Според мен това е достатъчна обосновка да изпратим null в /dev/null. Как да стане това? Най-безболезненият вариант, за който се сещам, е да се забрани използването на ключовата дума null в по-силен вариант на strict mode и да се хвърля грешка, когато се достъпва променлива с тази стойност.

## Type coercion (тук нямам превод, освен „насилване на типове“)

Да започнем с екскурзия в света на оператора ==:

Чувал съм, че тройното равно било по-добро:

Както виждаме, чудовището, наречено „type coercion“, превръща произволни типове в произволни други типове по не особено прости правила. Добрата новина е, че имаме минимален контрол над това:

  • методът toValue ни позволява да предефинираме поведението на обект при превръщане към число.
  • методът toString ни позволява да предефинираме поведението на обект при превръщане към низ.
  • методът Symbol.toPrimitive се използва едновременно за превръщане към число, низ или булев тип.

Същевременно, нямаме никакъв контрол над това кога езикът решава да направи от числото низ (каквото всъщност се случва при сортирането на масив от числа), което прави горните евристики недостатъчно утешение за хората с непоносимост към слабата типизация. Как да се подобри езикът? Да се забрани type coercion (освен, може би, в условни изрази и при индексиране на обекти) в по-силен вариант на „strict mode“.

Извод

След тази статия ще прозвучи иронично, но не смятам, че JavaScript е инструмент на дявола. Разбира се, човек не бива да очаква много от език, създаден за две седмици с изискванията хем да прилича на Java, хем да не прилича на Java. Но, както казах в началото, нововъведенията в езика не спомагат за славата му. Не бива и да го мразим от все сърце – относително лесно може да се пише елегантен JavaScript. Според мен не е редно да го възприемаме като нещо повече от посредствен език с посредствени проблеми.

И да, проблемите на езика са предимно социални – от една страна много JavaScript библиотеки и инструменти чупят обратна съвместимост през няколко месеца, защото хората зад тях постоянно откриват нови парадигми, а от друга страна доста хора отчаяно се опитват да се научат да програмират по метода на пробата и грешката (няма да правя коментари за сечението на двете множества). Като резултат получаваме хаос – а каква промяна тогава да очакваме за езика?

 

Янис Василев е един от лекторите на Astea Conference през 2017-та година. За себе си казва, че е програмист, полу-статистик, самопровъзгласен музикант и дървен философ. Той обича спането, бананите и борбата с присъщите човешки предразсъдъци. Харесва лога на конференции на тениските си и капка хаос в програмите си.

 

 

 


Стани част от потребителските групи на DEV.BG. Виж всички потребителски групи и избери най-интересните за теб.

Прочети още:
Да ти повярват, че можеш – кратка история за мотивацията и за вътрешната комуникация
Разговор за JavaScript с Мартин Чаов

 

Share This