May 20th, 2021

Учебник по JavaScript: ошибка в примере, eval, use strict

Начало: Учебник по JavaScript: как устроены примеры, движок учебника, Prism.js.

На разбор устройства примеров в учебнике по JavaScript в прошлом посте у меня ушло довольно много времени. Зачем мне это понадобилось? Дело в том, что при запуске первого примера из подраздела 1.1 «Браузерное окружение, спецификации» второй части учебника со страницы самого этого подраздела выдается ошибка: «TypeError: window.sayHi is not a function». Вот код примера:
function sayHi() {
    alert("Hello");
}

// глобальные функции доступны как методы глобального объекта:
window.sayHi();

Я скопировал этот скрипт к себе на компьютер и запустил в браузере. Ошибки не случилось, код отработал так, как предполагалось. Поэтому я и стал разбирать устройство примеров в учебнике, ведь очевидно, что ошибка возникает где-то там.

В результате в прошлом посте я разобрался, как работают примеры в учебнике вообще и как работает данный пример в частности. Запуск кода примера происходит в одной из функций, объявленных в скрипте codeBox.js (см. предыдущий пост). Код примера помещается в переменную runCode, а затем запускается (строка 351 скрипта) вот таким образом:
try {
    window["eval"].call(window, runCode);
} catch (e) {
    alert(e.constructor.name + ": " + e.message);
}

Я решил смоделировать ситуацию у себя на компьютере и написал вот такой код:
"use strict";

let runCode = `
    function sayHi() {
        alert("Hello");
    }

    // глобальные функции доступны как методы глобального объекта:
    window.sayHi();
`

window["eval"].call(window, runCode);

Я включил строгий режим в моем скрипте с помощью "use strict";, потому что в первых подразделах учебника его авторами было оговорено, что все примеры в учебнике запускаются в строгом режиме. Далее я поместил код примера из учебника в переменную runCode, использовав при этом обратные кавычки (по-английски «backticks»), потому что они позволяют легко вводить многострочные литералы. В конце скрипта код примера из учебника запускается с помощью встроенной функции eval.

Этот вариант скрипта у меня тоже запустился без ошибок. Тогда я стал прогонять скрипт codeBox.js на сайте учебника пошагово и заметил, что там строгий режим включается именно для скрипта, помещаемого в переменную runCode. Я изменил моделирующий код своего скрипта следующим образом:

"use strict";

let runCode = `
    "use strict"; // включаем строгий режим внутри runCode
    
    function sayHi() {
        alert("Hello");
    }

    // глобальные функции доступны как методы глобального объекта:
    window.sayHi();
`

window["eval"].call(window, runCode);

И вот уже для такого варианта скрипта получаем искомую ошибку «TypeError: window.sayHi is not a function».

В чем причина ошибки? Дело в том, что без включения строгого режима у eval не будет своего лексического окружения и функция sayHi будет считаться в данном случае глобальной и попадет в глобальный объект window, откуда ее можно будет вызвать указанным способом — window.sayHi();.

Если же в скрипте, передаваемом на исполнение функции eval, будет включен строгий режим, у eval будет своё лексическое окружение, функция sayHi будет считаться локальной, а не глобальной, и поэтому она не попадет в глобальный объект window, а потому обращение к ней через этот объект window.sayHi(); приведет к ошибке «TypeError: window.sayHi is not a function» (то есть по-русски: «Функции sayHi в объекте window не существует»).

В принципе, об этом было рассказано в подразделе 14.2 «Eval: выполнение строки кода» учебника.

Также об этом упомянуто в следующей большой статье, посвященной строгому режиму:
https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Strict_mode

Авторы учебника могут исправить эту ошибку, к примеру, передав скрипт в переменной runCode для исполнения функцией eval без включения в скрипте строгого режима.

JavaScript: порядок выполнения скриптов и модулей на HTML-странице

Запустил у себя на компьютере следующий скрипт из подраздела 1.1 «Браузерное окружение, спецификации» второй части учебника по JavaScript:

Файл myscript.js:
"use strict";

// заменим цвет фона на красный,
document.body.style.background = "red";

// а через секунду вернём как было
setTimeout(() => document.body.style.background = "", 1000);

И неожиданно получил ошибку «Uncaught TypeError: Cannot read property 'style' of null» (при запуске скрипта со страницы учебника ошибки не происходит).

Мне было понятно, что я неправильно написал HTML-страницу, с которой загружаю скрипт, но в первой части учебника проблем с нею не было:
<!doctype html>

<meta charset="utf-8">

<script src="myscript.js"></script>

Как оказалось, из этого мой браузер («Microsoft Edge» на движке «Chromium») конструирует такой код (посмотрел в инструментах разработчика (F12), пункт меню «Elements»):
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="myscript.js"></script>
  </head>
  <body></body>
</html>

И тут я вспомнил, что в подразделе 13.1 «Модули, введение» первой части учебника я читал, что обычные скрипты (как в данном случае у меня) загружаются и запускаются по мере загрузки HTML-страницы.

Получается, что мой скрипт начинает свою работу до появления у HTML-страницы элемента body (как видно выше, скрипт загружается в разделе head HTML-документа, до загрузки body, ведь документ загружается сверху вниз, от начала к концу), поэтому при попытке обращения к этому элементу не может его найти в HTML-документе. Из-за этого выдается ошибка.

(Добавление от 21 мая 2021 г.: сейчас читаю подраздел 1.3 «Навигация по DOM-элементам» второй части учебника и в нем про это тоже рассказано.)

С модулями (это такой же файл-скрипт, только в HTML у тега script должен быть атрибут type="module") такой ошибки не произойдет, потому что хоть модули и загружаются по мере загрузки HTML-страницы, но их запуск происходит после полной загрузки HTML-страницы.

Таким образом, чтобы исправить обсуждаемую ошибку, в данном случае есть минимум три возможных способа:

1. Загрузить файл myscript.js (сам этот файл не меняем) с HTML-страницы как модуль:
<!doctype html>

<meta charset="utf-8">

<script src="myscript.js" type="module"></script>

2. Загрузить файл myscript.js (сам этот файл не меняем) с HTML-страницы с атрибутом defer (на русский это слово переводится как «отложить»; имеется в виду — отложить запуск скрипта до полной загрузки HTML-страницы) в теге script:
<!doctype html>

<meta charset="utf-8">

<script src="myscript.js" defer></script>

3. Переставить загрузку и запуск скрипта myscript.js (сам этот файл не меняем) на HTML-странице после (ниже) открывающего тега body. Например, так:
<!doctype html>

<meta charset="utf-8">

<body></body>

<script src="myscript.js"></script>
Или так:
<!doctype html>

<meta charset="utf-8">

<body>
  <script src="myscript.js"></script>
</body>

Добавление от 02.10.2021 г.: более подробно эта тема рассмотрена в пятом разделе «Загрузка документа и ресурсов» второй части обсуждаемого учебника.