Най-трудните предизвикателства на най-трудната JavaScript тема за разработчиците.

Ако попитате разработчик: „Коя е най-трудната JS тема за вас?“, никога няма да чуете, че това са ES6 модули. Но статистиката знае по-добре! Преброихме броя на неправилните отговори на тестове в нашия телеграм канал по различни теми и открихме, че модулът ES6 е един от анти-лидерите. Други ТОП-5 най-трудни теми за разработчиците можете да видите тук.

Изглежда, че много разработчици смятат, че ES6 модулите не са нищо повече от ключови думи export — import. Всъщност е много по-разнообразно. Той има мощни функции и хлъзгави клопки, за които малко разработчици знаят. В тази статия ще разгледаме всички тях, използвайки викторините на нашия телеграм канал като примери. Този път няма да класираме задачите в горната част по броя на верните отговори, тъй като обясненията за някои задачи може да се основават на предишните, но за по-голям интерес ще посочим процента на разработчиците, които са отговорили на теста правилно. Отивам!

Тест #1. 53% верни отговори

„Опитайте сами“

Първо, нека си припомним цялото разнообразие от синтаксис за импортиране и експортиране:

Ако проверите таблицата със синтаксис за импортиране, ще видите, че няма синтаксис, съответстващ на нашия код:

import { default } from ‘./module.mjs’;

Защото този синтаксис е забранен. Кодът на теста извежда следната грешка:

SyntaxError: Неочаквана запазена дума

В ред import { default } from ‘./module.mjs’; default е името на експорта и името на променлива в този обхват, което е забранено, тъй като default е запазена дума. Поправката е доста лесна:

import { default as foo } from ‘./module.mjs’;

Сега default е името на експорта, а foo е името на променлива. С други думи, ако искате да използвате именуван синтаксис за импортиране за експортиране по подразбиране, трябва да го преименувате. Това е, толкова просто!

Викторина №2. 35% верни отговори

„Опитайте сами“

Важният нюанс, за който не много разработчици знаеха, е, че вносът е увеличен. Тоест, те се повишават, докато двигателят анализира кода. Всички зависимости ще бъдат заредени преди кодът да се изпълни.

Ето защо ще видим регистрационни файлове в следния ред:

helper.js, index.js, 3

Ако искате някакъв код да бъде изпълнен преди декларация за импортиране, помислете дали да го преместите в отделен файл:

Сега имаме очаквания резултат:

index.js, helper.js, 3

Тест #3. 42% верни отговори

„Опитайте сами“

Модулите са единични.

Без значение колко пъти импортирате модул от едно и също място или от различни места, модулът ще бъде изпълнен и зареден само веднъж. С други думи, има само един екземпляр на модула.

Тест #4. 34% верни отговори

„Опитайте сами“

В коментарите към предишната ни статия, в която събрахме най-трудните викторини от нашия канал, някои написаха, че не смятат за необходимо да знаят езика толкова задълбочено. Е, не съм съгласен. Вярваме, че колкото повече знаете, толкова по-ефективни сте (при други равни условия). Това твърдение е многократно доказано в практиката на нашата компания.

Може би тази функция е една от онези, за които ще се пише - „защо трябва да знаем това“. И наистина е малко вероятно да го използвате всеки ден. Но един ден ще ви бъде от полза и колко прекрасно е да спестите време, което бихте прекарали в Google.

Така според MDN:

Обектът import.meta излага на JavaScript модул специфични за контекста метаданни. Съдържа информация за модула.

Той връща обект със свойство url, указващо основния URL адрес на модула. Това ще бъде или URL адресът, от който е получен скриптът, за външни скриптове, или основният URL адрес на документа, съдържащ документ, за вградени скриптове.

Имайте предвид, че това ще включва параметри на заявката и/или хеш (т.е. след ? или #).

Тест #5. 45% верни отговори

„Опитайте сами“

Друг изключително важен момент, който повечето разработчици пропускат, е, че импортираните променливи се държат като константи в обхвата на импортиращия модул.

За да накарате кода да работи, можете да експортирате, например, обект и да промените неговото свойство.

Тест #6. 11% верни отговори

„Опитайте сами“

Този тест изглежда има най-малък брой верни отговори. От една страна, това е очаквано, защото това е ъглов случай. От друга страна, ако останете фокусирани малко повече, ще видите, че задачата всъщност се основава на фундаментални неща, които всеки трябва да знае (и вероятно знае, просто не успя да ги разпознае).

На първо място това

export default function foo() {}

е равно на

function foo() {}
export { foo as default }

и бихме искали да кажем, че това също е равно на

function foo() {}
export default foo

но не е... Не се изненадвайте, просто продължете да четете. Ще се върнем към това в следващия тест.

И сега е време да запомните, че функциите се издигат и инициализирането на променлива винаги върви след декларация на функция/променлива. Не е ли това основата на основите?

След като двигателят обработи кода на модула, той изглежда така:

Така резултатът от теста е number.

Тест #7. 17% верни отговори

„Опитайте сами“

В повечето случаи импортираните данни са активни. Тоест, ако експортираната стойност се е променила, тази промяна се отразява в импортираната променлива.

Но това не е вярно за експортирането по подразбиране:

export default foo;

Когато използвате този синтаксис, не експортирате променлива, а нейната стойност. Можете да експортирате стойности по подразбиране, без изобщо да използвате променливи, като това:

export default ‘hello’;
export default 42;

Ако погледнете таблицата със синтаксиса за експортиране от тест #1, ще видите, че export default function () {} е в различна колона (Default export) от export default foo (Export of values).

Това е така, защото те се държат по различен начин, функциите все още се предават като препратки на живо:

Нека отново да разгледаме таблицата за износ.

export { foo as default }; е в колона Named Export, която е различна и от двете. Но за нас единственото важно нещо е да не е в графата Export of values. Така че това означава, че когато експортирате данни по този начин, това ще бъде обвързване на живо с импортираната стойност.

Тест #8. 40% верни отговори

„Опитайте сами“

Редът import { num } from ‘./module2.mjs’; ще изведе грешка, тъй като конструкцията за импортиране трябва да е на най-високото ниво на скрипта:

SyntaxError: Неочакван токен „{‘

Това е важно ограничение, което, заедно с ограничението за използване на променливи във файловите пътища, прави модулите ES6 да бъдат статични. Това означава, че не е нужно да изпълнявате код, за да откриете всички зависимости между модулите, за разлика от модулите Common.js, които се използват в Node.js.

В този пример, който използва модули Common.js, за да разберете кой модул a или b ще бъде зареден, трябва да се изпълни следният код:

Статичният характер на модулите има много предимства. Ето някои от тях:

  1. Винаги знаете точната структура на импортираните данни. Това помага на линтърите да намерят правописни грешки, преди да изпълнят кода.
  2. Асинхронно зареждане. Тъй като модулите са статични, можете да заредите импортирания, преди да изпълните тялото на модула.
  3. Поддръжка на кръгови зависимости. Ще проучим тази възможност по-подробно в следващия тест.
  4. Ефективно групиране. За да не говорим много по тази тема, можете да видите сами как пакетът Rollup може ефективно да изгради ES6 модули в тази статия.

В ES6, ако трябва да заредите модул условно, можете да използвате конструкцията, подобна на функция import(), която ще бъде подчертана в следващите тестове.

Тест #9. 33% верни отговори

„Опитайте сами“

В кода по-горе можем да видим кръговите зависимости: index.mjs импортира функциите double и square от module.mjs, докато module.mjs импортира функцията calculation от index.mjs.

Този код работи, защото модулите ES6 по своята същност поддържат кръгови зависимости доста добре. Например, ако пренапишем този код, за да използва модули Common.js, той вече няма да работи:

Това е често срещана „болка“ в Node.js. Нека видим как всъщност работи този код:

  1. index.js започва да зарежда
  2. зареждането е прекъснато на първия ред за зареждане module.js:

const helpers = require(‘./module.js’);

3. module.js започва да се зарежда

4. на ред console.log(actions.calculate(3)); кодът извежда грешка, защото actions.calculate не е дефиниран. Това е така, защото Common.js зарежда модулите синхронно. index.js все още не е зареден и обектът му за експортиране в момента е празен.

Ако извикате импортирана функция със закъснение, модулът index.js ще има време да се зареди и кодът ще работи съответно:

Както знаете от предишния тест, модулите ES6 поддържат кръгови зависимости, защото са статични — зависимостите на модула се зареждат преди кодът да бъде изпълнен.

Друго нещо, което кара горния код да работи, е повдигането. Когато се извика функцията calculate, все още не сме били на линия с нейната дефиниция.

Ето как изглежда кодът след групиране на модулите:

Няма да работи без повдигане.

Ако променим функцията за декларация на изчислението на израз на функция:

ще изведе следната грешка:

ReferenceError: Няма достъп до „изчисляване“ преди инициализация

Тест #10. 31% верни отговори

„Опитайте сами“

Изчакване от най-високо ниво е супер полезна функция, за която много разработчици не знаят, може би поради факта, че беше въведена съвсем наскоро, в ECMAScript 2022. А, това е хубаво!

Според предложение за очакване на tc39 от най-високо ниво:

Изчакването от най-високо ниво позволява на модулите да действат като големи асинхронни функции: С изчакване от най-високо ниво ECMAScript модулите (ESM) могат да изчакват ресурси, карайки другите модули, които ги импортират, да изчакват, преди да започнат да оценяват тялото си.

Стандартното поведение на модулите е, че кодът в модула не се изпълнява, докато всички модули, които той импортира, не бъдат заредени и техният код не бъде изпълнен (вижте Тест #2). Всъщност с появата на чакането от най-високо ниво нищо не се променя. Кодът в модул не се изпълнява, докато не бъде изпълнен целият код в импортираните модули, само че сега това включва изчакване всички чакани обещания в модула да бъдат разрешени.

Изход:

module.js
module.js: promise 1
module.js: promise 2
index.js
num = 20

Ако премахнем чаканията от редове 5 и 13 в module.js и добавим изчаквания във файла index.js по следния начин:

изходът ще бъде:

module.js
index.js
num = 5
module.js: promise 1
timeout num = 10
module.js: promise 2
timeout num = 20

Ще се върнем към функцията за изчакване от най-високо ниво в бъдещи тестове.

Тест #11. 16% верни отговори

„Опитайте сами“

Според MDN:

Извикването import(), обикновено наричано динамично импортиране, е израз, подобен на функция, който позволява асинхронно и динамично зареждане на ECMAScript модул. Тя позволява да се заобиколи синтактичната твърдост на декларациите за импортиране и да се зареди модул условно или при поискване.

Тази функция беше въведена в ES2020.

import(module) връща обещание, което изпълнява към обект, съдържащ всички експорти от модула.

Тъй като import(module) връща обещание, за да коригираме кода на теста, трябва да добавим ключова дума await преди извикването за импортиране:

Тук отново използваме чакането от най-високо ниво, което ни напомня за готината на тази функция.

Сигурен съм, че поне веднъж приложението ви се е сринало с грешка:

SyntaxError: await е валиден само в асинхронни функции

Това често се случва, когато се опитвате да извикате асинхронна функция от глобалния обхват. За да разрешим този проблем, трябваше да избягваме грозното:

Това не само е грозно, но може да причини грешка в случай на използване на този модел за асинхронно зареждане на модули. Например:

При импортиране на module1.mjs какъв ще бъде резултатът от num — стойността от module2 или undefined? Това ще зависи от това кога ще бъде достъпна променливата:

С изчакване от най-високо ниво, когато имате достъп до num, импортирано от module1, то никога няма да бъде undefined:

Тест #12. 21% верни отговори

„Опитайте сами“

Горният код ще изведе грешка:

TypeError: Не може да преобразува обект в примитивна стойност

Съгласете се, доста неочаквана формулировка за грешка. Нека да разберем откъде идва тази грешка.

В тази част от кода използваме динамично импортиране, което вече срещнахме в предишния пример. За да разберем проблема в този код, трябва да разгледаме по-отблизо върнатата стойност на import().

Стойностите на променливите module1 и module2 не са това, което очаквахме. import() връща обещание, което изпълнява към обект със същата форма като импортиране на пространство от имена:

import * as name from moduleName

Експортът default е наличен като ключ с име default.

Така че вместо стойностите 1 и 2, имаме обекти { default: 1 } и { default: 2 } ​​в променливите module1 и module2 съответно.

Е, защо получаваме такава странна грешка при умножаване на два обекта, а не NaN, както сме свикнали?

Това е така, защото върнатият обект има null прототип. Следователно той няма toString() метод, който се използва за преобразуване на обекти в примитиви. Ако този обект имаше прототип Object, щяхме да видим NaN в конзолата.

За да коригираме кода на теста, трябва да направим следните промени:

or

За да прочетете повече за import(): MDN.

Тест #13. 17% верни отговори

„Опитайте сами“

Синтаксисът export * from ‘module’ реекспортира всички наименувани експорти от файла ‘module’като именувани експорти на текущия файл. Ако има няколко експортирания с едно и също име, нито едно от тях не се експортира повторно.

Така че изпълнявайки този код, ще видим undefined в конзолата. Само 17% от отговорилите отговориха правилно на този тест и по-голямата част от отговорилите (59%) смятаха, че този код ще изведе грешка. И наистина това тихо прекъсване не изглежда типично за строг режим. (Както си спомняме, JavaScript модулите са автоматично в строг режим.) Кажете ми в коментарите, ако знаете причината за това поведение.

Между другото, ако в същата ситуация изрично импортираме x, ще имаме грешка, както се очаква:

import { x } from ‘./intermediate.js’;

SyntaxError: Заявеният модул ‘./intermediate.js’ съдържа противоречиви експорти на звезди за име ‘x’

В заключение.

Както винаги, искаме да ви насърчим да продължите да учите езика, на който пишете всеки ден и да го направим по-добър!

Абонирайте се зателеграмния канал, за да станете човекът, който знае всичко в офиса.

Следвайте ни вMedium, за да не пропускате нови неща.

Следвайте ни вLinkedInза други страхотни неща, които правим.

Споделете в коментарите — в коя тема от JavaScript мислите, че разбирате най-лошо? Може би следващия път ще посветим статия на това.